</td>
<td>
</td>
<td>
<p><span lang="EN-US">cms_free</span></p>
</td>
<td>
<p><span>分代年齡<span lang="EN-US"></span></span></p>
</td>
<td colspan="2">
<p><span>偏向鎖<span lang="EN-US"></span></span></p>
</td>
<td>
<p><span>鎖標誌位<span lang="EN-US"></span></span></p>
</td>
</tr><tr><td>
<p><span>無鎖<span lang="EN-US"></span></span></p>
</td>
<td>
<p><span lang="EN-US">unused</span></p>
</td>
<td>
<p><span lang="EN-US">hashCode</span></p>
</td>
<td>
</td>
<td>
</td>
<td>
</td>
<td colspan="2">
<p><span lang="EN-US">01</span></p>
</td>
</tr><tr><td>
<p><span>偏向鎖<span lang="EN-US"></span></span></p>
</td>
<td colspan="2">
<p><span lang="EN-US">ThreadID(54bit) Epoch(2bit)</span></p>
</td>
<td>
</td>
<td>
</td>
<td>
<p><span lang="EN-US">1</span></p>
</td>
<td colspan="2">
<p><span lang="EN-US">01</span></p>
</td>
</tr></tbody></table>
複製程式碼
在執行期間 Mark Word 裡儲存的資料會隨著鎖標誌位的變化而變化。
在瞭解了相關概念後,接下來介紹Java是如何保證併發程式設計中的安全的。
四、synchronized
用法
將多條操作共享資料的執行緒程式碼封裝起來,當有執行緒在執行這些程式碼的時候,其他執行緒時不可以參與運算的。必須要當前執行緒把這些程式碼都執行完畢後,其他執行緒才可以參與運算。
synchronized(物件)
{
需要被同步的程式碼 ;
}
複製程式碼
修飾符 synchronized 返回值 方法名(){
}
複製程式碼
synchronized 的作用主要有三個:
(1)確保執行緒互斥的訪問同步程式碼
(2)保證共享變數的修改能夠及時可見
(3)有效解決重排序問題。
鎖物件
- 對於同步方法,鎖是當前
例項物件 。
- 對於靜態同步方法,鎖是當前物件的
Class 物件 。
- 對於同步方法塊,鎖是
synchonized 括號裡配置的物件。
實現原理
在編譯的位元組碼中加入了兩條指令來進行程式碼的同步。
monitorenter :
每個物件有一個監視器鎖(monitor) 。當monitor 被佔用時就會處於鎖定狀態,執行緒執行monitorenter 指令時嘗試獲取monitor 的所有權,過程如下:
- 如果
monitor 的進入數為0,則該執行緒進入monitor ,然後將進入數設定為1,該執行緒即為monitor 的所有者。
- 如果執行緒已經佔有該
monitor ,只是重新進入,則進入monitor 的進入數加1.
- 如果其他執行緒已經佔用了
monitor ,則該執行緒進入阻塞狀態,直到monitor 的進入數為0,再重新嘗試獲取monitor 的所有權。
monitorexit:
執行monitorexit 的執行緒必須是objectref 所對應的monitor 的所有者。
指令執行時,monitor 的進入數減1,如果減1後進入數為0,那執行緒退出monitor ,不再是這個monitor 的所有者。其他被這個monitor 阻塞的執行緒可以嘗試去獲取這個 monitor 的所有權。
synchronized 的語義底層是通過一個monitor 的物件來完成,其實wait/notify 等方法也依賴於monitor 物件,這就是為什麼只有在同步的塊或者方法中才能呼叫wait/notify 等方法,否則會丟擲java.lang.IllegalMonitorStateException 的異常的原因。
好處和弊端
好處:解決了執行緒的安全問題。
弊端:相對降低了效率,因為同步外的執行緒的都會判斷同步鎖。獲得鎖和釋放鎖帶來效能消耗。
編譯器對synchronized優化
Java6 為了減少獲得鎖和釋放鎖所帶來的效能消耗,引入了“偏向鎖”和“輕量級鎖”,所以在Java6 裡鎖一共有四種狀態:無鎖狀態,偏向鎖狀態,輕量級鎖狀態和重量級鎖狀態,它會隨著競爭情況逐漸升級。鎖可以升級但不能降級。
-
偏向鎖:大多數情況下鎖不僅不存在多執行緒競爭,而且總是由同一執行緒多次獲得。偏向鎖的目的是在某個執行緒獲得鎖之後(執行緒的id會記錄在物件的Mark Wod 中),消除這個執行緒鎖重入(CAS)的開銷,看起來讓這個執行緒得到了偏護。
-
輕量級鎖(CAS):輕量級鎖是由偏向鎖升級來的,偏向鎖執行在一個執行緒進入同步塊的情況下,當第二個執行緒加入鎖爭用的時候,偏向鎖就會升級為輕量級鎖;輕量級鎖的意圖是在沒有多執行緒競爭的情況下,通過CAS操作嘗試將MarkWord更新為指向LockRecord的指標,減少了使用重量級鎖的系統互斥量產生的效能消耗。
-
重量級鎖:虛擬機器使用CAS操作嘗試將MarkWord更新為指向LockRecord的指標,如果更新成功表示執行緒就擁有該物件的鎖;如果失敗,會檢查MarkWord是否指向當前執行緒的棧幀,如果是,表示當前執行緒已經擁有這個鎖;如果不是,說明這個鎖被其他執行緒搶佔,此時膨脹為重量級鎖。
鎖狀態對應的Mark Word
以32位JVM為例:
鎖狀態
|
25 bit
|
4bit
|
1bit
|
2bit
|
23bit
|
2bit
|
是否是偏向鎖
|
鎖標誌位
|
輕量級鎖
|
指向棧中鎖記錄的指標
|
00
|
重量級鎖
|
指向互斥量(重量級鎖)的指標
|
10
|
GC標記
|
空
|
11
|
偏向鎖
|
執行緒ID
|
Epoch
|
物件分代年齡
|
1
|
01
|
五、volatile
volatile 是Java中的一個關鍵字,用來修飾共享變數(類的成員變數、類的靜態成員變數)。
被修飾的變數包含兩層語義:
執行緒寫入變數時不會把變數寫入快取,而是直接把值重新整理回主存。同時,其他執行緒在讀取該共享變數的時候,會從主記憶體重新獲取值,而不是使用當前快取中的值。(因此會帶來一部分效能損失)。注意:往主記憶體中寫入的操作不能保證原子性。
禁止指令重排序有兩層意思:
1)當程式執行到volatile 變數的讀操作或者寫操作時,在其前面的操作的更改肯定全部已經進行,且結果已經對後面的操作可見;在其後面的操作肯定還沒有進行;
2)在進行指令優化時,不能將在對volatile 變數訪問的語句放在其後面執行,也不能把volatile 變數後面的語句放到其前面執行。
**底層實現:**觀察加入volatile 關鍵字和沒有加入volatile 關鍵字時所生成的彙編程式碼發現,加入volatile 關鍵字時,會多出一個lock字首指令 。
六、Lock
應用場景
如果一個程式碼塊被synchronized 修飾了,當一個執行緒獲取了對應的鎖,並執行該程式碼塊時,其他執行緒便只能一直等待,等待獲取鎖的執行緒釋放鎖,而這裡獲取鎖的執行緒釋放鎖只會有兩種情況:
- 獲取鎖的執行緒執行完了該程式碼塊,然後執行緒釋放對鎖的佔有;
- 執行緒執行發生異常,此時JVM會讓執行緒自動釋放鎖。
如果這個獲取鎖的執行緒由於要等待IO或者其他原因(比如呼叫sleep方法)被阻塞了,但是又沒有釋放鎖,會讓程式效率很差。
因此就需要有一種機制可以不讓等待的執行緒一直無期限地等待下去(比如只等待一定的時間或者能夠響應中斷),通過Lock 就可以辦到。
原始碼分析
與Lock相關的介面和類位於J.U.C 的java.util.concurrent.locks 包下。
(1)Lock介面
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
複製程式碼
- 獲取鎖
lock():獲取鎖,如果鎖被暫用則一直等待。
tryLock(): 有返回值的獲取鎖。注意返回型別是
boolean ,如果獲取鎖的時候鎖被佔用就返回false ,否則返回true 。
tryLock(long time, TimeUnit unit):比起tryLock()就是給了一個時間期限,保證等待引數時間。
lockInterruptibly():當通過這個方法去獲取鎖時,如果執行緒正在等待獲取鎖,則這個執行緒能夠響應中斷,即中斷執行緒的等待狀態。也就使說,當兩個執行緒同時通過lock.lockInterruptibly() 想獲取某個鎖時,假若此時執行緒A獲取到了鎖,而執行緒B只有在等待,那麼對執行緒B呼叫threadB.interrupt() 方法能夠中斷執行緒B的等待過程。
注意:當一個執行緒獲取了鎖之後,是不會被interrupt() 方法中斷的。因為本身在前面的文章中講過單獨呼叫interrupt() 方法不能中斷正在執行過程中的執行緒,只能中斷阻塞過程中的執行緒。因此當通過lockInterruptibly() 方法獲取某個鎖時,如果不能獲取到,只有進行等待的情況下,是可以響應中斷的。用synchronized 修飾的話,當一個執行緒處於等待某個鎖的狀態,是無法被中斷的,只有一直等待下去。
(2)ReentrantLock類
ReentrantLock ,意思是“可重入鎖”。ReentrantLock 是唯一實現了Lock 介面的類,並且ReentrantLock 提供了更多的方法,基於AQS(AbstractQueuedSynchronizer) 來實現的。
並且,ConcurrentHashMap 並沒有採用synchronized 進行控制,而是使用了ReentrantLock 。
- 構造方法
ReentrantLock 分為公平鎖和非公平鎖,可以通過構造方法來指定具體型別:
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
複製程式碼
public void lock() {
sync.lock();
}
複製程式碼
而sync 是一個abstract 內部類:
abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = -5179523762034025860L;
abstract void lock();
複製程式碼
其lock() 方法用的是構造得到的FairSync 物件,即sync 的實現類。
public ReentrantLock() {
sync = new NonfairSync();
}
static final class NonfairSync extends Sync {
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
複製程式碼
而compareAndSetState 是AQS 的一個方法,也就是基於CAS 操作。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
複製程式碼
嘗試進一步獲取鎖(呼叫繼承自父類sync 的final 方法):
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
複製程式碼
首先會判斷 AQS 中的 state 是否等於 0,0表示目前沒有其他執行緒獲得鎖,當前執行緒就可以嘗試獲取鎖。如果 state 大於 0 時,說明鎖已經被獲取了,則需要判斷獲取鎖的執行緒是否為當前執行緒(ReentrantLock 支援重入),是則需要將 state + 1,並將值更新。
如果 tryAcquire(arg) 獲取鎖失敗,則需要用addWaiter(Node.EXCLUSIVE) 將當前執行緒寫入佇列中。寫入之前需要將當前執行緒包裝為一個 Node 物件(addWaiter(Node.EXCLUSIVE)) 。
即回到:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
複製程式碼
公平鎖和非公平鎖的釋放流程都是一樣的:
public void unlock() {
sync.release(1);
}
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
複製程式碼
(3)ReadWriteLock介面與ReentrantReadWriteLock類
public interface ReadWriteLock {
Lock readLock();
Lock writeLock();
}
複製程式碼
在ReentrantLock 中,執行緒之間的同步都是互斥的,不管是讀操作還是寫操作,但是在一些場景中讀操作是可以並行進行的,只有寫操作才是互斥的,這種情況雖然也可以使用ReentrantLock 來解決,但是在效能上也會損失,ReadWriteLock 就是用來解決這個問題的。
- 實現-ReentrantReadWriteLock類
在ReentrantReadWriteLock 中分別定義了讀鎖和寫鎖,與ReentrantLock 類似,讀鎖和寫鎖的功能也是通過Sync 實現的,Sync 存在公平和非公平兩種實現方式,不同的是表示鎖狀態的state 的定義,在ReentrantReadWriteLock 中具體定義如下:
static final int SHARED_SHIFT = 16;
static final int SHARED_UNIT = (1 << SHARED_SHIFT);
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
static final class HoldCounter {
int count = 0;
final long tid = Thread.currentThread().getId();
}
static final class ThreadLocalHoldCounter extends ThreadLocal<HoldCounter> {
public HoldCounter initialValue() {
return new HoldCounter();
}
}
private transient ThreadLocalHoldCounter readHolds;
private transient HoldCounter cachedHoldCounter;
private transient Thread firstReader = null;
private transient int firstReaderHoldCount;
複製程式碼
其中,包含兩個靜態內部類:ReadLock() 與WriteLock() ,都實現了Lock介面 。
獲取讀鎖:
- 如果不存線上程持有寫鎖,則獲取讀鎖成功。
- 如果其他執行緒持有寫鎖,則獲取讀鎖失敗。
- 如本執行緒持有寫鎖,並且不存在等待寫鎖的其他執行緒,則獲取讀鎖成功。
- 如本執行緒持有寫鎖,並且存在等待寫鎖的其他執行緒,則如果本執行緒已經持有讀鎖,則獲取讀鎖成功,如果不能存在讀鎖,則此次獲取讀鎖失敗。
獲取寫鎖:
- 判斷是否有執行緒持有鎖,包括讀鎖和寫鎖,如果有,則執行步驟2,否則步驟3
- 如果寫鎖為空(此時由於1步驟判斷存在鎖,則存在持有讀鎖的執行緒),或者持有寫鎖的不是本執行緒,直接返回失敗,如果寫鎖數量大於MAX_COUNT,返回失敗,否則更新state,並且返回true
- 如果需要寫鎖堵塞判斷,或者CAS失敗直接返回false,否則設定持有寫鎖的執行緒為本執行緒,並且返回true
- 通過writerShouldBlock寫鎖堵塞判斷
final boolean writerShouldBlock() {
return hasQueuedPredecessors();
}
public final boolean hasQueuedPredecessors() {
Node t = tail;
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
複製程式碼
七、比較
Lock和synchronized
synchronized 是基於JVM層面實現的,而Lock是基於JDK層面實現的。Lock 需要lock 和release ,比synchronized 複雜,但Lock 可以做更細粒度的鎖,支援獲取超時、獲取中斷,這是synchronized 所不具備的。Lock的實現主要有ReentrantLock 、ReadLock 和WriteLock ,讀讀共享,寫寫互斥,讀寫互斥。
-
Lock是一個介面,而synchronized是Java中的關鍵字,synchronized是內建的語言實現;
-
synchronized在發生異常時,會自動釋放執行緒佔有的鎖,因此不會導致死鎖現象發生;而Lock在發生異常時,如果沒有主動通過unLock()去釋放鎖,則很可能造成死鎖現象,因此使用Lock時需要在finally塊中釋放鎖;
-
Lock可以讓等待鎖的執行緒響應中斷,而synchronized卻不行,使用synchronized時,等待的執行緒會一直等待下去,不能夠響應中斷;
-
通過Lock可以知道有沒有成功獲取鎖,而synchronized卻無法辦到。
-
Lock可以提高多個執行緒進行讀操作的效率。
-
Lock實現和synchronized不一樣,後者是一種悲觀鎖,它膽子很小,它很怕有人和它搶吃的,所以它每次吃東西前都把自己關起來。而Lock底層其實是CAS 樂觀鎖的體現,它無所謂,別人搶了它吃的,它重新去拿吃的就好啦,所以它很樂觀。底層主要靠volatile 和CAS 操作實現的。
synchronized和volatile
-
volatile本質是在告訴jvm當前變數在暫存器(工作記憶體)中的值是不確定的,需要從主存中讀取;
-
synchronized則是鎖定當前變數,只有當前執行緒可以訪問該變數,其他執行緒被阻塞住。
-
volatile僅能使用在變數級別;synchronized則可以使用在變數、方法、和類級別的
-
volatile僅能實現變數的修改可見性,不能保證原子性;而synchronized則可以保證變數的修改可見性和原子性
-
volatile不會造成執行緒的阻塞;synchronized可能會造成執行緒的阻塞。
-
volatile標記的變數不會被編譯器優化;synchronized標記的變數可以被編譯器優化
七、死鎖問題
死鎖有四個必要條件,打破一個即可去除死鎖。
四個必要條件:
一個資源每次只能被一個程式使用。
一個執行緒因請求資源而阻塞時,對已獲得的資源保持不放。
執行緒已獲得的資源,在末使用完之前,不能強行剝奪。
若干執行緒之間形成一種頭尾相接的迴圈等待資源關係。
死鎖的例子
同步巢狀時,兩個執行緒互相鎖住,都不釋放,造成死鎖。
舉例:
建立兩個字串a和b,再建立兩個執行緒A和B,讓每個執行緒都用synchronized鎖住字串(A先鎖a,再去鎖b;B先鎖b,再鎖a),如果A鎖住a,B鎖住b,A就沒辦法鎖住b,B也沒辦法鎖住a,這時就陷入了死鎖。
public class DeadLock {
public static String obj1 = "obj1";
public static String obj2 = "obj2";
public static void main(String[] args){
Thread a = new Thread(new Lock1());
Thread b = new Thread(new Lock2());
a.start();
b.start();
}
}
class Lock1 implements Runnable{
@Override
public void run(){
try{
System.out.println("Lock1 running");
while(true){
synchronized(DeadLock.obj1){
System.out.println("Lock1 lock obj1");
Thread.sleep(3000);
synchronized(DeadLock.obj2){
System.out.println("Lock1 lock obj2");
}
}
}
}catch(Exception e){
e.printStackTrace();
}
}
}
class Lock2 implements Runnable{
@Override
public void run(){
try{
System.out.println("Lock2 running");
while(true){
synchronized(DeadLock.obj2){
System.out.println("Lock2 lock obj2");
Thread.sleep(3000);
synchronized(DeadLock.obj1){
System.out.println("Lock2 lock obj1");
}
}
}
}catch(Exception e){
e.printStackTrace();
}
}
}
複製程式碼
八、鎖的概念
在 java 中鎖的實現主要有兩類:內部鎖 synchronized (物件內建的monitor鎖)和顯示鎖java.util.concurrent.locks.Lock 。
指的是同一執行緒外層函式獲得鎖之後 ,內層遞迴函式仍然有獲取該鎖的程式碼,但不受影響,執行物件中所有同步方法不用再次獲得鎖。synchronized 和Lock 都具備可重入性。
synchronized 就不是可中斷鎖,而Lock是可中斷鎖。
按等待獲取鎖的執行緒的等待時間進行獲取,等待時間長的具有優先獲取鎖權利。synchronized 就是非公平鎖;對於ReentrantLock 和ReentrantReadWriteLock ,它預設情況下是非公平鎖,但是可以設定為公平鎖。
對資源讀取和寫入的時候拆分為2部分處理,讀的時候可以多執行緒一起讀,寫的時候必須同步地寫。ReadWriteLock 就是讀寫鎖,它是一個介面,ReentrantReadWriteLock 實現了這個介面。
讓執行緒去執行一個無意義的迴圈,迴圈結束後再去重新競爭鎖,如果競爭不到繼續迴圈,迴圈過程中執行緒會一直處於running 狀態,但是基於JVM的執行緒排程,會讓出時間片,所以其他執行緒依舊有申請鎖和釋放鎖的機會。自旋鎖省去了阻塞鎖的時間空間(佇列的維護等)開銷,但是長時間自旋就變成了“忙式等待”,忙式等待顯然還不如阻塞鎖。所以自旋的次數一般控制在一個範圍內,例如10,100等,在超出這個範圍後,自旋鎖會升級為阻塞鎖。
是一種悲觀鎖,synchronized 就是一種獨佔鎖,會導致其它所有需要鎖的執行緒掛起,等待持有鎖的執行緒釋放鎖。
每次不加鎖,假設沒有衝突去完成某項操作,如果因為衝突失敗就重試,直到成功為止。
導致其它所有需要鎖的執行緒掛起,等待持有鎖的執行緒釋放鎖。
關於JUC
包含了兩個子包:atomic以及lock,另外在concurrent下的阻塞佇列以及executors,以後再深入學習吧,下面這個圖很是經典:
參考連結
|