摘要:在之前的文章中學(xué)習(xí)了關(guān)鍵字,可以保證變量在線程間的可見性,但他不能真正的保證線程安全。線程執(zhí)行到指令時(shí),將會(huì)嘗試獲取對(duì)象所對(duì)應(yīng)的的所有權(quán),即嘗試獲得對(duì)象的鎖。從可見性上來說,線程通過持有鎖的方式獲取變量的最新值。
在之前的文章中學(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. 對(duì)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è)線程在對(duì)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è)線程可以對(duì)synchronized關(guān)鍵字保護(hù)起來的區(qū)域進(jìn)行操作,相對(duì)于volatile來說是比較重量級(jí)的。
Java的synchronized關(guān)鍵字具體表現(xiàn)有以下三種形式:
作用于實(shí)例方法,鎖的是當(dāng)前實(shí)例對(duì)象。
作用于靜態(tài)方法,鎖的是當(dāng)前類。
作用于代碼塊,鎖的是Synchronized里配置的對(duì)象。
下面是一個(gè)示例,將synchronized作用于一個(gè)給定對(duì)象instance,每當(dāng)線程要進(jìn)入被包裹的代碼塊,會(huì)請(qǐng)求instance的鎖。如果有其他線程已經(jīng)持有了這把鎖,那么新到的線程就必須等待,這樣保證了每次只有一個(gè)線程會(huì)執(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); } }
對(duì)于java中的代碼塊同步,JVM是基于進(jìn)入和退出Monitor對(duì)象來實(shí)現(xiàn)代碼塊同步的,將monitorenter指令插入到同步代碼塊的開始位置,monitorexit插入到方法結(jié)束處和異常處,每一個(gè)對(duì)象都有一個(gè)monitor與之對(duì)應(yīng),當(dāng)一個(gè)monitor被持有后,它將處于鎖定狀態(tài)。線程執(zhí)行到monitorenter指令時(shí),將會(huì)嘗試獲取對(duì)象所對(duì)應(yīng)的monitor的所有權(quán),即嘗試獲得對(duì)象的鎖。
如下面字節(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
對(duì)于實(shí)例方法或者靜態(tài)方法上加的synchronized關(guān)鍵字,在方法上會(huì)有一個(gè)標(biāo)志位代表,如下面字節(jié)碼所示。
public synchronized void increase(); flags: ACC_PUBLIC, ACC_SYNCHRONIZED
在我看來,synchronized相對(duì)于volatile的強(qiáng)大之處在于保證了線程安全性以及做到了線程同步,同時(shí)也能做到volatile提供的線程間可見性以及有序性。從可見性上來說,線程通過持有鎖的方式獲取變量的最新值。從有序性上來說,synchronized限制每次只有一個(gè)線程可以訪問同步的代碼,無論內(nèi)部指令順序如何被打亂,jvm會(huì)保證最終執(zhí)行的結(jié)果總是一樣,其他線程只能在獲得鎖后讀取結(jié)果數(shù)據(jù),不會(huì)讀到中間值,所以有序性問題也得到了解決。
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/70341.html
摘要:目前看的部分主要是這個(gè)關(guān)鍵字。語言提供了,保證了所有線程能看到共享變量最新的值。前綴的指令在多核處理器下會(huì)做兩件事情將當(dāng)前處理器緩存行的數(shù)據(jù)寫回到系統(tǒng)內(nèi)存。 這一章節(jié)的話,主要是講一下在并發(fā)操作中常見的volatile、synchronized以及原子操作的相關(guān)知識(shí)。 目前看的部分主要是volatile這個(gè)關(guān)鍵字。 volatile 根據(jù)Java語言規(guī)范第3版中對(duì)volatile的定義...
摘要:前言今天的筆記來了解一下原子操作以及中如何實(shí)現(xiàn)原子操作。概念原子本意是不能被進(jìn)一步分割的最小粒子,而原子操作意為不可被中斷的一個(gè)或一系列操作。處理器實(shí)現(xiàn)原子操作處理器會(huì)保證基本內(nèi)存操作的原子性。 showImg(https://segmentfault.com/img/bVVIRA?w=1242&h=536); 前言 今天的筆記來了解一下原子操作以及Java中如何實(shí)現(xiàn)原子操作。 概念 ...
摘要:前言并發(fā)編程的目的是讓程序跑的更快,但并不是啟動(dòng)更多的線程,這個(gè)程序就跑的更快。盡可能降低上下文切換的次數(shù),有助于提高并發(fā)效率。死鎖并發(fā)編程中的另一挑戰(zhàn)是死鎖,會(huì)造成系統(tǒng)功能不可用。 前言 并發(fā)編程的目的是讓程序跑的更快,但并不是啟動(dòng)更多的線程,這個(gè)程序就跑的更快。有以下幾種挑戰(zhàn)。 挑戰(zhàn)及方案 上下文切換 單核CPU上執(zhí)行多線程任務(wù),通過給每個(gè)線程分配CPU時(shí)間片的方式來實(shí)現(xiàn)這個(gè)機(jī)制。...
摘要:開頭正式開啟我入職的里程,現(xiàn)在已是工作了一個(gè)星期了,這個(gè)星期算是我入職的過渡期,算是知道了學(xué)校生活和工作的差距了,總之,盡快習(xí)慣這種生活吧。當(dāng)時(shí)是看的廖雪峰的博客自己也用做爬蟲寫過幾篇博客,不過有些是在前人的基礎(chǔ)上寫的。 showImg(https://segmentfault.com/img/remote/1460000010867984); 開頭 2017.08.21 正式開啟我...
摘要:假設(shè)不發(fā)生編譯器重排和指令重排,線程修改了的值,但是修改以后,的值可能還沒有寫回到主存中,那么線程得到就是很自然的事了。同理,線程對(duì)于的賦值操作也可能沒有及時(shí)刷新到主存中。線程的最后操作與線程發(fā)現(xiàn)線程已經(jīng)結(jié)束同步。 很久沒更新文章了,對(duì)隔三差五過來刷更新的讀者說聲抱歉。 關(guān)于 Java 并發(fā)也算是寫了好幾篇文章了,本文將介紹一些比較基礎(chǔ)的內(nèi)容,注意,閱讀本文需要一定的并發(fā)基礎(chǔ)。 本文的...
閱讀 918·2021-10-27 14:19
閱讀 1145·2021-10-15 09:42
閱讀 1567·2021-09-14 18:02
閱讀 765·2019-08-30 13:09
閱讀 3010·2019-08-29 15:08
閱讀 2113·2019-08-28 18:05
閱讀 977·2019-08-26 10:25
閱讀 2813·2019-08-23 16:28