Java 并發(fā)編程學(xué)習(xí)筆記之核心理論基礎(chǔ)
并發(fā)編程是Java程序員最重要的技能之一,也是最難掌握的一種技能。它要求編程者對(duì)計(jì)算機(jī)最底層的運(yùn)作原理有深刻的理解,同時(shí)要求編程者邏輯清晰、思維縝密,這樣才能寫出高效、安全、可靠的多線程并發(fā)程序。本系列會(huì)從線程間協(xié)調(diào)的方式(wait、notify、notifyAll)、Synchronized及Volatile的本質(zhì)入手,詳細(xì)解釋JDK為我們提供的每種并發(fā)工具和底層實(shí)現(xiàn)機(jī)制。在此基礎(chǔ)上,我們會(huì)進(jìn)一步分析java.util.concurrent包的工具類,包括其使用方式、實(shí)現(xiàn)源碼及其背后的原理。本文是該系列的第一篇文章,是這系列中最核心的理論部分,之后的文章都會(huì)以此為基礎(chǔ)來分析和解釋。
一、共享性
數(shù)據(jù)共享性是線程安全的主要原因之一。如果所有的數(shù)據(jù)只是在線程內(nèi)有效,那就不存在線程安全性問題,這也是我們?cè)诰幊痰臅r(shí)候經(jīng)常不需要考慮線程安全的主要原因之一。但是,在多線程編程中,數(shù)據(jù)共享是不可避免的。最典型的場(chǎng)景是數(shù)據(jù)庫中的數(shù)據(jù),為了保證數(shù)據(jù)的一致性,我們通常需要共享同一個(gè)數(shù)據(jù)庫中數(shù)據(jù),即使是在主從的情況下,訪問的也同一份數(shù)據(jù),主從只是為了訪問的效率和數(shù)據(jù)安全,而對(duì)同一份數(shù)據(jù)做的副本。我們現(xiàn)在,通過一個(gè)簡(jiǎn)單的示例來演示多線程下共享數(shù)據(jù)導(dǎo)致的問題:
代碼段一:
package com.paddx.test.concurrent; public class ShareData { public static int count = 0; public static void main(String[] args) { final ShareData data = new ShareData(); for (int i = 0; i < 10; i++) { new Thread(new Runnable() { @Override public void run() { try { //進(jìn)入的時(shí)候暫停1毫秒,增加并發(fā)問題出現(xiàn)的幾率 Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } for (int j = 0; j < 100; j++) { data.addCount(); } System.out.print(count + " "); } }).start(); } try { //主程序暫停3秒,以保證上面的程序執(zhí)行完成 Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("count=" + count); } public void addCount() { count++; } }
上述代碼的目的是對(duì)count進(jìn)行加一操作,執(zhí)行1000次,不過這里是通過10個(gè)線程來實(shí)現(xiàn)的,每個(gè)線程執(zhí)行100次,正常情況下,應(yīng)該輸出1000。不過,如果你運(yùn)行上面的程序,你會(huì)發(fā)現(xiàn)結(jié)果卻不是這樣。下面是某次的執(zhí)行結(jié)果(每次運(yùn)行的結(jié)果不一定相同,有時(shí)候也可能獲取到正確的結(jié)果):
可以看出,對(duì)共享變量操作,在多線程環(huán)境下很容易出現(xiàn)各種意想不到的的結(jié)果。
二、互斥性
資源互斥是指同時(shí)只允許一個(gè)訪問者對(duì)其進(jìn)行訪問,具有唯一性和排它性。我們通常允許多個(gè)線程同時(shí)對(duì)數(shù)據(jù)進(jìn)行讀操作,但同一時(shí)間內(nèi)只允許一個(gè)線程對(duì)數(shù)據(jù)進(jìn)行寫操作。所以我們通常將鎖分為共享鎖和排它鎖,也叫做讀鎖和寫鎖。如果資源不具有互斥性,即使是共享資源,我們也不需要擔(dān)心線程安全。例如,對(duì)于不可變的數(shù)據(jù)共享,所有線程都只能對(duì)其進(jìn)行讀操作,所以不用考慮線程安全問題。但是對(duì)共享數(shù)據(jù)的寫操作,一般就需要保證互斥性,上述例子中就是因?yàn)闆]有保證互斥性才導(dǎo)致數(shù)據(jù)的修改產(chǎn)生問題。Java 中提供多種機(jī)制來保證互斥性,最簡(jiǎn)單的方式是使用Synchronized。現(xiàn)在我們?cè)谏厦娉绦蛑屑由蟂ynchronized再執(zhí)行:
代碼段二:
package com.paddx.test.concurrent; public class ShareData { public static int count = 0; public static void main(String[] args) { final ShareData data = new ShareData(); for (int i = 0; i < 10; i++) { new Thread(new Runnable() { @Override public void run() { try { //進(jìn)入的時(shí)候暫停1毫秒,增加并發(fā)問題出現(xiàn)的幾率 Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } for (int j = 0; j < 100; j++) { data.addCount(); } System.out.print(count + " "); } }).start(); } try { //主程序暫停3秒,以保證上面的程序執(zhí)行完成 Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("count=" + count); } /** * 增加 synchronized 關(guān)鍵字 */ public synchronized void addCount() { count++; } }
現(xiàn)在再執(zhí)行上述代碼,會(huì)發(fā)現(xiàn)無論執(zhí)行多少次,返回的最終結(jié)果都是1000。
三、原子性
原子性就是指對(duì)數(shù)據(jù)的操作是一個(gè)獨(dú)立的、不可分割的整體。換句話說,就是一次操作,是一個(gè)連續(xù)不可中斷的過程,數(shù)據(jù)不會(huì)執(zhí)行的一半的時(shí)候被其他線程所修改。保證原子性的最簡(jiǎn)單方式是操作系統(tǒng)指令,就是說如果一次操作對(duì)應(yīng)一條操作系統(tǒng)指令,這樣肯定可以能保證原子性。但是很多操作不能通過一條指令就完成。例如,對(duì)long類型的運(yùn)算,很多系統(tǒng)就需要分成多條指令分別對(duì)高位和低位進(jìn)行操作才能完成。還比如,我們經(jīng)常使用的整數(shù) i++ 的操作,其實(shí)需要分成三個(gè)步驟:(1)讀取整數(shù) i 的值;(2)對(duì) i 進(jìn)行加一操作;(3)將結(jié)果寫回內(nèi)存。這個(gè)過程在多線程下就可能出現(xiàn)如下現(xiàn)象:
這也是代碼段一執(zhí)行的結(jié)果為什么不正確的原因。對(duì)于這種組合操作,要保證原子性,最常見的方式是加鎖,如Java中的Synchronized或Lock都可以實(shí)現(xiàn),代碼段二就是通過Synchronized實(shí)現(xiàn)的。除了鎖以外,還有一種方式就是CAS(Compare And Swap),即修改數(shù)據(jù)之前先比較與之前讀取到的值是否一致,如果一致,則進(jìn)行修改,如果不一致則重新執(zhí)行,這也是樂觀鎖的實(shí)現(xiàn)原理。不過CAS在某些場(chǎng)景下不一定有效,比如另一線程先修改了某個(gè)值,然后再改回原來值,這種情況下,CAS是無法判斷的。
四、可見性
要理解可見性,需要先對(duì)JVM的內(nèi)存模型有一定的了解,JVM的內(nèi)存模型與操作系統(tǒng)類似,如圖所示:
從這個(gè)圖中我們可以看出,每個(gè)線程都有一個(gè)自己的工作內(nèi)存(相當(dāng)于CPU高級(jí)緩沖區(qū),這么做的目的還是在于進(jìn)一步縮小存儲(chǔ)系統(tǒng)與CPU之間速度的差異,提高性能),對(duì)于共享變量,線程每次讀取的是工作內(nèi)存中共享變量的副本,寫入的時(shí)候也直接修改工作內(nèi)存中副本的值,然后在某個(gè)時(shí)間點(diǎn)上再將工作內(nèi)存與主內(nèi)存中的值進(jìn)行同步。這樣導(dǎo)致的問題是,如果線程1對(duì)某個(gè)變量進(jìn)行了修改,線程2卻有可能看不到線程1對(duì)共享變量所做的修改。通過下面這段程序我們可以演示一下不可見的問題:
package com.paddx.test.concurrent; public class VisibilityTest { private static boolean ready; private static int number; private static class ReaderThread extends Thread { public void run() { try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } if (!ready) { System.out.println(ready); } System.out.println(number); } } private static class WriterThread extends Thread { public void run() { try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } number = 100; ready = true; } } public static void main(String[] args) { new WriterThread().start(); new ReaderThread().start(); } }
從直觀上理解,這段程序應(yīng)該只會(huì)輸出100,ready的值是不會(huì)打印出來的。實(shí)際上,如果多次執(zhí)行上面代碼的話,可能會(huì)出現(xiàn)多種不同的結(jié)果,下面是我運(yùn)行出來的某兩次的結(jié)果:
當(dāng)然,這個(gè)結(jié)果也只能說是有可能是可見性造成的,當(dāng)寫線程(WriterThread)設(shè)置ready=true后,讀線程(ReaderThread)看不到修改后的結(jié)果,所以會(huì)打印false,對(duì)于第二個(gè)結(jié)果,也就是執(zhí)行if (!ready)時(shí)還沒有讀取到寫線程的結(jié)果,但執(zhí)行System.out.println(ready)時(shí)讀取到了寫線程執(zhí)行的結(jié)果。不過,這個(gè)結(jié)果也有可能是線程的交替執(zhí)行所造成的。Java 中可通過Synchronized或Volatile來保證可見性,具體細(xì)節(jié)會(huì)在后續(xù)的文章中分析。
五、順序性
為了提高性能,編譯器和處理器可能會(huì)對(duì)指令做重排序。重排序可以分為三種:
?。?)編譯器優(yōu)化的重排序。編譯器在不改變單線程程序語義的前提下,可以重新安排語句的執(zhí)行順序。
?。?)指令級(jí)并行的重排序?,F(xiàn)代處理器采用了指令級(jí)并行技術(shù)(Instruction-Level Parallelism, ILP)來將多條指令重疊執(zhí)行。如果不存在數(shù)據(jù)依賴性,處理器可以改變語句對(duì)應(yīng)機(jī)器指令的執(zhí)行順序。
?。?)內(nèi)存系統(tǒng)的重排序。由于處理器使用緩存和讀/寫緩沖區(qū),這使得加載和存儲(chǔ)操作看上去可能是在亂序執(zhí)行。
我們可以直接參考一下JSR 133 中對(duì)重排序問題的描述:
?。?) ?。?)
先看上圖中的(1)源碼部分,從源碼來看,要么指令 1 先執(zhí)行要么指令 3先執(zhí)行。如果指令 1 先執(zhí)行,r2不應(yīng)該能看到指令 4 中寫入的值。如果指令 3 先執(zhí)行,r1不應(yīng)該能看到指令 2 寫的值。但是運(yùn)行結(jié)果卻可能出現(xiàn)r2==2,r1==1的情況,這就是“重排序”導(dǎo)致的結(jié)果。上圖(2)即是一種可能出現(xiàn)的合法的編譯結(jié)果,編譯后,指令1和指令2的順序可能就互換了。因此,才會(huì)出現(xiàn)r2==2,r1==1的結(jié)果。Java 中也可通過Synchronized或Volatile來保證順序性。
六 總結(jié)
本文對(duì)Java 并發(fā)編程中的理論基礎(chǔ)進(jìn)行了講解,有些東西在后續(xù)的分析中還會(huì)做更詳細(xì)的討論,如可見性、順序性等。后續(xù)的文章都會(huì)以本章內(nèi)容作為理論基礎(chǔ)來討論。如果大家能夠很好的理解上述內(nèi)容,相信無論是去理解其他并發(fā)編程的文章還是在平時(shí)的并發(fā)編程的工作中,都能夠?qū)Υ蠹矣泻芎玫膸椭?/p>
相關(guān)文章
Mybatis中resultMap的Colum和property屬性詳解
這篇文章主要介紹了Mybatis中resultMap的Colum和property屬性,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教。2022-01-01詳解mybatis中association和collection的column傳入多個(gè)參數(shù)問題
這篇文章主要介紹了詳解mybatis中association和collection的column傳入多個(gè)參數(shù)問題,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-10-10spring boot @PathVariable傳遞帶反斜杠參數(shù) / 的處理
這篇文章主要介紹了spring boot @PathVariable傳遞帶反斜杠參數(shù) / 的處理操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2021-02-02Springboot實(shí)現(xiàn)全局自定義異常的方法詳解
這篇文章主要介紹了Springboot實(shí)現(xiàn)全局自定義異常的方法詳解,SpringBoot的項(xiàng)目已經(jīng)對(duì)有一定的異常處理了,但是對(duì)于我們開發(fā)者而言可能就不太合適了,因此我們需要對(duì)這些異常進(jìn)行統(tǒng)一的捕獲并處理,需要的朋友可以參考下2023-11-11淺談java 字符串,字符數(shù)組,list間的轉(zhuǎn)化
下面小編就為大家?guī)硪黄獪\談java 字符串,字符數(shù)組,list間的轉(zhuǎn)化。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2016-11-11關(guān)于spring中aop的注解實(shí)現(xiàn)方法實(shí)例詳解
這篇文章主要給大家介紹了關(guān)于spring中aop的注解實(shí)現(xiàn)方法的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面跟著小編來一起看看吧。2017-08-08解決@JsonInclude(JsonInclude.Include.NON_NULL)不起作用問題
這篇文章主要介紹了解決@JsonInclude(JsonInclude.Include.NON_NULL)不起作用問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-06-06