目錄
概念、理論
併發:多個線程操作相同的資源,優點:效率高、資源利用率高,缺點:線程可能不安全、數據可能不一致,需要使用一些方式保證線程安全、數據一致
高併發:服務器能同時處理大量請求
線程安全:當多個線程訪問某個類,不管採用何種調度方式、線程如何交替執行,這個類都能表現出正確的行爲。
造成線程不安全的原因
- 存在共享資源
- 多個線程同時操作同一共享資源,操做不具有原子性
如何實現線程安全?
- 使多線程不同時操作同一共享資源:eg. 只使用單線程、必要的部分加鎖、使用juc的併發容器、併發工具類
- 使對共享資源的操作具有原子性:eg.使用原子類
- 不共享資源:eg. 使用ThreadLocal
- 用final修飾共享資源,使之只讀、不可修改
只要實現以上任意一點,即可實現線程安全
互斥鎖的特性
- 互斥性:同一時刻只能有1個線程對這部分數據進行操作,互斥性也常叫做操作的原子性
- 可見性:如果多個線程同時操作相同的數據(讀、寫),對數據做的修改能及時被其它線程觀測到。可見性用happens-before原則保證
鎖的實現原理
獲取鎖:把主內存中對應的共享資源讀取到本地內存中,將主內存中的該部分共享資源置爲無效
釋放鎖:把本地內存中的資源刷到主內存中,作爲共享資源,把本地內存中的該部分資源置爲無效
juc包簡介
juc包提供了大量的支持併發的類,包括
- 線程池executor
- 鎖locks,locks包及juc下一些常用類CountDownLatch、Semaphore基於AQS實現。jdk將同步的通用操作封裝在抽象類AbstractQueuedSynchronizer中,acquire()獲取資源的獨佔權(獲取鎖),release()釋放資源的獨佔權(釋放鎖)
- 原子類atomic,atomic包基於CAS實現,實現了多線程下無鎖操作
- 併發容器(集合)collections
- 併發工具類tools
實現線程安全的常用方式
synchronized
synchronized的用法
// 修飾普通方法
public synchronized void a(){
}
// 修飾靜態方法
public static synchronized void b(){
}
public static Object lock = new Object();
public void c(){
// 修飾代碼塊。同步代碼塊,鎖住一個對象
synchronized (lock){
}
}
synchronized可以修飾方法、代碼塊,修飾的操作是原子性的,同一時刻只能有1個線程訪問、執行
- 修飾普通方法,執行該方法時會自動鎖住該方法所在的對象
- 修飾靜態方法,加的是類鎖,執行該方法時會鎖住所在類的class對象,即鎖住該類所有實例
- 修飾代碼塊,加的是對象鎖,會鎖住指定對象
如果要修飾方法,儘量用普通方法,因爲靜態方法因爲會鎖住類所有的實例,嚴重影響效率。
synchronized的實現原理
synchronized使用對象作爲鎖,對象在內存的佈局分爲3部分:對象頭、實例數據、對齊填充,對象頭佔64位
- 前32位是Mark Word,存儲對象的hashCode、gc分代年齡、鎖類型、鎖標誌位等信息
- 後32位是類型指針,存儲對象所屬的類的元數據的引用,jvm通過類型指針確定此對象是哪個類的實例
Mark Work結構如下
每個對象都關聯了一個Monitor(這也是爲什麼每個對象都可以作爲鎖的原因),鎖的指針指向對象對應的Monitor,當某個線程持有鎖時,Monitor處於鎖定狀態
synchronized的4種鎖狀態及膨脹方向
無鎖 -> 偏向鎖 -> 輕量級鎖 -> 重量級鎖
- 無鎖:沒有線程要獲取鎖,未加鎖
- 偏向鎖:大多數情況下,鎖不存在多線程競爭,很多時候都是同一線程多次申請鎖。偏向鎖簡化了線程再次申請鎖的流程,減少了同一線程多次獲取同一個鎖的代價。偏向鎖只適用於鎖競爭不激烈的情況
- 輕量級鎖:適用於鎖競爭一般的情況
- 重量級鎖:適用於鎖競爭激烈的情況
使用Lock接口
synchronized使用前自動加鎖、使用完自動釋放鎖,很方便。synchronized是悲觀鎖的實現,每次操作共享資源前都要先加鎖;以前是重量級鎖,性能低,經過不斷優化,量級輕了很多,性能和Lock相比差距不再很大。
Lock需要自己加鎖、用完需要自己釋放。Lock是樂觀鎖的實現,每次先操作共享資源,提交修改時再驗證共享資源是否被其它線程修改過;Lock是輕量級鎖,性能很高。
Lock接口有很多實現類,常用的有ReentrantLock 可重入鎖、ReadWriteLock 讀寫鎖,也可以自己實現Lock接口來實現自定義的鎖。
ReentrantLock 可重入鎖
重入:一個線程再次獲取自己已持有的鎖
public class Xxx{
public final static ReentrantLock lock=new ReentrantLock(); //鎖對象都可以加個final防止被修改
//public final static ReentrantLock lock=new ReentrantLock(true); //可指定是否是公平鎖,缺省時默認false
public void a() {
lock.lock(); //獲取鎖,如果未獲取到鎖,會一直阻塞在這裏
// lock.tryLock(); //只嘗試1次,如果未獲取到鎖,直接失敗不執行後面的代碼
//.... //操作共享資源
lock.unlock(); //釋放鎖
}
public void b() {
try {
lock.tryLock(30, TimeUnit.SECONDS); //如果獲取鎖失敗,會在指定時間內不停嘗試。此句代碼可能會拋出異常
//.... //操作共享資源
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
if (!lock.isFair()){
lock.unlock(); //如果獲取到鎖,最終要釋放鎖
}
}
}
public void c() {
lock.lock();
try {
//.... //操作共享資源
} catch (Exception e) {
e.printStackTrace();
}finally {
lock.unlock(); //如果操作共享資源時可能發生異常,最終要釋放鎖
}
}
}
ReentrantLock如何實現公平鎖、非公平鎖?
使用鏈表存儲等待同一把鎖的線程,將線程添加到鏈表尾部,釋放鎖後
- 公平鎖:將鎖分配給鏈表頭部的線程
- 非公平鎖:將鎖分配個鏈表中的任意一個線程
將獲得鎖的線程從鏈表中移出
synchronized、ReentrantLock的比較
- synchronized是關鍵字,ReentrantLock是類
- 機制不同,synchronized是操作對象的Mark Word,ReentrantLock是使用Unsafe類的park()方法加鎖
- synchronized是非公平鎖,ReentrantLock可以設置是否是公平鎖
- ReentrantLock可以實現比synchronized更細粒度的控制,比如設置鎖的公平性
- 鎖競爭不激烈時,synchronized的性能往往要比ReentrantLock高;鎖競爭激烈時,synchronized膨脹爲重量級鎖,性能不如ReentrantLock
- ReentrantLock可以設置獲取鎖的等待時間,避免死鎖
ReadWriteLock 讀寫鎖
ReadWriteLock將鎖細粒度化分爲讀鎖、寫鎖,synchronized、ReentrantLock 同一時刻最多隻能有1個線程獲取到鎖,讀鎖同一時刻可以有多個線程獲取鎖,但都只能進行讀操作,寫鎖同一時刻最多隻能有1個線程獲取鎖進行寫操作,其它線程不能進行讀寫操作。
讀寫鎖做了更加細緻的權限劃分,加讀鎖時多個線程可以同時對共享資源進行讀操作,相比於synchronized、ReentrantLock,在以讀爲主的情況下可以提高性能。
ReadWriteLock是接口,常用的實現類是ReentrantReadWriteLock 可重入讀寫鎖。
public class Xxx {
public static ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
public static ReentrantReadWriteLock.ReadLock readLock = lock.readLock(); //從讀寫鎖獲取讀鎖
public static ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock(); //從讀寫鎖獲取寫鎖
//.....
public void a(){
//....
readLock.lock();
//..... 操作共享資源
readLock.unlock();
//....
}
}
讀鎖、寫鎖的操作方式和ReentrantLock完全相同,都可以設置超時,這3種鎖都是可重入鎖
鎖降級
在獲取寫鎖後,寫鎖可以降級爲讀鎖
public class Xxx {
public static ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
public static ReentrantReadWriteLock.ReadLock readLock = lock.readLock(); //讀鎖
public static ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock(); //寫鎖
//.....
public void a(){
//....
writeLock.lock(); //獲取寫鎖
//..... 對共享資源進行寫操作
readLock.lock(); //獲取讀鎖(仍然持有寫鎖)
writeLock.unlock(); //釋放寫鎖(只持有讀鎖,寫鎖->讀鎖,鎖降級)
//..... //對共享資源進行讀操作
readLock.unlock(); //釋放讀鎖
//....
}
}
- 鎖降級後,線程仍然持有寫鎖,需要自己釋放寫鎖
- 鎖降級的意義在於:後續對共享資源只進行讀操作,及時釋放寫鎖可以讓其它線程也能獲取到讀鎖、進行讀操作
- 鎖降級的應用場景:對數據比較敏感,在修改數據之後,需要校驗數據
- 寫鎖可以降級爲讀鎖,但讀鎖不能升級爲寫鎖
AQS如何用int值表示讀寫狀態
AbstractQueuedSynchronizer,抽象類
int,4字節32位,高位(前16位)表示讀鎖狀態,低位(後16位)表示寫鎖狀態。狀態指的是重入次數,最大爲2^16-1=65536
StampedLock
StampedLock是jdk1.8新增的類,可以獲取讀寫鎖、讀鎖、寫鎖,可以選擇悲觀鎖、樂觀鎖,但StampedLock是不可重入的,且API比其他方式複雜,使用難度稍高。
ThreadLocal
ThreadLocal維護了一個map,這個map中存儲的數據是當前線程獨有的。ThreadLocal可以保證各個線程的數據互不干擾,併發場景下可以實現無狀態調用,適用於各個線程依賴不同的變量值完成操作的場景。
public class Xxx {
private static ThreadLocal<Integer> i = ThreadLocal.withInitial(() -> 100); //必須要初始化值
public void a() {
i.set(20); //設置值
Integer value = i.get(); //獲取值
i.remove(); //移出set()賦的值,重置爲初始化時的值,即100
}
}
volatile
volatile的使用
public static volatile boolean flag = true; //禁止對此變量進行指令重排序
volatile只能修飾變量,實現了該變量的可見性、可以禁止指令重排序,當該變量的被某個線程修改時會自動通知其它使用此變量的線程。
volatile只實現了可見性,沒有實現原子性,嚴格來說並沒有實現線程安全,一般只用於
- 作爲開關 ,eg. while(flag){ }
- 在懶漢式單例中修飾對象實例,禁止指令重排序
volatile、synchronized的比較
原子類
i++、++i、i–、--i、+=、-=等操作都不是原子性的,juc的atomic包下的類提供了自增、自減、比較賦值、取值修改等原子性方法,可以線程安全地進行操作,因爲類中的方法都是原子性的,所有叫做原子類。
public class Xxx {
public static AtomicInteger i = new AtomicInteger(0); //int
public static AtomicLong l = new AtomicLong(0); //long
public static AtomicBoolean b = new AtomicBoolean(false); //boolean
public static AtomicReference<User> user = new AtomicReference<>(new User()); //引用
public static AtomicIntegerArray intArr = new AtomicIntegerArray(new int[]{1, 23}); //int[ ]
public static AtomicLongArray longArr = new AtomicLongArray(new long[]{1, 23}); //long[ ]
public static AtomicIntegerFieldUpdater<User> userId1 = AtomicIntegerFieldUpdater.newUpdater(User.class,"id"); //對象的int型字段
public static AtomicLongFieldUpdater<User> userId2 = AtomicLongFieldUpdater.newUpdater(User.class,"id"); //對象的long型字段
public static AtomicReferenceFieldUpdater<User, List> userOrderList= AtomicReferenceFieldUpdater.newUpdater(User.class, List.class,"orderList"); //對象的引用型字段
}
- 原子類使用CAS實現樂觀鎖,併發支持好、效率高
- CAS提交修改失敗時會while循環進行重試,如果重試時間過長,會給cpu帶來很大開銷
- 可能發生ABA問題。有2個原子類解決了ABA問題 :AtomicMarkableReference、AtomicStampedReference,使用標記、郵戳實現樂觀鎖,和版本號、時間戳機制差不多,避免了ABA問題。
- 只能保證單個變量的原子性,只能進行簡單操作,如果要保證多個變量、稍微複雜點的操作的原子性,要用其它方式來實現線程安全(一般是加鎖)
併發容器
Vector、Hashtable 的方法都使用synchronized修飾,是線程安全的,但缺點較多,基本不使用這2個類。
Collections.synchronizedXxx()可以將集合轉換爲同步集合,是使用synchronized鎖住整個集合,效率低下,不推薦。
juc提供了常用的併發容器,使用CAS保證線程安全,效率高,常見的併發容器如下
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>(); //有序,按照插入順序排列,內部使用Object[]存儲元素
CopyOnWriteArraySet<String> set = new CopyOnWriteArraySet<>(); //無序,CopyOnWriteArraySet內部使用CopyOnWriteArrayList存儲元素
ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>(); //map
ConcurrentLinkedQueue<String> queue1 = new ConcurrentLinkedQueue<>(); //基於鏈表的隊列
LinkedBlockingQueue<String> queue2 = new LinkedBlockingQueue<>(); //基於鏈表的阻塞隊列,如果參數指定了元素個數,則有界、不能擴容,如果未指定,則無界
ArrayBlockingQueue<String> queue3 = new ArrayBlockingQueue<>(20); //基於數組的阻塞隊列,指定容量,不能擴容(有界)
ArrayBlockingQueue<String> queue4 = new ArrayBlockingQueue<>(20,true); //可以指定是否是公平鎖,默認false
阻塞指的是,在進行某些操作時,會阻塞線程
在生產者/消費者的線程協作模式中,常用阻塞隊列LinkedBlockingQueue作爲倉庫
LinkedBlockingQueue<String> queue = new LinkedBlockingQueue<>(); //基於鏈表的阻塞隊列
//入隊的3個方法
queue.offer(""); //返會操作結果,boolean,如果隊列滿了放不下,返回false
queue.add(""); //返會操作結果,boolean,如果隊列滿了放不下,會拋出異常
try {
queue.put(""); //如果隊列滿了,會阻塞線程,直到隊列元素變少、可以放進去
} catch (InterruptedException e) {
e.printStackTrace();
}
//出隊的3個方法
queue.poll(); //如果隊列是空的,返回null
queue.remove(); //如果隊列是空的,會拋出異常
try {
queue.take(); //在隊列爲空的時候,會阻塞線程,直到有元素可彈出
} catch (InterruptedException e) {
e.printStackTrace();
}
併發工具類
CountDownLatch
CountDownLatch是一個計數器,常用於等待某些線程執行完畢
CountDownLatch countDownLatch = new CountDownLatch(2); //指定次數
new Thread(()->{
//.....
countDownLatch.countDown(); //次數-1
}).start();
new Thread(()->{
//......
countDownLatch.countDown();
}).start();
try {
countDownLatch.await(); //阻塞當前線程,直到次數爲0時才繼續往下執行,即等待2個線程執行完畢
//......
} catch (InterruptedException e) {
e.printStackTrace();
}
CyclicBarrier 柵欄
CyclicBarrier cyclicBarrier = new CyclicBarrier(3); //指定await的線程數
new Thread(()->{
//......
try {
cyclicBarrier.await(); //第一個
//.....
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
new Thread(()->{
//......
try {
cyclicBarrier.await(); //第二個
//.....
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
//.....
try {
cyclicBarrier.await(); //第三個
//.....
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
線程執行到await()處會阻塞,停下來,直到指定數量的線程都執行到await()纔會繼續往下執行。
CountDownLatch用於一些線程等待另一些線程執行完畢,類似超市收銀員等待顧客挑好東西來結賬;CyclicBarrier用於指定數量的線程互相等待,類似於大家指定地點集合。
Semaphore 信號量
Semaphore用於限流
Semaphore semaphore = new Semaphore(2); //指定信號量
// Semaphore semaphore = new Semaphore(2,true); //可指定是否使用公平鎖,默認false
new Thread(() -> {
//......
try {
semaphore.acquire(); //使用1個信號量,信號量-1。如果信號量爲0,沒有可用的信號量,阻塞線程直到獲取到信號量
//....
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release(); //操作完釋放信號量,信號量+1
}
}).start();
Exchanger
交換機,用於2條線程之間交換數據,只能用於2條線程之間,即一個Exchanger對象只能被2條線程使用(成對)
Exchanger<String> stringExchanger = new Exchanger<>(); //泛型指定交換的數據類型
new Thread(()->{
try {
String data = stringExchanger.exchange("are you ok?");
System.out.println("線程1接收到的數據:" + data); //ok
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
new Thread(()->{
try {
String data = stringExchanger.exchange("ok");
System.out.println("線程2接收到的數據:" + data); //are you ok
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
單例與線程安全
單例有2種模式
- 餓漢式:在類加載時就實例化,線程安全
- 懶漢式:在需要使用實例時才實例化,可能是線程不安全的
餓漢式
//餓漢式
public class A {
private static A a=new A(); //用靜態成員保存實例,調用構造方法創建實例。類加載時會初始化靜態成員
//.....
private A(){ //構造方法私有化,隱藏掉
}
public static A getInstance(){ //把獲取實例的方法暴露出去
return a; //只有1步,原子性,線程安全
}
//.....
}
懶漢式
//懶漢式 寫法一
class A {
private static A a; //用靜態成員保存實例
//.....
private A(){ //構造方法私有化,隱藏掉
}
public static A getInstance(){ //暴露獲取實例的方法,多步,不具有原子性,不是線程安全的
if (null==a){
a = new A();
}
return a;
}
//.....
}
寫法二:用synchronized修飾獲取獲取實例的靜態方法,但這種方式獲取實例時會鎖住類,使多個線程不能同時獲取實例,效率低下
//懶漢式 寫法三
class A {
private static volatile A a; //volatile禁止指令重排序
//.....
private A(){
}
public static A getInstance(){
if (null==a){
synchronized (A.class){ //優化寫法,只在創建實例時鎖住類
a = new A();
}
}
return a;
}
//.....
}
鎖的分類
-
自旋鎖:未獲取到鎖時進入等待狀態,多線程切換上下文會消耗系統資源,頻繁切換上下文不值得,jvm會在線程沒獲取到鎖時,暫時執行空循環等待獲取鎖,即自旋,循環次數即自旋次數;如果在指定自旋次數內沒獲取到鎖,則掛起線程,切換上下文,執行其它線程。鎖默認是自旋的。
-
自適應自旋鎖:自旋次數不固定,由上一次獲取該鎖的自旋時間及鎖持有者的狀態決定,更加智能
-
阻塞鎖:阻塞鎖會改變線程的運行狀態,讓線程進入阻塞狀態進行等待,當獲得相應信號(喚醒或阻塞時間結束)時,進入就緒狀態
-
重入鎖:已持有鎖的線程,在未釋放鎖時,可以再次獲取到該鎖
public class Xxx{
public final static ReentrantLock lock=new ReentrantLock();
public void a() {
lock.lock();
//.....
b(); //如果鎖是可重入的,則b()直接獲取到鎖;如果鎖不是可重入的,則b()需要單獨獲取獲取鎖,但鎖還沒被a()釋放,b()會一直獲取不到鎖
//.....
lock.unlock();
}
public void b() {
lock.lock();
//......
lock.unlock();
}
}
-
讀鎖:是一種共享鎖 | S鎖(share),多條線程可同時操作共享資源,但都只能進行讀操作、不能進行寫操作
-
寫鎖:是一種排它鎖 | 互斥鎖 | 獨佔鎖 | X鎖,同一時刻最多隻能有1個線程可以對共享資源進行寫操作,其它線程不能對該資源進行讀寫
-
悲觀鎖:每次操作共享資源時,認爲期間其它線程一定會修改共享資源,每次操作共享數據之前,都要給共享資源加鎖
-
樂觀鎖:每次操作共享資源時,認爲期間其它線程一般不會修改共享資源,操作共享資源時不給共享資源加鎖,只在提交修改時驗證數據是否被其它線程修改過,常用版本號等方式實現樂觀鎖
-
公平鎖:等待鎖的線程按照先來先得順序獲取鎖(慎用)
-
非公平鎖:釋放鎖後,等待鎖的線程都可能獲取到鎖,不是先來先得
非公平鎖可能導致某些線程長時間甚至一直獲取不到鎖,但這種情況畢竟是極少數;使用公平鎖,爲保證公平性有額外的開銷,會降低性能,所以一般使用非公平鎖
- 偏向鎖:初次獲取鎖後,鎖進入偏向模式,當獲取過鎖的線程再次獲取該鎖時會簡化獲取鎖的流程,即鎖偏向於曾經獲取過它的線程
鎖消除:編譯時會掃描上下文,自動去除不可能存在線程競爭的鎖
鎖細化:如果只操作共享資源的一部分,不用給整個共享資源加鎖,只需給要操作的部分加鎖即可。使用細粒度的鎖可以讓多個線程同時操作共享資源的不同部分,提高效率。
鎖粗化:要操作共享資源的多個部分,如果每次只給部分加鎖,頻繁加鎖、釋放鎖會影響性能,可以擴大鎖的作用範圍,給整個共享資源加鎖,避免頻繁加鎖帶來的開銷。
指令重排序
指令重排序:編譯器、處理器會對指令序列重新排序,提高執行效率、優化程序性能
int a=1;
int b=1;
以上2條指令會被重排序,可能2條指令併發執行,可能int a=1;先執行,可能int b=1;先執行。
指令重排序遵循的2個原則
1、 數據依賴性,不改變存在數據依賴關係的兩個操作的執行順序。
int a=1;
int b=a;
b依賴於a,重排序不能改變這2個語句的執行順序
2、as-if-serial原則,重排序不能改變單條線程的執行結果
int a=1;
int b=a;
執行結果是a=1、b=1,重排序後執行得到的也要是這個結果
數據同步接口
有時候需要對接第三方的項目,或者公司大部門之間對接業務,不能直接連接、操作他們的數據庫,一般是建中間庫|中間表,把我們|他們需要的數據放到中間庫|表中,去中間庫|表獲取數據。更新數據庫時需要同步更新中間庫|表。
中間表的設計
- 只存儲要使用的字段即可
- 需要用一個字段記錄該條數據的狀態:已入庫、正在處理、處理時發生異常、已處理
- 需要用一個字段記錄數據入庫時間
- 需要用一個字段記錄處理時間
記錄時間是爲了日後好排查問題、統計分析
對中間表的處理
可以使用生產者/消費者的線程協作模式
- 生產者分批讀取中間表中未處理的數據 where status=‘xxx’,放到倉庫中。因爲數據量一般很大,所以通常要分批讀取,防止倉庫裝不下。如果要操作多張表,很多操作都差不多,可以抽象出接口
- 消費者處理倉庫中的數據
操作時需要更新中間表中的數據狀態、處理時間