並發編程的原則:設計並發編程的目的是為了使程序獲得更高的執行效率,但絕不能出現數據一致性(數據准確)問題,如果並發程序連最基本的執行結果准確性都無法保證,那並發編程就沒有任何意義。
為什麼會出現數據不正確:
如果一個資源(變量,對象,文件,數據庫)可以同時被很多線程使用就會出現數據不一致問題,也就是我們說的線程安全問題。這樣的資源被稱為共享資源或臨界區。
舉個例子:
一個共享變量m,現在有兩個線程同時對它進行累加操作,各執行10000次,那麼我麼期待的結果是20000,但實際上並不是這樣的。看代碼:
package com.jalja.base.threadTest; public class SynchronizedTest implements Runnable{ private static volatile int m=0; public static void main(String[] args) { Runnable run=new SynchronizedTest(); Thread thread1=new Thread(run); Thread thread2=new Thread(run); thread1.start(); thread2.start(); try { //join() 使main線程等待這連個線程執行結束後繼續執行下面的代碼 thread1.join(); thread2.join(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("m的最終結果:"+m); } public void run() { for(int i=0;i<10000;i++){ m++; } } }
無論運行多少次 m總是小於20000。為什麼會出現這樣的結果呢?當線程thread1在將m++的結果寫入內存之前,線程thread2已經從內存中讀取了m的值,並在這個值(過時值)上進行++操作,最後將m=1寫入內存中(可能就覆蓋了thread1計算的m=1的值,也可能是出現thread1覆蓋了thread2的值)。出現這樣的結果是必然的。
如何控制多線程操作共享數據引起的數據准確性問題呢?使用“序列化訪問臨界資源”的方案,即在同一時刻,只能有一個線程訪問臨界資源,也稱作同步互斥訪問,也就是保證我們的共享資源每次只能被一個線程使用,一旦該資源被線程使用,其他線程將不得擁有使用權。在Java中,提供了兩種方式來實現同步互斥訪問:synchronized和Lock。
互斥鎖:顧名思義,就是互斥訪問目的的鎖。
舉個簡單的例子:如果對臨界資源加上互斥鎖,當一個線程在訪問該臨界資源時,其他線程便只能等待。
在Java中,每一個對象都擁有一個鎖標記(monitor),也稱為監視器,多線程同時訪問某個對象時,只有擁有該對象鎖的線程才能訪問。
在Java中,可以使用synchronized關鍵字來標記一個需要同步的方法或者同步代碼塊,當某個線程調用該對象的synchronized方法或者訪問synchronized代碼塊時,這個線程便獲得了該對象的鎖,其他線程暫時無法訪問這個方法,只有等待這個方法執行完畢或者代碼塊執行完畢,這個線程才會釋放該對象的鎖,其他線程才能執行這個方法或者代碼塊。通過這種方式達到我們上面提到的在同一時刻,只能有一個線程訪問臨界資源。
synchronized用法:
1、同步代碼塊
synchronized(synObject) { }
用法:將synchronized作用於一個給定的對象或類的一個屬性,所以每當有線程執行這段代碼塊,該線程會先請求獲取對象synObject的鎖,如果該鎖已被其他線程占有,那麼新的線程只能等待,從而使得其他線程無法同時訪問該代碼塊。
package com.jalja.base.threadTest; public class SynchronizedTest implements Runnable{ private static volatile int m=0; public static void main(String[] args) { Runnable run=new SynchronizedTest(); Thread thread1=new Thread(run); Thread thread2=new Thread(run); thread1.start(); thread2.start(); try { //join() 使main線程等待這連個線程執行結束後繼續執行下面的代碼 thread1.join(); thread2.join(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("m的最終結果:"+m); } public void run() { synchronized (this) { for(int i=0;i<10000;i++){ m++; } } } }
該代碼是使用當前對象作為互斥鎖,下面我們使用類的一個屬性作為互斥鎖。
package com.jalja.base.threadTest; public class SynchronizedTest implements Runnable{ private static volatile int m=0; private Object object=new Object(); public static void main(String[] args) { Runnable run=new SynchronizedTest(); Thread thread1=new Thread(run); Thread thread2=new Thread(run); thread1.start(); thread2.start(); try { //join() 使main線程等待這連個線程執行結束後繼續執行下面的代碼 thread1.join(); thread2.join(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("m的最終結果:"+m); } public void run() { synchronized (object) { for(int i=0;i<10000;i++){ m++; } } } }
1、同步方法
package com.jalja.base.threadTest; public class SynchronizedTest implements Runnable{ private static int m=0; public static void main(String[] args) { Runnable run=new SynchronizedTest(); Thread thread1=new Thread(run); Thread thread2=new Thread(run); thread1.start(); thread2.start(); try { //join() 使main線程等待這連個線程執行結束後繼續執行下面的代碼 thread1.join(); thread2.join(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("m的最終結果:"+m); } public synchronized void run() { for(int i=0;i<10000;i++){ m++; } } }
這段代碼中,synchronzied作用於一個實例方法,就是說當線程在進入run()方法前,必須獲取當前對象實例鎖,本例中對象實例鎖就是run。在這裡提醒大家認真看這三段代碼中main函數的實現,在這裡我們使用Runnable創建兩個線程,並且這兩個線程都指向同一個Runnable接口實例,這樣才能保證兩個線程在工作中,使用同一個對象鎖,從而保證線程安全。
一種錯誤的理解:
package com.jalja.base.threadTest; public class SynchronizedTest implements Runnable{ private static int m=0; public static void main(String[] args) { Thread thread1=new Thread(new SynchronizedTest()); Thread thread2=new Thread(new SynchronizedTest()); thread1.start(); thread2.start(); try { //join() 使main線程等待這連個線程執行結束後繼續執行下面的代碼 thread1.join(); thread2.join(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("m的最終結果:"+m); } public synchronized void run() { for(int i=0;i<10000;i++){ m++; } } }
這段代碼的運行結果是錯誤的,請看main函數的實現方式,使用Runnable創建兩個線程,但是兩個線程擁有各自的Runnable實例,所以當thread1線程進入同步方法時加的是自己的對象實例鎖,而thread2在進入同步方法時關注的是自己的實例鎖,兩個線程擁有不同的對象實例鎖,因此無法達到互斥的要求。
略作改動:
package com.jalja.base.threadTest; public class SynchronizedTest implements Runnable{ private static int m=0; public static void main(String[] args) { Thread thread1=new Thread(new SynchronizedTest()); Thread thread2=new Thread(new SynchronizedTest()); thread1.start(); thread2.start(); try { //join() 使main線程等待這連個線程執行結束後繼續執行下面的代碼 thread1.join(); thread2.join(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("m的最終結果:"+m); } public void run() { for(int i=0;i<10000;i++){ count(); } } public static synchronized void count(){ m++; } }
這樣處理結果就是我麼想要的了,在這裡我們將處理業務的代碼封裝成一個靜態的同步方法,那現在訪問該同步方法需要的是當前類的鎖,而類在內存中只有一份,所以無論如何,他們使用的都是同一個鎖。