多線程及鎖總結
注:本博客參考了網上的文章結合自己工作總結后所寫,主要用於記錄自己工作所得,如有錯誤請批評指正。
參考:https://blog.csdn.net/tyyj90/article/details/78236053
參考:https://www.cnblogs.com/wxd0108/p/5479442.html
想了解多線程底層及原理的
參考:http://hbprotoss.github.io/post/java多線程總結/
1.多線程簡介
1.1簡介:
線程是操作系統能夠進行運算調度的最小單位,它被包含在進程之中,是進程中的實際運作單位。程序員可以通過它進行多處理器編程,你可以使用多線程對運算密集型任務提速。
1.2使用條件:
多個任務執行順序不影響結果。
1.3使用場景:
- 提高用戶體驗,用戶交互問題、避免用戶等待時間過長。
- 減少阻塞,為了等待網絡、文件系統、用戶或其他I/O響應而耗費大量的執行時間,此時使用多線程可以提高效率。
- 可拆分的大任務,大任務處理起來比較耗時,使用多線程並行加快處理(例如:分片上傳,分布式計算)。
- 監聽輪詢重發任務。
- 其他。
2. 多線程同步實現
目前常用的能實現多線程同步的有:
- synchroized配合wait()、notify()、notifyAll()
- Lock類ReentrantLock重入鎖、ReentrantReadWriteLock讀寫鎖及結合condition
- 使用特殊域變量(volatile)實現線程同步
- 使用局部變量(ThreadLocal)實現線程同步等
2.1 synchronized
synchronized是java關鍵字,它可以作為函數的修飾符,也可作為函數內的語句,可以配合await(),notify()/notifyAll()使用。
Synchronized的作用主要有三個:
- 確保線程互斥的訪問同步代碼。
- 保證共享變量的修改能夠及時可見。
- 有效解決重排序問題。
2.1.1 synchronized與Lock區別
| 類別 | synchronized | Lock |
|---|---|---|
| 存在層次 | Java的關鍵字,在jvm層面上 | 是一個類 |
| 鎖的釋放 | 1、以獲取鎖的線程執行完同步代碼,釋放鎖 2、線程執行發生異常,jvm會讓線程釋放鎖 | 在finally中必須釋放鎖,不然容易造成線程死鎖 |
| 鎖的獲取 | 假設A線程獲得鎖,B線程等待。如果A線程阻塞,B線程會一直等待 | 分情況而定,Lock有多個鎖獲取的方式,具體下面會說道,大致就是可以嘗試獲得鎖,線程可以不用一直等待 |
| 鎖狀態 | 無法判斷 | 可以判斷 |
| 鎖類型 | 可重入 不可中斷 非公平 | 可重入 可中斷 可公平(兩者皆可) |
| 性能 | 少量同步 | 大量同步 |
2.2 Lock類
Lock接口的主要api:
- lock():獲取鎖,如果鎖被暫用則一直等待
- unlock():釋放鎖
- tryLock(): 注意返回類型是boolean,如果獲取鎖的時候鎖被占用就返回false,否則返回true
- tryLock(long time, TimeUnit unit):比起tryLock()就是給了一個時間期限,保證等待參數時間
- lockInterruptibly():用該鎖的獲得方式,如果線程在獲取鎖的階段進入了等待,那么可以中斷此線程,先去做別的事
Lock鎖分下面兩種:
- 重入鎖ReentrantLock
- 讀寫鎖ReentrantReadWriteLock
2.2.1 重入鎖ReentrantLock
可重入的意義在於持有鎖的線程可以繼續持有,並且要釋放對等的次數后才真正釋放該鎖。是可中斷鎖,通過lockInterruptibly()+interrupt()實現。
ReentrantLock也可以設置為公平鎖,ReentrantLock lock = new ReentrantLock(true);true為公平鎖,false非公平(默認)。
ReentrantLock的一些方法:
- isFair():判斷鎖是否是公平鎖
- isLocked():判斷鎖是否被任何線程獲取了
- isHeldByCurrentThread():判斷鎖是否被當前線程獲取了
- hasQueuedThreads():判斷是否有線程在等待該鎖
代碼示例:
private Lock lock = new ReentrantLock();
public void testMethod() {
try {
lock.lock();
doSomeThing();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
finally
{
lock.unlock();
}
}
使用流程:
- 先new一個實例
private Lock lock = new ReentrantLock(); - 加鎖
r.lock()或r.lockInterruptibly();
此處也是個不同,后者可被打斷。當a線程lock后,b線程阻塞,此時如果是lockInterruptibly,那么在調用b.interrupt()之后,b線程退出阻塞,並放棄對資源的爭搶,進入catch塊。(如果使用后者,必須throw interruptable exception 或catch) - 釋放鎖
r.unlock()
必須做!何為必須做呢,要放在finally里面。以防止異常跳出了正常流程,導致災難。這里補充一個小知識點,finally是可以信任的:經過測試,哪怕是發生了OutofMemoryError,finally塊中的語句執行也能夠得到保證。否則可能造成死鎖。
2.2.2 讀寫鎖ReentrantReadWriteLock
ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
ReadLock r = lock.readLock();
WriteLock w = lock.writeLock();
用法類似重入鎖ReentrantLock不多介紹。
兩者都有lock,unlock方法。寫寫,寫讀互斥;讀讀不互斥。可以實現並發讀的高效線程安全代碼
2.2.3 Condition
Lock對象里面可以創建多個Condition(對象監視器)實例,線程對象可以注冊在指定Condition中,從而有選擇性的進行線程通知,在調度線程上更加靈活。
Condition中的await()方法相當於Object的wait()方法,Condition中的signal()方法相當於Object的notify()方法,Condition中的signalAll()相當於Object的notifyAll()方法。
Condition相比Object的wait()notify()具有更細粒度,對於同一個鎖,我們可以創建多個Condition,在不同的情況下使用不同的Condition。
代碼示例:
await方法
public class MyService {
private Lock lock = new ReentrantLock();
private Condition condition=lock.newCondition();
public void testMethod() {
try {
lock.lock();
System.out.println("開始wait");
condition.await();
for (int i = 0; i < 5; i++) {
System.out.println("ThreadName=" + Thread.currentThread().getName()
+ (" " + (i + 1)));
}
} catch (InterruptedException e) {
// TODO 自動生成的 catch 塊
e.printStackTrace();
}
finally
{
lock.unlock();
}
}
}
通過創建Condition對象來使線程wait,必須先執行lock.lock方法獲得鎖
signal方法
public void signal()
{
try
{
lock.lock();
condition.signal();
}
finally
{
lock.unlock();
}
}
condition對象的signal方法可以喚醒wait線程
2.3 volatile實現線程同步
聲明變量是 volatile 的,JVM 保證了每次讀變量都從內存中讀,跳過 CPU cache 這一步。volatile不會提供任何原子操作,它也不能用來修飾final類型的變量。
當一個變量定義為 volatile 之后,將具備兩種特性:
-
保證此變量對所有的線程的可見性,這里的“可見性”,當一個線程修改了這個變量的值,volatile 保證了新值能立即同步到主內存,以及每次使用前立即從主內存刷新。但普通變量做不到這點,普通變量的值在線程間傳遞均需要通過主內存來完成。
-
禁止指令重排序優化。有volatile修飾的變量,賦值后多執行了一個“load addl $0x0, (%esp)”操作,這個操作相當於一個內存屏障(指令重排序時不能把后面的指令重排序到內存屏障之前的位置),只有一個CPU訪問內存時,並不需要內存屏障;(什么是指令重排序:是指CPU采用了允許將多條指令不按程序規定的順序分開發送給各相應電路單元處理)。
2.4 ThreadLocal實現線程同步
一般同步機制采用了“以時間換空間”的方式,提供一份變量,維護一個隊列讓不同的線程排隊訪問。線程間數據共享。
ThreadLocal采用了“以空間換時間”的方式。為每一個線程都提供了一份副本變量,因此可以同時訪問而互不影響。線程間數據隔離。
如果使用ThreadLocal管理變量,則每一個使用該變量的線程都獲得該變量的副本,副本之間相互獨立,這樣每一個線程都可以隨意修改自己的變量副本,而不會對其他線程產生影響。
優點:效率高。缺點:耗內存。
原理:維護一個以線程為key,變量為value的map。
api:
- T get():返回此線程局部變量的當前線程副本中的值,如果這是線程第一次調用該方法,則創建並初始化此副本。
- void remove():移除此線程局部變量的值。這可能有助於減少線程局部變量的存儲需求。如果再次訪問此線程局部變量,那么在默認情況下它將擁有其 initialValue。
- void set(T value):將此線程局部變量的當前線程副本中的值設置為指定值。許多應用程序不需要這項功能,它們只依賴於 initialValue() 方法來設置線程局部變量的值(需要重寫initialValue())。
代碼示例:
ThreadLocal在hibernateUtil應用
private static ThreadLocal<Session> threadLocal = new ThreadLocal<Session>()
public static Session getCurrentSession(){
Session s=(Session) threadLocal.get();
//打開一個新的session,如果這個線程還不存在的話
if(s==null) {
s=sessionFactory.openSession();
threadLocal(s);
}
return s;
}
3.多線程鎖
常見的鎖:
- 公平鎖/非公平鎖
- 可重入鎖
- 獨享鎖/共享鎖
- 互斥鎖/讀寫鎖
- 樂觀鎖/悲觀鎖
- 分段鎖
- 偏向鎖/輕量級鎖/重量級鎖
- 自旋鎖
- 其他鎖
這些分類並不是全是指鎖的狀態,有的指鎖的特性,有的指鎖的設計。
3.1 悲觀鎖/樂觀鎖:
注:悲觀鎖/樂觀鎖並不是鎖的類型,而是指看待並發同步的角度。
悲觀鎖:阻塞同步,會上鎖,synchorized關鍵詞是一種悲觀鎖。Lock鎖也是一種悲觀鎖。
樂觀鎖:非阻塞同步,不會上鎖,利用CAS原理,在更新的時候會判斷一下在此期間別人有沒有去更新這個數據,可以使用版本號等機制。樂觀鎖適用於多讀的應用類型,這樣可以提高吞吐量。 Ps(cas設計缺陷可能會造成ABA問題)
3.2 公平鎖/非公平鎖:
公平鎖:盡量以請求鎖的順序來獲取鎖。按順序排隊排最前的獲取鎖,即等的最久的獲取鎖。線程獲取鎖執行完后需要重新排隊。
非公平鎖:無法保證鎖的獲取是按照請求鎖的順序進行的。synchorized為非公平鎖,Lock默認為非公平鎖,可設置為公平鎖。非公平鎖的優點在於吞吐量比公平鎖大。
可中斷鎖/不可中斷鎖:
顧名思義不解釋。
synchorized是一種不可中斷鎖,Lock可以通過lockInterruptibly()+interrupt()實現可中斷
3.3 獨享鎖/共享鎖:
獨享鎖指該鎖一次只能被一個線程所持有,共享鎖可以被多個線程持有。
ReentrantLock,synchorized是獨享鎖,ReadWriteLock中讀鎖為共享鎖。共享鎖可保證並發,效率高。
3.4 互斥鎖/讀寫鎖:
互斥鎖/讀寫鎖是對獨享鎖/共享鎖的實現。
3.5 分段鎖:
分段鎖其實是一種鎖的設計,並不是具體的一種鎖,對於ConcurrentHashMap而言,其並發的實現就是通過分段鎖的形式來實現高效的並發操作,ConcurrentHashMap中的分段鎖稱為Segment,它即類似於HashMap(JDK7與JDK8中HashMap的實現)的結構,即內部擁有一個Entry數組,數組中的每個元素又是一個鏈表;同時又是一個ReentrantLock(Segment繼承了ReentrantLock)。當需要put元素的時候,並不是對整個HashMap進行加鎖,而是先通過hashcode來知道他要放在那一個分段中,然后對這個分段進行加鎖,所以當多線程put的時候,只要不是放在一個分段中,就實現了真正的並行的插入。但是,在統計size的時候,可就是獲取HashMap全局信息的時候,就需要獲取所有的分段鎖才能統計。
分段鎖的設計目的是細化鎖的粒度,當操作不需要更新整個數組的時候,就僅僅針對數組中的一項進行加鎖操作。
Ps(HashTable容器使用synchronized來保證線程安全,所有訪問HashTable的線程都競爭同一把鎖,高並發訪問時效率遠低於ConcurrentHashMap)
3.6 自旋鎖:
在Java中,自旋鎖是指嘗試獲取鎖的線程不會立即阻塞,而是采用循環的方式去嘗試獲取鎖,這樣的好處是減少線程上下文切換的消耗,缺點是循環會消耗CPU。
