并發(fā)
1. 實現(xiàn)并啟動線程的兩種方式:
1)繼承Thread類,重寫run方法,用start啟動; 2) 實現(xiàn)Runnable接口,實現(xiàn)run方法,用new Thread(Runnable target).start()來啟動; 3)實現(xiàn)Callable接口的call方法,有返回值。(前兩種run方法無返回值)
run是由不同的線程的執(zhí)行的,仍然可以執(zhí)行main中的其他操作。
join:一個線程可以在其他線程之上調用join()方法,其效果是等待一段時間直到第二個線程結束才繼續(xù)執(zhí)行。
并發(fā)編程中的三個特性:
1. 原子性(Atomicity):一個操作不能被打斷,要么全部執(zhí)行完畢,要么不執(zhí)行。原子操作可以由線程保證其不可中斷。當定義Long和double時可以用volatile修飾,就會獲得原子性(因為Long和double是64位的,JVM會把64位的讀寫當作分離的32位操作來執(zhí)行。)
2. 可見性: 一個線程對共享變量做了修改之后,其他的線程立即能夠看到(感知到)該變量這種修改(變化)。
3. 有序性: 在單線程程序里,我們總是以為代碼的執(zhí)行是從前往后的,依次執(zhí)行的。
但是在多線程并發(fā)時,程序的執(zhí)行就有可能出現(xiàn)亂序。在本線程內觀察,操作都是有序的(線程內表現(xiàn)為串行語義(WithIn Thread As-if-Serial Semantics));如果在一個線程中觀察另外一個線程,所有的操作都是無序的(“指令重排”現(xiàn)象和“工作內存和主內存同步延遲”現(xiàn)象)。
Java提供了兩個關鍵字volatile和synchronized來保證多線程之間操作的有序性,volatile關鍵字本身通過加入內存屏障來禁止指令的重排序,而synchronized關鍵字通過一個變量在同一時間只允許有一個線程對其進行加鎖的規(guī)則來實現(xiàn)。
4. happens-before原則 Java內存模型中定義的兩項操作之間的次序關系,如果說操作A先行發(fā)生于操作B,操作A產(chǎn)生的影響能被操作B觀察到。
中斷
1. Executor 的中斷操作
Executor的shutdown方法可以防止新任務提交到Executor,當前線程將繼續(xù)運行在shutdown()被調用前提交的所有任務。
2. interrupted()
3. 鎖
1 synchronized關鍵字:隱式鎖
1)可以把方法標記為synchronized來防止資源沖突。但在使用并發(fā)的時候需要將域設置為private,因為synchronized無法防止其他任務直接訪問域。
2)synchronized底層是如何實現(xiàn)的:信號量,鎖this。
2 Lock:顯式鎖
lock() unlock()方法。結構如下:
lock.lock()
try{
return; //return必須在try中出現(xiàn),以確保unlock不會過早發(fā)生,從而暴露給了第二個任務
}finally{
lock.unlock()
}
3. volatile關鍵字:
volatile關鍵字保證了應用中的可視性。如果將一個域聲明成volatile的,那么只要對這個域產(chǎn)生了寫操作,那么所有的讀操作都可以都可以看到這個操作。
如果一個域完全由synchronized防護,那就不必設置為volatile。
volatile可以防止指令重排,但注意!volatile并不保證原子性。(因為如果當前值與該變量以前的值相關,那么volatile關鍵字就不起作用:例如count++, count = count+1)
5. 多線程中的信息交互:
- Thread類的方法:sleep(), 讓線程暫停執(zhí)行指定的時間,但對象鎖依然保持。休眠時間結束后線程回到就緒態(tài)。
- Object中的方法:wait(), notify(), notifyAll();wait()導致線程放棄對象鎖進入等待池,只有調用對象的notidy()方法(或者notifyAll()方法)才能喚醒等待池中的線程進入等鎖池。
- join:在線程中調用另一個線程的 join() 方法,會將當前線程掛起,而不是忙等待,直到目標線程結束。
- java.util.concurrent 類庫中提供了 Condition 類來實現(xiàn)線程之間的協(xié)調,可以在 Condition 上調用 await() 方法使線程等待,其它線程調用 signal() 或 signalAll() 方法喚醒等待的線程。
相比于 wait() 這種等待方式,await() 可以指定等待的條件,因此更加靈活。
6. 多線程公用一個數(shù)據(jù)變量需要注意什么?
1)當在Runnable對象中定義了全局變量,且run方法會修改該變量時,如果有多個線程同時使用該對象,就會造成全局變量的值被同時修改,造成錯誤。
2)ThreadLocal可以用于解決線程間共享變量,使用ThreadLocal聲明的變量即使在線程中屬于全局變量,對于每個線程來說,這個變量也是獨立的。
3)volatile變量,每次被線程訪問時都強迫線程從主內存中重讀該變量的最新值。當變量被修改時,也會強迫線程將最新的值刷新回主內存中。
7. 線程池
- 用途:事先創(chuàng)建若干個可執(zhí)行的線程放入一個池(容器),需要的時候從池中獲取線程,不用自行新建,使用完畢不需要銷毀線程,而是放回線程池,從而減少創(chuàng)建和銷毀線程對象的開銷。
- 如何設計一個動態(tài)大小的資源池? 一個線程池有四個基本組成部分:
1) 線程管理器(Threadpool):用于創(chuàng)建并管理線程池,添加新任務;
2)工作線程(PoolWorker):線程池中線程,在沒有任務是處于等待狀態(tài),可以循環(huán)執(zhí)行任務;
3)任務接口(Task):每個任務必須實現(xiàn)的接口,以供工作線程調度任務的執(zhí)行,它主要規(guī)定了任務的入口,任務執(zhí)行完成后的收尾工作,任務的執(zhí)行狀態(tài)等;
4)任務隊列(TaskQueue):用于存放沒有處理的任務。提供一種緩存機制。
8. AQS (Abstract Queued Synchronizer)
- CountDownLatch:用來控制一個線程等待多個線程。
維護了一個計數(shù)器 cnt,每次調用 countDown() 方法會讓計數(shù)器的值減 1,減到 0 的時候,那些因為調用 await() 方法而在等待的線程就會被喚醒。
912a7886-fb1d-4a05-902d-ab17ea17007f.jpg
(圖片轉載自 https://github.com/CyC2018/CS-Notes/blob/master/docs/notes/Java%20并發(fā).md#十java-內存模型) -
CyclicBarrier:用來控制多個線程互相等待,只有當多個線程都到達時,這些線程才會繼續(xù)執(zhí)行。
和 CountdownLatch 相似,都是通過維護計數(shù)器來實現(xiàn)的。線程執(zhí)行 await() 方法之后計數(shù)器會減 1,并進行等待,直到計數(shù)器為 0,所有調用 await() 方法而在等待的線程才能繼續(xù)執(zhí)行。
f944fac3-482b-4ca3-9447-17aec4a3cca0.png - Semaphore:Semaphore 類似于操作系統(tǒng)中的信號量,可以控制對互斥資源的訪問線程數(shù)。
9. 內存模型
- 棧:Java??偸桥c線程關聯(lián)在一起的,每當創(chuàng)建一個線程,JVM就會為該線程創(chuàng)建對應的Java棧。Java棧數(shù)據(jù)不是線程共有的,所以不需要關心其數(shù)據(jù)一致性,也不會存在同步鎖的問題。
- 堆:堆是存儲Java對象的地方,Java堆是GC管理的主要區(qū)域,從內存回收的角度來看,由于現(xiàn)在GC基本都采用分代收集算法,所以Java堆還可以細分為:新生代和老年代;新生代再細致一點有Eden空間、From Survivor空間、To Survivor空間等。
-
Java內存模型的主要目標是定義程序中各個變量的訪問規(guī)則,即在JVM中將變量存儲到內存和從內存中取出變量這樣的底層細節(jié)。
1) JVM規(guī)定了所有的變量都存儲在主內存(Main Memory)中,每個線程還有自己的工作內存(Working Memory)。線程的工作內存中保存了該線程使用到的變量的主內存的副本拷貝,線程對變量的所有操作(讀取、賦值等)都必須在工作內存中進行,而不能直接讀寫主內存中的變量,不同的線程之間也無法直接訪問對方工作內存中的變量,線程之間值的傳遞都需要通過主內存來完成。
2) Java 內存模型定義了 8 個操作來完成主內存和工作內存的交互操作。
read:把一個變量的值從主內存?zhèn)鬏數(shù)焦ぷ鲀却嬷?br> load:在 read 之后執(zhí)行,把 read 得到的值放入工作內存的變量副本中
use:把工作內存中一個變量的值傳遞給執(zhí)行引擎
assign:把一個從執(zhí)行引擎接收到的值賦給工作內存的變量
store:把工作內存的一個變量的值傳送到主內存中
write:在 store 之后執(zhí)行,把 store 得到的值放入主內存的變量中
lock:作用于主內存的變量
unlock:
b6a7e8af-91bf-44b2-8874-ccc6d9d52afc.jpg
10. 多線程開發(fā)實踐
- 給線程起個有意義的名字,這樣可以方便找 Bug。
- 縮小同步范圍,從而減少鎖爭用。例如對于 synchronized,應該盡量使用同步塊而不是同步方法。
- 多用同步工具少用 wait() 和 notify()。首先,CountDownLatch, CyclicBarrier, Semaphore 和 Exchanger 這些同步類簡化了編碼操作,而用 wait() 和 notify() 很難實現(xiàn)復雜控制流;其次,這些同步類是由最好的企業(yè)編寫和維護,在后續(xù)的 JDK 中還會不斷優(yōu)化和完善。
- 使用 BlockingQueue 實現(xiàn)生產(chǎn)者消費者問題。
5.多用并發(fā)集合少用同步集合,例如應該使用 ConcurrentHashMap 而不是 Hashtable。 - 使用本地變量和不可變類來保證線程安全。
- 使用線程池而不是直接創(chuàng)建線程,這是因為創(chuàng)建線程代價很高,線程池可以有效地利用有限的線程來啟動任務。


