在之前的文章中學(xué)習(xí)了volatile關(guān)鍵字蹂窖,volatile可以保證變量在線程間的可見性虑省,但他不能真正的保證線程安全赌躺。
/**
* @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);
}
}
如上述代碼所示逾滥,如果說兩個(gè)線程是正確的并發(fā)執(zhí)行的話,最后得到的結(jié)果應(yīng)該是2000000,但結(jié)果往往是小于2000000禁添。那么這是為什么呢撮胧?
經(jīng)過閱讀書籍,可以得知老翘,i++的這個(gè)操作芹啥,其實(shí)是要分成3步。
1. 讀取i的當(dāng)前值到操作棧
2. 對i的當(dāng)前值+1
3. 寫回i+1后的值
經(jīng)過了上述3步铺峭,才完成了i++ 的這個(gè)操作墓怀,volatile保證了寫回內(nèi)存后,i的最新值能夠被其他線程獲取逛薇,但i++的這三個(gè)動(dòng)作不是一個(gè)整體捺疼,即不是原子操作疏虫,是可以被拆開的永罚。
比如,線程1和2同時(shí)讀取了i為0,并各自在自己的線程中計(jì)算得到i=1卧秘,先后寫入這個(gè)i的值呢袱,導(dǎo)致雖然i++被執(zhí)行了兩次,但是實(shí)際i的值只增加了1翅敌。
如果要解決這個(gè)問題羞福,就要保證多個(gè)線程在對i進(jìn)行++ 這個(gè)操作時(shí)完全同步,即i++的這三步是一起完成的蚯涮,當(dāng)線程1在寫入時(shí)治专,其他線程不能讀也不能寫卖陵,因?yàn)樵诰€程1寫完之前,其他線程讀到的肯定是一個(gè)過期的數(shù)據(jù)张峰。
Java提供了synchronized來實(shí)現(xiàn)這個(gè)功能泪蔫,保證多線程執(zhí)行時(shí)候的同步,某一時(shí)刻只有一個(gè)線程可以對synchronized關(guān)鍵字保護(hù)起來的區(qū)域進(jìn)行操作喘批,相對于volatile來說是比較重量級的撩荣。
Java的synchronized關(guān)鍵字具體表現(xiàn)有以下三種形式:
- 作用于實(shí)例方法,鎖的是當(dāng)前實(shí)例對象饶深。
- 作用于靜態(tài)方法餐曹,鎖的是當(dāng)前類。
- 作用于代碼塊敌厘,鎖的是Synchronized里配置的對象台猴。
下面是一個(gè)示例,將synchronized作用于一個(gè)給定對象instance俱两,每當(dāng)線程要進(jìn)入被包裹的代碼塊卿吐,會請求instance的鎖。如果有其他線程已經(jīng)持有了這把鎖锋华,那么新到的線程就必須等待嗡官,這樣保證了每次只有一個(gè)線程會執(zhí)行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是基于進(jìn)入和退出Monitor對象來實(shí)現(xiàn)代碼塊同步的衍腥,將monitorenter指令插入到同步代碼塊的開始位置,monitorexit插入到方法結(jié)束處和異常處纳猫,每一個(gè)對象都有一個(gè)monitor與之對應(yīng)婆咸,當(dāng)一個(gè)monitor被持有后,它將處于鎖定狀態(tài)芜辕。線程執(zhí)行到monitorenter指令時(shí)尚骄,將會嘗試獲取對象所對應(yīng)的monitor的所有權(quán),即嘗試獲得對象的鎖侵续。
如下面字節(jié)碼所示倔丈,代表上文代碼中的同步代碼塊。
13: monitorenter
14: getstatic #2 // Field i:I
17: iconst_1
18: iadd
19: putstatic #2 // Field i:I
22: aload_2
23: monitorexit
對于實(shí)例方法或者靜態(tài)方法上加的synchronized關(guān)鍵字状蜗,在方法上會有一個(gè)標(biāo)志位代表需五,如下面字節(jié)碼所示。
public synchronized void increase();
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
在我看來轧坎,synchronized相對于volatile的強(qiáng)大之處在于保證了線程安全性以及做到了線程同步宏邮,同時(shí)也能做到volatile提供的線程間可見性以及有序性。從可見性上來說,線程通過持有鎖的方式獲取變量的最新值蜜氨。從有序性上來說械筛,synchronized限制每次只有一個(gè)線程可以訪問同步的代碼,無論內(nèi)部指令順序如何被打亂飒炎,jvm會保證最終執(zhí)行的結(jié)果總是一樣变姨,其他線程只能在獲得鎖后讀取結(jié)果數(shù)據(jù),不會讀到中間值厌丑,所以有序性問題也得到了解決定欧。