基本概念
線程和進程
-
概念
- 操作系統是包含多個進程,進程包含多個線程(至少一個)。
- 進程:unix環境,運行的程序,是系統資源分配的基本單位,包括文件/網絡句柄(共享)、內存(隔離)、用戶id等。
- 線程:cpu的基本調度單位,每個線程執行的都是進程代碼的某個片段。包括棧、PC(指向自己代碼所在的內存)、TLS(Thread Local Storage)
-
區別
- 起源:先有進程后有線程,由於輸入設備較慢,為了充分利用cpu資源而產生線程。
- 概念:進程是資源分配基本單位;線程是cpu調度基本單位
- 通信:進程間通過IPC;同一進程內的線程可以通過讀寫同一進程內的數據實現通信
- 資源:線程使用進程內的資源,線程間的共享資源,如進程代碼段,不共享的如各自的堆棧。
- 開銷不同:線程的創建、終止、切換、共享內存和文件資源都更輕量(進程間需要通過內核幫忙)
java 在設計之初就支持多線程,而且可以一對一映射到操作系統的內核線程。
多線程
原因:單線程效率能夠滿足自然不用考慮多線程。多線程主要為了提高CPU利用率(發揮多核作用、避免無效等待)、便於編程建模(任務拆分)、性能定律(阿姆達爾定律:處理器多,其對程序效率的提高取決於程序的並行任務比例)。
場景:同時做多件事、提高效率、大並發量
局限:上下文切換消耗、異構化任務(任務都不一樣)難以並行、線程安全問題(數據准確、活躍性問題)。
並發、高並發
並發性:與串行性相對應,不同的部分可以無序或同時執行而不影響結果。
高並發是一種狀態,多線程是解決的方法之一。
QPS、帶寬、PV、UV、並發連接數(大於同時在線的用戶數)、服務器平均請求等待時間
同步異步、阻塞非阻塞
同步與異步:被調用方是否提醒,有提醒為異步
阻塞和非阻塞:調用方是否可以在結果出來前做別的事
核心知識
實現多線程
本質構造 Thread 一種
一般不用 Thread:1)從解耦角度,run應該和類本身的創建分開,Thread是將run()進行重寫,Runnable則調用傳入對象的方法;2)它用的是繼承而非實現;3)不能借助線程池
Oracle說有兩種,但從本質上來說只有一種方法創建線程,就是構造Thread類,而執行線程有兩種方法,分別是Runnable,將Runnable對象傳遞給Thread類的run方法調用Runnable的run方法,Thread重寫run方法並執行。如果從代碼表面實現來看,則有很多種,比如線程池、匿名內部類、lambda、計時器等。
線程再多也就百級別,因為線程本身就消耗資源,再提高應該考慮異步。
對於IO密集型操作,多線程提升效果不大,重點是提高並發度(異步)
啟動
使用start
start涉及兩個線程:主線程和子線程。主線程執行start實際上是告訴jvm有空時創建子線程去執行。當上下文、棧、pc等資源准備后(就緒狀態),等待cpu資源,之后才執行。
start不能重復調用,比如當第一個start執行后,線程進入end,此時就不能調用start重新啟動了。源碼來看,start一開始會判斷線程狀態是否為0,即未啟動狀態,然后把線程加入線程組,調用start0方法,並修改started狀態。
start是讓JVM創建線程去執行run,而直接調用run,則是由main來執行run
停止
使用interrupt通知而非強制。因為執行的線程本省更清楚如何來停止。
線程停止情況:正常執行完或者出現異常停止,其占據的資源會被JVM回收。
當線程在 sleep 等可響應中斷的方法中被 interrupt,會拋異常。如果能保證 sleep 時收到 interrupt,就可以不使用 isInterrupted 判斷。例如迭代中有 sleep,則不需要在迭代的條件中添加 isInterrupted。線程一旦響應中斷就會把 isInterrupted 標記清除,所以如果在 while 里 try-catch sleep,是無法停止線程的。
響應中斷的方法:
wait()
sleep()
join()
BlockingQueue.take()/put()
Lock.lockInterruptibly()
CountDownLatch.await()
CyclicBarrier.await()
Exchanger.exchange()
InterruptibleChannel相關方法
Selector相關方法
interrupt 之所以能夠停止阻塞,是因為底層調用的是 cpp 代碼,那涉及到操作系統層面的操作。
在開發中,如果 run 函數中調用一個方法,這個方法的異常不應該在這個方法內 catch,而是應該拋出,讓調用方來覺得怎么處理。如果方法的編寫者自己想做些處理,可以 catch,但在處理完后要 interrupt,這樣調用方才能檢查到 interrupted。在 run 方法中已經不能往上拋異常了。
直接調用stop方法,解鎖已鎖定的所有監視器,強制終止線程而非完成再終止。
suspend和resume已經被棄用,因為掛起線程時帶上鎖,容易出現死鎖。
使用 volatile 加 標記變量 並不能很好的停止線程,因為在線程阻塞時不能拋異常,進而判斷終止。
static boolean interrupted() 判斷后會清除 interrupted 標記。判斷的是當前主線程而不是調用者
boolean isInterrupted() 不會清除
面試回答:正確停止線程的方法是 interrupt請求,這樣能夠保證任務正常中斷。但這個方法需要請求方(發送interrupt)、被請求方(適當時候檢查isInterrupted或處理InterruptedException)、子方法被調用方(拋出異常或者自己處理后再恢復interrupted)互相配合。對於interrupt無法中斷的阻塞,那就只能根據不同的類調用不同的方法來中斷,比如 ReentrantLock.lockInterruptibly()
線程狀態

java runnable 對應系統的 ready 和 running。右邊三種都是阻塞狀態,它跟長時間運行的方法不同在於返回時間不可控。實際上,waiting 可以到 blocked(剛被喚醒),waiting 和 timed_waiting 可以到 terminated(出現異常)
Block:線程請求鎖失敗時進入阻塞隊列,該狀態線程會不斷請求,成功后返回runnable
Waiting:調用特定方法主動進入等待隊列,釋放cpu和已獲得的鎖,等待其他線程喚醒
Timed_waiting:timeout 時自動進入阻塞隊列。
sleep僅放棄cpu而不釋放鎖
wait和notify必須配套使用,即必須使用同一把鎖調用。調用wait和notify的對象必須是他們所處同步塊的鎖對象。
重要方法
wait 讓當前線程(不管是誰調用)進入阻塞並釋放 monitor,只有調用該對象的 notify 且剛好是本對象本喚醒、notifyAll、interrupt、wait的timeout、interrupt,才會被喚醒。如果遇到中斷,則會釋放掉 monitor。wait、notify、notifyAll只能在 synchronized 代碼塊或方法中使用,這是為了防止准備執行wait時被切換到notify,結果切換回來執行wait后就沒有人notify了。而 sleep 只針對自己,並不需要配合,所以不需要在 synchronized 中。另外,代碼處於 synchronized,意味着即便已經調用 notify/notifyAll,monitor仍要等到執行完 synchronized 部分才會被釋放。而此時被喚醒的線程就會進入 Blocked 狀態。
wait、notify、notifyAll的調用都必須先有 monitor,即進入 synchronized,這是一個鎖級別的操作,粒度更細,所以定義在 Object 下(對象頭就有鎖狀態)。功能實際上比較底層,使用 condition 更方便。持有多個鎖的時候注意防止死鎖。
不要用 Thread 的 wait,因為 Thread 在退出時會自動調用 notify,這樣會打亂自己原來的設計。
Sleep 方法可以讓線程進入 Waiting 狀態,不占用 cpu 資源,但不會釋放鎖(synchronized 和 lock),知道規定時間后再執行,如果休眠被中斷,會拋出異常,清除中斷狀態。
wait/notify 和 sleep 的比較:相同在於阻塞、響應中斷,不同在於同步方法、釋放鎖、指定時間、所屬類。
synchronized (lock) {
lock.notify(); // 如果 直接 notify,會出現 IllegalMonitorStateException
lock.wait();
}
public synchronized void method(){
wait(); // 實際上是 this.wait();
notify();
}
public Class Test{
public static synchronized void method(){
Test.class.notify();
}
}
join:新的線程加入我們,所以我們要等待它執行完再出發。主線程等待子線程。主線程執行子線程.join時,catch 中加上子線程.interrupt。主線程執行join時處於 wait 狀態。join的源碼中是借助 wait 實現的,而 notify 是依賴 jvm 實現,即上面提到的每個 Thread 執行完都會調用 notifyAll 方法。
thread.join();
// 上下效果一樣
synchronized (thread) {
thread.wait();
}
yield 釋放 cpu 時間片,但不會釋放鎖,仍是 running,隨時可能被再次執行。但 jvm 並不保證遵循這個原則,所以自己開發中一般不用 yield,但並發包里面用得不少。
線程
編號:系統用、自增(從1開始,因為是 ++num)。除了 main 線程外,jvm還自動創建其他線程,所以馬上建立的main外線程不是2
Thread Group(main, ...), Finalizer(執行對象finalize方法), Reference Handler(GC、引用相關線程), Signal Dispatcher(把操作系統信號發給程序)
名稱:可以隨時修改
是否為守護線程:為用戶線程提供服務,如果只剩下守護線程,jvm就會停止。線程默認繼承父線程(用戶或守護),可以把用戶線程改為守護線程,但沒必要。通常守護線程都是由jvm啟動的,jvm啟動時會先啟動 main。
對比守護和普通:整體無區別,唯一區別在於是否影響jvm退出
優先級:10個級別,默認5。子線程會繼承。程序設計不應該依賴優先級,不同的操作系統對優先級的定義是不一樣的。
操作系統優先級的級別和java的不一致,而且系統有可能有越過優先級的分配資源的功能,這樣優先級就無效了。低優先級甚至有可能餓死。
子線程的異常
子線程出現的異常,主線程難以感知(子線程拋異常,主線程照樣執行),其異常在主線程無法用傳統方法捕獲。
處理方法:UncaughtExceptionHandler 來實現全局的子線程異常捕獲
Thread.setDefaultUncaughtExceptionHandler(handler);
ThreadGroup本身實現了UncaughtExceptionHandler,子線程繼承自它,所以子線程會調用它實現的uncaughtException,該方法不停地調用父類的uncaughtException,直到父類為null,然后看是否設置了uncaughtExceptionHandler,有的話調用,沒有就輸出棧信息。
run方法本身不能拋異常。
並發安全問題
當多個線程訪問一個對象時,如果不用考慮這些線程在運行時環境下的調度和交替執行,也不需要進行額外的同步,或者在調用方進行任何其他的協調操作,調用這個對象的行為都可以獲得正確的結果,那這個對象就是線程安全的。
本質是資源競爭,一些場景有
-
a++:兩個線程各自給同一個變量做加法,結果會比預定的要小。因為某個線程a取數加1,但還沒寫回內存,其他線程就進行讀取,獲得還沒加1的結果,而在此基礎上加1就會漏掉線程a的。
-
活躍性問題:死鎖、飢餓
-
對象發布和初始化
對象能夠被發布的情況:public、方法返回、方法參數傳入。
逸出:方法返回一個private對象;還沒完成初始化。
解決方法
- 每次返回private對象時,都新建一個副本,而不是直接給對象。
- 監聽者模式中,通過工程方法,保證 listener 構造方法執行完后再進行注冊,最后才把 listener返回。
總結性提醒:
- 共享變量或資源,如對象屬性、靜態變量、共享緩存、數據庫
- 所有依賴時序的操作,即便每一步都是線程安全的都有問題,例如read-modify-write(a++)、check-then-act
- 數據間的綁定關系,例如ip和端口號
- 使用第三方類時調研其線程安全性
性能問題:
- 調度:
- 上下文切換:掛起進程/線程,把進程/線程狀態放到內存某處,加載下一個進程/線程的狀態到寄存器,最后轉跳到程序計數器所指向的位置。
- 緩存開銷:緩存失效
- 密集切換場景:搶鎖、IO
- 協作:內存同步(synchronized、volatile等會禁止編譯器的一些優化、cpu緩存失效)
JVM優化包括指令重排、消除不必要的鎖
Java內存模型
JVM內存結構 vs Java 內存模型 vs Java對象模型
jvm內存結構:和jvm的運行時區域有關
java內存模型:和java的並發編程有關
java對象模型:和java對象在jvm中的表現形式有關
詳細看《New Job Interview Part 1》和《開發拾遺》
JMM
C語言不存在內存模型,依賴處理器,而處理器不同硬件有不同實現,結果相同代碼有不同的運行效果。JMM就是為了解決這種不一致而產生的統一的一組規范,各JVM開發者都需要遵循。volatile、synchronized、Lock等原理都是JMM。有了JMM,java開發者就可以通過同步工具和關鍵字開發並發程序。
重排序、可見性、原子性
重排序
出現情況:jit翻譯、jvm解釋、cpu重排
對於小概率事件,可以通過死循環,直到出現小概率事件才退出,進而證明小概率的存在。
可見性
各線程的本地緩存是相互不可見的,只能通過主內存來共享信息,所以可見性的根本原因是多級緩存的存在。volatile 能夠保證線程讀取前,在其他線程本地緩存中修改的值flush到主內存。
JMM對多級緩存和內存進行了抽象,出現本地內存(working memory)、buffer、主內存。
Happens-before:前一個操作的結果能夠被后一個操作看到,那就符合 happens-before。符合這個原則的情況有:
- 單線程:前一條代碼的操作結果會被下一條代碼看到
- 鎖操作(synchronized和Lock):前一個釋放鎖的線程的操作會被下一個獲得鎖的線程看到
- volatile:前一個線程的寫操作會被下一個線程的讀操作看到。注意,這讓 volatile 有一個近朱者赤的作用,比如 a = 1; b = a,如果b加了 volatile,那么讀線程一定能看到a的變化。
- 線程啟動:thread.start()可以看到前面已經執行的結果(與單線程情況類似)
- join:threada.join()后面的肯定可以看到前面的操作的結果
- 傳遞性原則(與單線程類似)
- 中斷:一個線程被其他線程中斷,那么isInterrupted或者拋出InterruptedException一定能看到。
- 工具類:
- 線程安全容器的get肯定能看到之前put的結果
- CountDownLatch、Semaphore、CyclicBarrier、Future、線程池
例題
# thread1
x=1
y_read=y
# thread2
y=1
x_read=x
# 有可能 t1 和 t2 得到 y_read=0、x_read=0,由於亂序和可見性。
通過 object == null 來判斷是否加鎖時,要用 volatile object,否則 object 在鎖代碼塊中進行實例化,分為分配內存、初始化變量、變量覆蓋到內存,這幾步有可能讓鎖外面的線程獲得沒有構建好的object
鎖的安全問題
數據庫中有些有檢測並放棄事務來解決死鎖的功能,但JVM中並沒有自動處理的功能,壓力測試也不能找出所有潛在的死鎖。
產生原因:競爭資源/調度順序不當
找死鎖的方法:jstack pid、MXBean
條件與解決:
- 互斥條件(有鎖):一般無法破除,除了設計初期
- 請求保持:一次性申請所有資源。(有時影響范圍較大)
- 環路等待:
- 資源按照隊列線性申請:起點相同,例如統一加鎖順序,比如規定對唯一id小的先加鎖,如果相同,再搶一把備用鎖
- 異類:哲學家問題里,加入一個先拿又筷再拿左筷的人
- 中介檢查與恢復:
- 終止:線程按照優先級,例如重要性、已占用資源、運行時間等,來終止
- 搶占:線程回退,放棄資源
- 不可剝奪:當進程申請的資源得不到滿足,必須釋放占有的資源;有一個中介做分配。或者超時釋放。
對於線上死鎖,防范於未然是必須的,之后一旦發現,先保存案發現場,重啟服務,然后根據案發現場的信息查找原因,修改代碼,重新發布。
開發中注意:
- synchronized不具備嘗試鎖能力,可以通過 Lock 的 tryLock(timeout) 來實現。一旦超時,發日志,報警。
- 多使用並發類而不是自己寫底層的鎖,例如atomic包下效率一般比Lock高。
- 盡量降低鎖的粒度,例如如果使用同步代碼塊,就不使用同步方法;專鎖專用
- 避免鎖嵌套
- 分配資源前看能不能回收。
典型算法:銀行家算法,現用「所需資源」-「已有資源」=「差距資源」,然后通過「剩余資源」去配對每個「差距資源」,「剩余資源」大於「差距資源」的,優先分配,待分配完的進程執行完后歸還資源,然后再重新分配。
活鎖:線程在運行,但是一直得不到進展,消耗cpu資源。解決方法:
- 往重試機制加入隨機因素
- 對於消息隊列的重試,可以先把數據放到發送隊列的末尾;設置重試限制,超過限制的寫入數據庫。一旦數據庫收到這些信息,會觸發告警,而且定期會把數據庫的這些信息嘗試進行同步。
飢餓:線程始終得不到cpu資源。解決方法:避免持有鎖而一直不釋放、不要設置優先級
並發工具
線程池
好處:加快響應速度(避免不停的創建和銷毀);合理利用CPU和內存(循環利用);統一管理(提供一些統計信息)
場景:服務器,或者5個線程以上的情況
原理解析
線程池的7個屬性:
- corePoolSize:假設是a,那么同時來了b個任務(b<a),則線程池會創建出b個線程,之后這b個就成固定不會被回收,成了最小線程數
- MaximumPoolSize
- keepAliveTime和unit決定buffer的線程沒工作時維持多久
- workQueue:任務隊列,來不及執行的任務,滿了才在 corePoolSize 的基礎上增加(如果沒有超過 MaximumPoolSize 的話)。選擇 SynchronousQueue 相當於沒有隊列,選擇 LinkedBlockingQueue 相當於無界隊列,選擇 ArrayBlockingQueue 有界
- threadFactory新建woker線程,即這個線程池維護的線程,並進行一些屬性設置,比如線程名,優先級等等,handler任務裝滿隊列,且線程數已經達到最大值時的策略,默認報錯
數量設置:一般IO密集型,core5倍以上,CPU密集型,core的一到兩倍。一般公式
core_num * (1+avg_waiting_time/avg_working_time),更具體就需要壓測。其他策略:
.AbortPolicy:丟棄任務並拋出RejectedExecutionException異常。
.DiscardPolicy:也是丟棄任務,但是不拋出異常。
.DiscardOldestPolicy:丟棄隊列最前面的任務,然后重新嘗試執行任務(重復此過程)
.CallerRunsPolicy:由調用線程處理該任務
execute的執行步驟:

線程池如何實現線程復用?底層會創建 worker,其實現 runnable,但組合了 Thread,所以可以調用 run 方法,實際是調用 runWorker,里面先把 worker 的 task 取出,然后實現這個 task 的 run 方法
線程池狀態
- running
- shutdown:不接受新任務,但處理排隊任務
- stop:不接受新任務,也不處理排隊任務,並中斷正在進行的任務
- tidying:所有任務已經終止,線程數變為零,並執行 terminate 鈎子方法
- terminated:terminate運行,等待被回收了
內置線程池
默認線程池的弊端:newSingleThredExecutor、newFixedThreadPool中的Queue是Integer最大值,newCachedThreadPool中的maximumPoolSize是Integer最大值,都可能OOM
ScheduledThreadPool 里面使用 DelayedWorkQueue,對於一些定時任務可以考慮使用。
使用
//runnable
ExecutorService pool = Executors.newxxxThreadPool(); //Scheduled可以指定執行的延遲,速率等。
Runnable command = new MyRunnable();
pool.execute(command);//.submit配合future使用
pool.shutdown();// 進入停止狀態,即不再接收任務(新提交會拋異常),里面所有線程執行完才結束線程池。
pool.isShutdown(); // 是否進入 shutdown 狀態
pool.isTerminated(); // 是否都執行完了
pool.awaitTermination(); // 阻塞,等待一段時間,一旦發現已經 terminated,就返回true,超過時間就返回 false,被中斷也會返回 false
shutdownNow(); // interrupt正在執行的任務,隊列中的任務返回出一個List<Runnable>,需要做相應處理
// 鈎子:重寫 beforeExecute 和 afterExecute 來增加功能,比如日志、統計
// 生產中,線程池最好還是自己定義,這里就涉及 任務隊列、線程工廠 和 拒絕策略 的實現
//callable
ExecutorService pool = Executors.newxxxThreadPool();
List<Future> futureList = new ArrayList<>();
for(int i=0; i<20; i++){
Callable<Integer> task = new MyCallable();
Future future = pool.submit(task);
futureList.add(future);
}
for(Future<Integer> f: futureLIst){
Int res = future.get();//等線程執行完畢后才有,並繼續執行。
}
pool.shutdown();
//監控方法
getTaskCount, getCompletedTaskCount, getPoolSize(當前線程數), getActiveCount()
ThreadLocal
場景
- 每個線程需要一個獨享的對象,通常是工具類,例如 SimpleDateFormat和Random。
- 每個線程內需要保存全局變量,例如攔截器中獲取的用戶信息,可能很多地方都用得上,而這些信息如果在各個地方都傳遞,會很麻煩。
使用
- 設置初始值:
- initValue:初始化不依賴外部參數的。實際第一次 get 時才執行。
- set:需要外部參數的
- 取值:get,如果執行 remove,之后的 get 會重新初始化
原理
每個 Thread 都有一個 ThreadLocalMap,里面存儲多個 Entry<ThreadLocal<?>, T> 對象。當調用 ThreadLocal 實例的 get 時,會先獲取執行這個方法的線程的 ThreadLocalMap,沒有的話執行初始化,把 initialValue 產生的 value 組裝成 entry 加入 map。ThreadLocal<?> 作為 key,通過內存地址來判斷是否重復。
開發注意
- 內存泄漏:由於 entry 中的 key 是弱引用,而 value 是強引用,value 在 Thread 不斷被復用的時候,即便 ThreadLocal 不再使用,value 也是不能被回收的。為了 value 能夠順利被回收,防止內存泄漏,當 ThreadLocal 不再需要時要主動調用 remove 方法。例如上面場景2中,可以通過攔截器在請求退出時調用 remove。
- 自動拆箱的空指針:如果方法返回的是包裝類,但結果是 null,而方法的返回是對應的基本類型,那么就會出現空指針異常。所以方法返回類型要留意。
- 盡量用框架本身的方法,比如 Spring 的 RequestContextHolder,它本身就是使用 ThreadLocal 的,而且避免我們忘記 remove。
Lock
最常見是 ReentrantLock,只允許一個線程訪問共享資源。
synchronized的不足
- 代碼塊中途不能退出,除非用 wait
- 不能設置超時
- 鎖條件單一(某個對象)
- 無法知道是否成功獲取到鎖
使用
lock:使用 lock 時,無論怎樣都要用 try-finally。lock不能被中斷,一旦陷入死鎖就永久等待。
tryLock:可設置超時,lockInterruptibly 相當於 timeout 設置為無限。
分類
- 是否鎖資源
- 樂觀(非互斥同步鎖):CAS,例如原子類、並發容器、GitHub
- 優劣:不停重試耗費資源
- 場景:並發寫少,鎖持有時間短,大部分是讀
- 悲觀(互斥同步鎖):synchronized (jvm優化后部分樂觀)和 Lock
- 優劣:阻塞和喚醒有性能劣勢,比如用戶態/系統態、上下文切換、檢查是否需要喚醒等;可能永久阻塞;優先級反轉(低優先級獲得鎖了)
- 場景:並發寫多,鎖持有時間較長(代碼負責,有循環)
- 樂觀(非互斥同步鎖):CAS,例如原子類、並發容器、GitHub
- 是否可以共享一把鎖:通常出現在讀寫鎖,ReentrantReadWriteLock,多讀或者一寫。讀操作插隊提高效率,但為了避免寫飢餓,ReentrantReadWriteLock 默認是隊列頭節點為寫鎖時禁止讀插隊,寫是一直可插隊的。寫鎖降級:一個操作寫完后是讀取,但不想一直持有寫鎖,所以希望寫完后降級為讀鎖。ReentrantReadWriteLock 允許釋放寫鎖前獲得讀鎖,但不能在釋放「所有」讀鎖前獲得寫鎖。
- 共享鎖:
- 獨占鎖
- 競爭時是否排隊
- 公平:排隊
- 非公平:合適時機嘗試插隊,失敗后排隊。非公平主要想利用正在排隊的線程被喚醒時的空檔期,讓后來的、已是運行狀態的線程馬上執行。ReentrantLock 默認策略;tryLock只要發現有鎖就馬上獲取,不會排隊,即便設置了公平
- 優劣:性能好,但可能產生飢餓線程
- 同一線程是否可以重復獲取同一把鎖(底層通過AQS)
- 可重入:
- 優劣:避免重新獲取鎖時出現死鎖;提高封裝性,避免重復的上解鎖
- 場景:遞歸處理資源。
- 使用:ReentrantLock 的相關方法,有getHoldCount, getQueueLength, isHeldByCurrentThread等方法,但線上用得少
- 不可重入
- 可重入:
- 是否可中斷
- 可中斷鎖:tryLock、lockInterruptibly
- 不可中斷鎖:synchronized
- 等待鎖過程
- 自旋鎖:不停嘗試獲得。
- 場景:並發不高的場景,臨界區小(獲取鎖后執行的時間短),效率比阻塞高
- 非自旋鎖:失敗阻塞,直到被喚醒
- 自旋鎖:不停嘗試獲得。
鎖的優化
- JVM:
- 自旋鎖和自適應:不會一直自旋,而且每次自旋的次數都可能調整
- 鎖消除:內部判斷鎖是否有必要
- 鎖粗化:多次獲得的都是相同的鎖,那就有可能只獲取一次,最后才釋放
- 開發:
- 縮小同步代碼塊
- 盡量不要鎖方法
- 通過消息隊列等方式把多個並行任務交給一個線程/進程處理來減少鎖的消耗
- 避免人為制造“熱點”,例如hashmap維護一個size變量,避免查看size時去遍歷,導致其他線程的寫阻塞
- 避免內嵌鎖
- 使用合適的並發工具
原子類
優劣(相比於鎖):粒度更細,但高度競爭時效率不高
大體類別:primary、array、reference、fieldupdater、adder、accumulator
reference:將引用的替換變為原子
fieldupdater:某個類的某個屬性升級為原子,而且可以選擇是否進行原子加減。具體例子查看learning_thread的atomic包
adder:比primary的increment效果好。本質是空間換時間,把不同線程對應到不同的cell上進行修改,降低沖突概率,使用多段鎖保證安全。內部有一個base變量和Cell數組,如果競爭不激烈,直接累加到base,如果競爭激烈,各個線程分散累加到自己的Cell[x]中(hash)。最后sum顯示當前結果,即base和cell的總和,但由於沒有加鎖,實際上的結果並非是快照。
accumulator:比adder更通用,適合並行計算場景,結合線程池。不過jdk8的並行流或許更好。
底層原理CAS
compare and set:通過比較內存的值是否與預期的值相等來判斷是否對內存的這個值進行更新。這個比較過程是CPU指令級別來保證原子性的。具體來說,Java 是通過它的 Unsafe 類來實現的,這個類包含不少 native 方法來調用操作系統的 api。
場景:樂觀鎖、並發容器(concurrentHashMap)、原子類
final(略)
並發容器
歷史拋棄:Vector、Hashtable、Collections.synchronizedList()
HashMap線程不安全的原因:
- 數據丟失原因:並發賦值時被覆蓋(table[index]=new entry)、已遍歷區間新增元素會丟失(transfer遷移數據時一些新增元素加到了已經遍歷的槽上)、新table被覆蓋(剛table = newtable,並插入元素,然后其他線程有執行table = newtable)等
- 死鏈原因(jdk7):不同線程擴容時對共享的舊table的同一slot上的鏈表進行修改時,使得entry的連接形成環,導致后來的put、get、transfer等操作遍歷該就table鏈時出現死循環。
ConcurrentHashMap原理:
-
put:

上面的「鎖住該槽」指鎖住數組中的 Node
-
get:計算hash,直接取值 or 紅黑找 or 遍歷鏈表找
不能用get、++、put,要直接用 replace
CopyOnWrite適合讀多寫少,例如黑名單、每日更新。和讀寫鎖的區別是讀和寫不互斥,但讀會有延遲。CopyOnWrite 可以在遍歷的時候修改集合元素,因為修改的對象和遍歷的對象是不一樣的。每次寫都會創建出一個新的對象。
並發隊列:
重要方法:
put、take:滿或者空會阻塞
add、remove、element:會拋異常,element返回頭元素,空拋異常
offer、poll:offer返回boolean,poll會刪除,都可阻塞
peek:peek不會刪除
ArrayBlockingQueue:指定容量,可以選擇是否公平。
LinkedBlockQueue:take和put分為兩把鎖
PriorityBlockingQueue:自然順序(非先進先出)
SynchronousQueue:容量為0
ConcurrentLinkedQueue:非阻塞隊列
選擇:容量不變/節省內存 ArrayBlockingQueue,容量可變 LinkedBlockQueue,是否需要排序,並發性
線程同步類
下面類基本淘汰了Object的wait、notify方式
CountDownLatch:經典是多等一或者一等多,但多等多也是可以的。針對事件,countdown並不會阻塞線程,不能復用,需要新建。
Semaphore:適合資源有限的限流場景,比如網關。類似輕量級的CountDownLatch。這個類更加注意公平,因為基本都是應對任務堆積的場景。
Condition:一個lock可以對應多個條件,所以更加靈活。
CyclicBarrier:針對線程的,因為調用的是await,可以復用
AQS
AbstractQueuedSynchronizer 是 jdk 中很多並發工具類的框架類,例如 ReentrantLock、Semaphore、CountDownLatch 等。這些類的內部會有一個靜態內部類繼承自AQS。它負責線程狀態的原子性管理、線程的阻塞和重啟、線程隊列管理。
AQS的三大核心:
- State:一個通過 CAS + volatile 實現安全並發增減的 int,不同並發類有不同含義。
- Deque(雙向鏈表):線程隊列
- 獲取和釋放方法:不同並發類有不同含義。獲取會先檢查 State,符合條件才成功獲取,並修改 State。釋放相反。
https://juejin.im/post/5c11d6376fb9a049e82b6253
https://mp.weixin.qq.com/s/sA01gxC4EbgypCsQt5pVog
Future
runnable 的 run 方法的返回值是 void 且沒有定義能夠拋出異常。正常來說,實現 run 方法的人更清楚這個任務會出什么異常,所以在 run 里面就把異常處理好是更好的選擇。
使用 Future,實現一個 Callable 接口
- 給線程傳入這個類的實例就能返回 Future 實例了。
- 用 FutureTask 來包裝這個實例,然后把 futuretask 交給線程執行。之后就能調用 futuretask 的方法了
future 接口的方法
boolean cancel(boolean mayInterruptIfRunning); // 如果清楚任務能夠處理好 interrupt,可以用 true
boolean isCancelled();
boolean isDone();
V get() throws InterruptedException, ExecutionException;
V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;
批量使用,定義一個 List<Callable<U>> 集合,然后交給executor,調用invokeAll,然后 block,直到所有任務完成。executor.invokeAll則只對最先完成的一個任務做處理,其他取消。下面則不需要等所有完成,可以提前對已完成的做處理。
ExecutorCompletionService service
= new ExecutorCompletionService(executor);
for (Callable<T> task : tasks) service.submit(task);
for (int i = 0; i < tasks.size(); i++) {
Process service.take().get()
// Do something else
}
public static CompletableFuture<String> readPage() {...}
public static List<String> getLinks(String content) {...}
CompletableFuture<String> contents = readPage();
CompletableFuture<List<URL>> links = contents.thenApply(Parser::getLinks);
當 contents 完成時,getLinks 就會被另一個線程調用。thenCompose 用於組合 T -> CompletableFuture<U> 和 U-> CompletableFuture<V> 為 T -> CompletableFuture<V>。其他一些方法可以讓一組future執行,只要有一個執行成功就放棄其他的。
參考:
玩轉Java並發工具,精通JUC,成為並發多面手
《碼出高效:Java開發手冊》
