併發程式設計-java多執行緒總結
目錄
先了解幾個概念
- 多執行緒:程序和執行緒是一對多的關係,一個程序(一個程式),由不同的執行緒來執行。有共享的空間也有獨立的空間。
- 並行: 同時進行,拿兩個cpu來跑同樣的程式同樣的程式碼片段,那就並行了。
- 併發:不同時進行,只有一個cpu,而多個執行緒都在爭取這個cpu資源。便是併發。用TPS和QPS去衡量併發程度。
- TPS:Transactions Per Second(每秒傳輸的事物處理個數),簡單說就是伺服器每秒處理事務的個數。
完整的包括: 請求+資料庫訪問+響應 - QPS:Queries Per Second(每秒查詢率),簡單說就是伺服器每秒處理完請求的個數。
1、執行緒的生命週期
先了解執行緒的生命週期,上圖。執行緒的生命週期從一個新的執行緒產生到結束中間會經歷非常多的情況,大體上如下圖,多執行緒環境下我們主要是再running的時候採取執行緒的保護措施,從而使多執行緒環境下,讓執行緒進入阻塞的狀態。這種保護思想其實就是排他了,到最後都得一個個來,無論式任務還是記憶體互不干擾,便達到執行緒安全了。

執行緒的生命週期
2、jvm記憶體模型
到了jdk8,記憶體模型已經有了相當的改變了,下圖是小編學習了幾篇優秀的博文學習,根據自己的理解繪製出來的,請多指教。

jdk8記憶體模型
獨立記憶體空間
從圖中可以看出執行緒安全的區域是在棧空間,每個執行緒會有獨立的棧空間,從而也解釋了為什麼方法內是執行緒安全的,而全域性變數這些是執行緒不安全的,因為這些都在堆區。
共享記憶體空間
堆空間,和MateSpace是被所有執行緒共享的,因此在處理多執行緒問題的時候,其實主要是處理這兩個空間的內容。共享區域在不加任何保護的情況下對其操作,會有異常結果。
怎麼做到執行緒安全?
- 只使用執行緒安全的記憶體空間,不使用共享的空間
- 對共享的記憶體空間採取保護措施,比如:加Lock,volatile修飾等
3、執行緒的實現方式
- 繼承Thread
package com.example.demo; import org.junit.Test; /** * Project <demo-project> * Created by jorgezhong on 2018/8/31 16:01. */ public class ThreadDemo { @Test public void extendThreadTest() { ExtendThread extendThread = new ExtendThread(); extendThread.start(); } class ExtendThread extends Thread { @Override public void run() { // TODO: 2018/8/31 } } }
- 實現Runnable介面
@Test public void runnableThreadTest(){ RunnableThread runnableThread = new RunnableThread(); Thread thread = new Thread(runnableThread); thread.start(); } class RunnableThread implements Runnable{ @Override public void run() { // TODO: 2018/8/31 } }
- Callable和Future
@Test public void callableThreadTest(){ CallableThread callableThread = new CallableThread(); FutureTask<String> stringFutureTask = new FutureTask<>(callableThread); Thread thread = new Thread(stringFutureTask); thread.start(); } /** * 這種實現是由返回值的 */ class CallableThread implements Callable<String>{ @Override public String call() { // TODO: 2018/8/31 return ""; } }
補充:Fulture和Callable(Future模式)
首先,這兩東西都在java.util.concurrent下,java本身就未多執行緒環境考慮了很多。看看下面的UML圖,RunnableFuturej繼承了Future和Runnable介面,將Future引入Runnable中,並且提供了預設實現FutureTask。RunnbleCallable和Future補充解決了兩個問題,一個是多執行緒阻塞解決方案,另一個則是返回值問題。我們知道Runnable和Thread定義的run()是沒有返回值的。而且當執行緒遇到IO阻塞的時候,只能等待,該執行緒無法做任何事情。Callable和Fulture分別解決了這兩個問題。Callable提供了返回值的呼叫,而Fulture提供了多執行緒非同步的機制。
Callable沒什麼好說的,例子如上面程式碼,就是多了個泛型的返回值,方法變成了call而已。Future就比較複雜了。FultureTask的構造方法接受Runnable或者Callable,也就是說Runnable和Callable的例項都可以使用Fulture來完成非同步獲取阻塞返回值的操作。

uml java fulture m
Future只有5個方法
- cancel:取消任務的執行。引數表示是否立即中斷任務
- isCancelled:判斷任務是否已經取消
- isDone:判斷任務是否已經完成
- get():阻塞到任務接受獲取返回值
- get(long,TimeUnit):指定超時時間,獲取返回值
Future模式缺陷
Fulture比較簡單,基本上只通過兩種方式:檢視狀態和等待完成。要麼去檢視一下是不是完成了,要麼就等待完成,而執行緒和執行緒之間的通訊只有通過等待喚醒機制來完成。原來的Fulture功能太弱,以至於google的Guava和Netty這些牛逼的框架都是重新去實現以拓展功能。而java8引入了實現了CompletionStage介面的CompletableFuture。可以說是極大的擴充套件了Future的功能。吸收了Guava的長處。
- CompletableFuture介紹
關於CompletableFuture和的具體內容,後續再寫一篇詳細介紹。結合java8的Stream API CompletionStage介面定義很多流式程式設計的方法,我們可以進行流式程式設計,這非常適用於多執行緒程式設計。CompletableFuture實現了該介面,並拓展了自己的方法。對比Fulture多了幾十個方法。大致可以分為同步的和非同步的兩種型別。而作業的時候,可以切入任務某一時刻,比如說完成後做什麼。還可以組合CompletionStage,也就是進行執行緒之間的協調作業。
- 使用執行緒池提交執行緒的實現(見下文)
4、執行緒池
我們可以看到java執行緒池相關的包,他們之間的關係如下圖。

java uml thread

java uml thread m
從uml類圖可以看出(圖片有點大,放大一下把),整個執行緒池構成其實是這樣的:
- 1、
Executor
封裝了執行緒的實現 - 2、
Executor
的子介面ExecutorService
定義了管理Executor
的一系列方法。
ThreadPoolExecutor
實現了ExecutorService
,定義了一系列處理多執行緒的內容,比如執行緒工程和儲存執行緒任務的佇列 - 3、
ScheduledExecutorService
擴充套件了ExecutorService
,增加了定時任務排程的功能。
ScheduledThreadPoolExecutor
實現了ScheduledExecutorService
,同時繼承ThreadPoolExecutor
的功能 - 4、
Executors
靜態類,包含了生成各種ExecutorService的方法。
從介面的組成可以看出,Executor、ExecutorService和ScheduledThreadPoolExecutor三個介面定義了執行緒池的基礎功能。可以理解為他們三個就是執行緒池。
那麼整個執行緒池是圍繞兩個預設實現ThreadPoolExecutor和ScheduledThreadPoolExecutor類來操作的。
至於操作,我發現java還蠻貼心的,預設實現的執行緒池只區分了可定時排程和不可定時排程的。實在是太過於靈活了,自己使用的話要配置一大堆引數,我想個執行緒池而已,給我搞這麼多配置表示很麻煩,只需要關心是不是定時的,只考慮我分配多少執行緒給執行緒池就好了。因此有了Executors
Executors操作兩個預設的實現類,封裝了了大量執行緒池的預設配置,並提供了以下幾種執行緒池給我們,我們只需要管線少部分必要的配置即可。
- Single Thread Executor:只有一個執行緒的執行緒池,順序執行
ExecutorService pool = Executors.newSingleThreadExecutor(); //提交實現到執行緒池 pool.submit(() -> { // TODO: 2018/8/31 do something });
- Cached Thread Pool:快取執行緒池,超過60s池內執行緒沒有被使用,則刪掉。就是一個動態的執行緒池,我們不需要關心執行緒數
ExecutorService pool = Executors.newCachedThreadPool(); //提交實現到執行緒池 pool.submit(() -> { // TODO: 2018/8/31 do something });
- Fixed Thread Pool:固定數量的執行緒池
//引數為執行緒數 ExecutorService pool = Executors.newFixedThreadPool(8); //提交實現到執行緒池 pool.submit(() -> { // TODO: 2018/8/31 do something });
- Scheduled Thread Pool:用於排程指定時間執行任務的執行緒池
//引數為執行緒數 ScheduledExecutorService pool = Executors.newScheduledThreadPool(8); /* * 提交到執行緒池 * 引數1:Runnable * 引數2:初始延遲時間 * 引數3:間隔時間 * 引數4:時間單位 */ pool.scheduleAtFixedRate(() -> { // TODO: 2018/8/31 do something }, 1000, 2000, TimeUnit.MILLISECONDS);
- Single Thread Scheduled Pool:排程指定時間執行任務的執行緒池,只有一個執行緒
ScheduledExecutorService pool = Executors.newSingleThreadScheduledExecutor(); //引數少了初始延遲時間 pool.schedule(() -> { // TODO: 2018/8/31 do something }, 1000, TimeUnit.MILLISECONDS);
- 執行緒池的配置策略
1、 考慮業務型別
除了考慮計算機效能外,更多的還是考慮業務邏輯,如果業務是運算密集型的,不適合開太多的執行緒,因為運算一般是cpu在算,cpu本身就是用於計算,極快,因此一個執行緒很快就能計算完畢。執行緒多了反而增加了資源的消耗。另一種是IO密集型業務,這種業務就比較是適合開多一點執行緒,因為IO、通訊這些業務本身就是非常慢的,大部分的系統的瓶頸都集中這兩方面。因此這些業務適合開多個執行緒。
2、配合cpu的核心和執行緒數
在我們配置執行緒的時候,可以參考cpu的匯流排程,儘量不超出匯流排程數。一般使用核心數。
5、保護措施
5.1、 synchronized
這其實是一個監視器。可以監視類和物件。
原理:可以這麼理解,每個例項化的物件都有一個公共的鎖,該鎖被該例項共享。因此對於該物件的所有被synchronized修飾的例項方法,是共享的同一個物件鎖。同理,類鎖也是一樣的,伴隨Class物件的生成,也會有一個類監視器,也就有一個預設的類鎖了,被synchronized修飾的所有靜態方法都共享一個類鎖。
缺陷:同步鎖關鍵子雖然方便,但是畢竟是被限制了修飾方式,因此不夠靈活,另外修飾在方法上是修飾了整個方法,因此效能在併發量大且頻繁的時候就顯得不那麼好了。
- 修飾例項方法:
public synchronized void synchronizedMethod(){ // TODO: 2018/8/29 do something }
- 修飾靜態方法:
public static synchronized void synchronizedMethod(){ // TODO: 2018/8/29 do something }
- 修飾程式碼快:
public void synchronizedMethod(){ //Object.class為鎖物件,其實就是鎖的鑰匙,使用同一把鑰匙的鎖是同步的 synchronized (Object.class){ // TODO: 2018/8/29 do something } }
5.2、Lock&&ReadWriteLock
由於synchronized的缺陷不夠靈活,對應的自然有靈活的解決方案。Lock便是解決方案。Lock是java.util.concurrent.locks包下的一個介面。但是Lock是靈活了,但是既然都多執行緒了,我們當然是最求效能啦。由於很多資料是對檢視沒有執行緒安全要求的,只需要對寫入修改要求執行緒安全即可,於是有了ReadWriteLock,讀寫鎖可以只對某一方加鎖,把鎖住的內容範圍更加縮小了,提升了效能。從下圖可以看到,ReentrantLock實現了Lock而ReentrantReadWriteLock實現了ReadWiteLock。我們可以直接使用它們的實現類實現鎖功能。

uml_java_lock
5.2.1、Lock
獲取鎖:lock()、tryLock()、lockInterruptibly()
釋放鎖:unLock()
直接上程式碼來學習效果是最快的
- DEMO:兩個執行緒爭取同一把鎖
package com.example.demo; import org.junit.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.concurrent.locks.ReentrantLock; /** * Project <demo-project> * Created by jorgezhong on 2018/8/30 15:48. */ public class LockDemo { private static final Logger LOGGER = LoggerFactory.getLogger(LockDemo.class); /** * 兩個執行緒爭取同一把鎖 */ @Test public void lockTest() throws InterruptedException { //造一把鎖先 ReentrantLock reentrantLock = new ReentrantLock(); Thread thread0 = new Thread(() -> { for (int i = 0; i < 5; i++) { lockTestHandle(reentrantLock); } }); Thread thread1 = new Thread(() -> { for (int i = 0; i < 5; i++) { lockTestHandle(reentrantLock); } }); thread0.start(); thread1.start(); while (thread0.isAlive() || thread1.isAlive()) {} } private void lockTestHandle(ReentrantLock reentrantLock) { try { //加鎖 reentrantLock.lock(); LOGGER.info("拿到鎖了,持有鎖5s"); Thread.sleep(5000); } catch (Exception e) { // TODO: 2018/8/30 do something } finally { // 記得自己釋放鎖,不然造成死鎖了 reentrantLock.unlock(); LOGGER.info("釋放鎖了"); } } }
執行結果:我們可以看到,迴圈的程式碼是連續的,沒有被其他執行緒干擾。確實是鎖上了,使用同一個鎖,必須等一個釋放了另一個才能持有。一個執行緒持有鎖,其他使用同一把鎖的執行緒就會同步阻塞,重新持有鎖之後才會結束阻塞的狀態,才能往下執行程式碼。
16:36:05.740 [Thread-0] INFO com.example.demo.LockDemo - 拿到鎖了 16:36:05.744 [Thread-0] INFO com.example.demo.LockDemo - 迴圈:0 持有鎖 16:36:05.746 [Thread-0] INFO com.example.demo.LockDemo - 迴圈:1 持有鎖 16:36:05.746 [Thread-0] INFO com.example.demo.LockDemo - 迴圈:2 持有鎖 16:36:05.746 [Thread-0] INFO com.example.demo.LockDemo - 迴圈:3 持有鎖 16:36:05.746 [Thread-0] INFO com.example.demo.LockDemo - 迴圈:4 持有鎖 16:36:05.746 [Thread-0] INFO com.example.demo.LockDemo - 釋放鎖了 16:36:05.746 [Thread-1] INFO com.example.demo.LockDemo - 拿到鎖了 16:36:05.746 [Thread-1] INFO com.example.demo.LockDemo - 迴圈:0 持有鎖 16:36:05.746 [Thread-1] INFO com.example.demo.LockDemo - 迴圈:1 持有鎖 16:36:05.746 [Thread-1] INFO com.example.demo.LockDemo - 迴圈:2 持有鎖 16:36:05.746 [Thread-1] INFO com.example.demo.LockDemo - 迴圈:3 持有鎖 16:36:05.746 [Thread-1] INFO com.example.demo.LockDemo - 迴圈:4 持有鎖 16:36:05.746 [Thread-1] INFO com.example.demo.LockDemo - 釋放鎖了 16:36:05.746 [Thread-1] INFO com.example.demo.LockDemo - 拿到鎖了 16:36:05.746 [Thread-1] INFO com.example.demo.LockDemo - 迴圈:0 持有鎖 16:36:05.747 [Thread-1] INFO com.example.demo.LockDemo - 迴圈:1 持有鎖 16:36:05.747 [Thread-1] INFO com.example.demo.LockDemo - 迴圈:2 持有鎖 16:36:05.747 [Thread-1] INFO com.example.demo.LockDemo - 迴圈:3 持有鎖 16:36:05.747 [Thread-1] INFO com.example.demo.LockDemo - 迴圈:4 持有鎖 16:36:05.747 [Thread-1] INFO com.example.demo.LockDemo - 釋放鎖了 16:36:05.747 [Thread-0] INFO com.example.demo.LockDemo - 拿到鎖了 ...... 16:36:05.748 [Thread-1] INFO com.example.demo.LockDemo - 迴圈:4 持有鎖 16:36:05.748 [Thread-1] INFO com.example.demo.LockDemo - 釋放鎖了 16:36:05.748 [Thread-0] INFO com.example.demo.LockDemo - 拿到鎖了 16:36:05.748 [Thread-0] INFO com.example.demo.LockDemo - 迴圈:0 持有鎖 16:36:05.748 [Thread-0] INFO com.example.demo.LockDemo - 迴圈:1 持有鎖 16:36:05.748 [Thread-0] INFO com.example.demo.LockDemo - 迴圈:2 持有鎖 16:36:05.748 [Thread-0] INFO com.example.demo.LockDemo - 迴圈:3 持有鎖 16:36:05.748 [Thread-0] INFO com.example.demo.LockDemo - 迴圈:4 持有鎖 16:36:05.748 [Thread-0] INFO com.example.demo.LockDemo - 釋放鎖了
- DEMO:可被中斷鎖
/** * lockInterruptibly:加了可中斷鎖的執行緒,如果在獲取不到鎖,可被中斷。 * <p> * 中斷其實是使用了異常機制,當呼叫中斷方法,會丟擲InterruptedException異常,捕獲它可處理中斷邏輯 */ @Test public void lockInterruptiblyTest() throws InterruptedException { ReentrantLock reentrantLock = new ReentrantLock(); Thread thread0 = new Thread(() -> { try { lockInterruptiblyTestHandle(reentrantLock); } catch (InterruptedException e) { LOGGER.info("被中斷了"); } }); Thread thread1 = new Thread(() -> { try { lockInterruptiblyTestHandle(reentrantLock); } catch (InterruptedException e) { LOGGER.info("被中斷了"); } }); thread1.setPriority(10); thread1.start(); thread0.start(); Thread.sleep(500); thread0.interrupt(); while (thread0.isAlive() || thread1.isAlive()) {} } private void lockInterruptiblyTestHandle(ReentrantLock reentrantLock) throws InterruptedException { /* * 加鎖不能放在try...finally塊裡面,會出現IllegalMonitorStateException,意思是當lockInterruptibly()異常的時候,執行了unlock()方法 * 其實就是加鎖都丟擲異常失敗了,你還去解鎖時不行的。放外面丟擲異常的時候就不會去解鎖了 */ reentrantLock.lockInterruptibly(); try { LOGGER.info("拿到鎖了,持有鎖5秒"); Thread.sleep(5000); } finally { // 釋放鎖 reentrantLock.unlock(); LOGGER.info("釋放鎖了"); } }
從結果可以看到,thread-0被中斷了之後不再繼續執行
20:11:22.227 [Thread-1] INFO com.example.demo.LockDemo - 拿到鎖了,持有鎖5秒 20:11:22.742 [Thread-0] INFO com.example.demo.LockDemo - 被中斷了 20:11:27.231 [Thread-1] INFO com.example.demo.LockDemo - 釋放鎖了 Process finished with exit code 0
5.2.2 ReadWriteLock
ReadWriteLock只是定義了讀鎖和寫鎖兩個方法,其具體實現和拓展再預設實現ReentrantReadWriteLock中。簡單來說讀寫鎖呢,提供讀鎖和寫鎖,將讀和寫要獲取的鎖型別分開,用一個對列來管理,所有的鎖都會經過佇列。當需要獲取寫鎖的時候,後買的讀寫鎖獲取都需要等待,知道該寫鎖被釋放才能進行。
@Test public void readWriteLockTest(){ ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(); ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock(); try { readEvent(); } catch (Exception e) { LOGGER.error(e.getMessage(),e); }finally { readLock.unlock(); } ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock(); try { writeEvent(); } catch (Exception e) { LOGGER.error(e.getMessage(),e); }finally { writeLock.unlock(); } } private void writeEvent() { // TODO: 2018/9/3 done write event } private void readEvent() { // TODO: 2018/9/3 done read event }
總的來說:凡是遇到寫,阻塞後面的執行緒佇列,讀與讀是不阻塞的。
5.3、 volatile
volatile可修飾成員變數,能保證變數的可見性,但是不能保證原子性,也就是說併發的時候多個執行緒對變數進行計算的話,結果是會出錯的,保證可見性只是能保證每個執行緒拿到的東西是最新的。
對於volatile來說,保證執行緒共享區域內容的可見性可以這麼來理解,堆記憶體的資料原來是需要拷貝到棧記憶體的,相當於複製一份過去,但是呢。再不加volatile的時候,棧區計算完之後在賦值給堆區,問題就產生了。加了volatile之後,執行緒訪問堆區的資料之後,堆區必須等待,知道棧區計算完畢將結果返回給堆區之後,其他執行緒才能繼續訪問堆區資料。
public volatile String name = "Jorgezhong";