0.先導的問題代碼
下面的程式碼示範了一個計數器,兩個執行緒同時對i進行累加的操作,各執行1000000次.我們期望的結果肯定是i=2000000.但是我們多次執行以後,會發現i的值永遠小於2000000.這是因為,兩個線程同時對i進行寫入的時候,其中一個線程的結果會覆蓋另外一個.
public class AccountingSync implements Runnable { static int i = 0; public void increase() { i++; } @Override public void run() { for (int j = 0; j < 1000000; j++) { increase(); } } public static void main(String[] args) throws InterruptedException { AccountingSync accountingSync = new AccountingSync(); Thread t1 = new Thread(accountingSync); Thread t2 = new Thread(accountingSync); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(i); } }
要從根本上解決這個問題,我們必須保證多個執行緒在對i進行操作的時候,要完全的同步.也就是說到A執行緒對i進行寫入的時候,B執行緒不僅不可以寫入,連讀取都不可以.
1.synchronized關鍵字的作用
關鍵字synchronized的作用其實就是實現線程間的同步.它的工作就是對同步的代碼進行加鎖,使得每一次,只能有一個線程進入同步塊,從而保證執行緒間的安全性.就像上面的程式碼中,i++的操作只能同時又一個執行緒在執行.
2.synchronized關鍵字的用法
指定物件加鎖:對給定的物件進行加鎖,進入同步程式碼區塊要獲得給定物件的鎖
直接作用於實例方法:相當於對當前實例加鎖,進入同步程式碼區塊要獲得當前實例的鎖(這要求創建Thread的時候,要用同一個Runnable的實例才可以)
直接作用於靜態方法:相當於給當前類別加鎖,進入同步程式碼區塊前要取得目前類別的鎖定
2.1指定物件加鎖
下面的程式碼,將synchronized作用於一個給定的對象.這裡有一個注意的,給定對像一定要是static的,否則我們每次new一個線程出來,彼此並不共享該對象,加鎖的意義也就不存在了.
public class AccountingSync implements Runnable { final static Object OBJECT = new Object(); static int i = 0; public void increase() { i++; } @Override public void run() { for (int j = 0; j < 1000000; j++) { synchronized (OBJECT) { increase(); } } } public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(new AccountingSync()); Thread t2 = new Thread(new AccountingSync()); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(i); } }
2.2直接作用於實例方法
synchronized關鍵字作用於實例方法,就是說在進入執行我們之前建立方法之前必須取得實例的鎖定實例. Thread實例的時候,要使用同一個Runnable的物件實例.否則,執行緒的鎖都不在同一個實例上面,無從去談加鎖/同步的問題了.
public class AccountingSync implements Runnable { static int i = 0; public synchronized void increase() { i++; } @Override public void run() { for (int j = 0; j < 1000000; j++) { increase(); } } public static void main(String[] args) throws InterruptedException { AccountingSync accountingSync = new AccountingSync(); Thread t1 = new Thread(accountingSync); Thread t2 = new Thread(accountingSync); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(i); } }
三行,說明關鍵字作用於實例方法上的正確用法.
2.3直接作用於靜態方法
將synchronized關鍵字作用在static方法上,就不用像上面的例子中,兩個線程要指向同一個例子一個Runnable方法.因為方法塊需要請求的是當前類別的鎖,而不是當前實例,線程間還是可以正確同步的.
public class AccountingSync implements Runnable { static int i = 0; public static synchronized void increase() { i++; } @Override public void run() { for (int j = 0; j < 1000000; j++) { increase(); } } public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(new AccountingSync()); Thread t2 = new Thread(new AccountingSync()); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(i); } }
3.錯誤的加鎖
從上面的例子裡 我們知道,如果我們需要一個計數器應用,為了保證數據的正確性,我們自然會需要對計數器加鎖,因此,我們可能會寫出下面的代碼:
public class BadLockOnInteger implements Runnable { static Integer i = 0; @Override public void run() { for (int j = 0; j < 1000000; j++) { synchronized (i) { i++; } } } public static void main(String[] args) throws InterruptedException { BadLockOnInteger badLockOnInteger = new BadLockOnInteger(); Thread t1 = new Thread(badLockOnInteger); Thread t2 = new Thread(badLockOnInteger); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(i); } }當我們運行上面代碼的時候,會發現輸出的i很小.這說明線程並沒有安全. 要解釋這個問題,要從Integer說起:在Java中,Integer屬於不變對象,和String一樣,對像一旦被創建,就不能被修改了.如果你有一個Integer=1,那麼它就永遠都是1.如果你想讓這個物件=2呢?只能重新建立一個Integer.每次i++之後,相當於呼叫了Integer的valueOf方法,我們看一下Integer的valueOf方法的源碼:
public static Integer valueOf(int i) { if (i >= IntegerCache.low && i <= IntegerCache.high) return IntegerCache.cache[i + (-IntegerCache.low)]; return new Integer(i); }
Integer.valueOf()實際上是一個工廠方法,他會傾向於一個新的Integer對象,並將值複製給一個新的Integeri;