在以前的文章中學習了volatile關鍵字,volatile能夠保證變量在線程間的可見性,但他不能真正的保證線程安全。java
/** * @author cenkailun * @Date 9/5/17 * @Time 20:23 */
public class ConcurrentAddWithVolatile implements Runnable {
private static ConcurrentAddWithVolatile instance = new ConcurrentAddWithVolatile();
private static volatile int i = 0;
public static void increase() {
i++;
}
public void run() {
for (int j = 0; j < 1000000; j++) {
i++;
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(instance,"線程1");
Thread t2 = new Thread(instance, "線程2");
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
}複製代碼
如上述代碼所示,若是說兩個線程是正確的併發執行的話,最後獲得的結果應該是2000000,但結果每每是小於2000000。那麼這是爲何呢?安全
通過閱讀書籍,能夠得知,i++的這個操做,實際上是要分紅3步。bash
1. 讀取i的當前值到操做棧
2. 對i的當前值+1
3. 寫回i+1後的值複製代碼
通過了上述3步,才完成了i++ 的這個操做,volatile保證了寫回內存後,i的最新值可以被其餘線程獲取,但i++的這三個動做不是一個總體,即不是原子操做,是能夠被拆開的。多線程
好比,線程1和2同時讀取了i爲0,並各自在本身的線程中計算獲得i=1,前後寫入這個i的值,致使雖然i++被執行了兩次,可是實際i的值只增長了1。併發
若是要解決這個問題,就要保證多個線程在對i進行++ 這個操做時徹底同步,即i++的這三步是一塊兒完成的,當線程1在寫入時,其餘線程不能讀也不能寫,由於在線程1寫完以前,其餘線程讀到的確定是一個過時的數據。jvm
Java提供了synchronized來實現這個功能,保證多線程執行時候的同步,某一時刻只有一個線程能夠對synchronized關鍵字保護起來的區域進行操做,相對於volatile來講是比較重量級的。學習
Java的synchronized關鍵字具體表現有如下三種形式:spa
下面是一個示例,將synchronized做用於一個給定對象instance,每當線程要進入被包裹的代碼塊,會請求instance的鎖。若是有其餘線程已經持有了這把鎖,那麼新到的線程就必須等待,這樣保證了每次只有一個線程會執行i++操做。線程
/** * @author cenkailun * @Date 9/5/17 * @Time 20:23 */
public class ConcurrentAddWithVolatile implements Runnable {
private static ConcurrentAddWithVolatile instance = new ConcurrentAddWithVolatile();
private static volatile int i = 0;
public static void increase() {
i++;
}
public void run() {
for (int j = 0; j < 1000000; j++) {
synchronized (instance) { //同步代碼塊
i++;
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(instance,"線程1");
Thread t2 = new Thread(instance, "線程2");
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
}複製代碼
對於java中的代碼塊同步,JVM是基於進入和退出Monitor對象來實現代碼塊同步的,將monitorenter指令插入到同步代碼塊的開始位置,monitorexit插入到方法結束處和異常處,每個對象都有一個monitor與之對應,當一個monitor被持有後,它將處於鎖定狀態。線程執行到monitorenter指令時,將會嘗試獲取對象所對應的monitor的全部權,即嘗試得到對象的鎖。code
以下面字節碼所示,表明上文代碼中的同步代碼塊。
13: monitorenter
14: getstatic #2 // Field i:I
17: iconst_1
18: iadd
19: putstatic #2 // Field i:I
22: aload_2
23: monitorexit複製代碼
對於實例方法或者靜態方法上加的synchronized關鍵字,在方法上會有一個標誌位表明,以下面字節碼所示。
public synchronized void increase();
flags: ACC_PUBLIC, ACC_SYNCHRONIZED複製代碼
在我看來,synchronized相對於volatile的強大之處在於保證了線程安全性以及作到了線程同步,同時也能作到volatile提供的線程間可見性以及有序性。從可見性上來講,線程經過持有鎖的方式獲取變量的最新值。從有序性上來講,synchronized限制每次只有一個線程能夠訪問同步的代碼,不管內部指令順序如何被打亂,jvm會保證最終執行的結果老是同樣,其餘線程只能在得到鎖後讀取結果數據,不會讀到中間值,因此有序性問題也獲得瞭解決。