目錄
6.ReentrantLock和Synchronized的共同點和不同點
ReentrantLock作爲Java中除了synchronized之外用的最多的鎖。ReentrantLock是利用AQS框架實現的樂觀鎖,在介紹ReentrantLock之前先看一下AQS也就是AbstractQueuedSynchronizer。
1.AQS
AQS是一個同步框架用來控制多線程訪問共享資源。使得多線程有順序的去訪問共享資源。AQS中有兩個很重要的元素
A.volatitle修飾狀態值state,線程通過CAS操作來改變state的值來作爲這個線程是否獲取到鎖的依據。一般都是默認state等於0的時候,這個時候鎖是沒有被其他線程獲取的,通過CAS操作改變了state的值,如果改變成功說明獲取到了鎖,如果改變不成功,說明沒有獲取到鎖
B.同步FIFO隊列,當線程沒有成功獲取鎖的時候,會將這個線程加入到隊列中,然後當線程釋放鎖後,會從隊列中喚醒一個線程重新佔有鎖。這個隊列是一個雙向隊列,但這個隊列不是一個真實聲明的隊列,它是通過一個個的Node組成的,每個Node都是對沒有獲取到鎖的線程的封裝,而且Node是有指向它前面和後面Node的指針,這樣多個Node組合在一起就形成了一個雙向隊列,這個Node是AbstractQueuedSynchronizer裏面的內部類。隊列的結構圖
Node對象的代碼
static final class Node {
//標記表示節點正在共享模式中等待
static final Node SHARED = new Node();
//標記表示節點正在獨佔模式下等待
static final Node EXCLUSIVE = null;
//waitStatus值表示線程已取消
static final int CANCELLED = 1;
//waitStatus值表示後繼者的線程需要被喚醒
static final int SIGNAL = -1;
//waitStatus值表示線程正在等待條件
static final int CONDITION = -2;
//waitStatus值表示下一個acquireShared應無條件傳播
static final int PROPAGATE = -3;
volatile int waitStatus;
// 當前節點的前驅節點
volatile Node prev;
// 當前節點的後續節點
volatile Node next;
// 當前節點指向的線程
volatile Thread thread;
// nextWaiter是“區別當前CLH隊列是 ‘獨佔鎖’隊列 還是 ‘共享鎖’隊列 的標記”
// 若nextWaiter=SHARED,則CLH隊列是“共享鎖”隊列;
// 若nextWaiter=EXCLUSIVE,(即nextWaiter=null),則CLH隊列是“獨佔鎖”隊列。
Node nextWaiter;
/**
* Returns true if node is waiting in shared mode.
*/
final boolean isShared() {
return nextWaiter == SHARED;
}
/**
* Returns previous node, or throws NullPointerException if null.
* Use when predecessor cannot be null. The null check could
* be elided, but is present to help the VM.
*
* @return the predecessor of this node
* 返回前驅節點
*/
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
Node() { // Used to establish initial head or SHARED marker
}
// Used by addWaiter
Node(Thread thread, Node mode) {
this.nextWaiter = mode;
this.thread = thread;
}
// Used by Condition
Node(Thread thread, int waitStatus) {
this.waitStatus = waitStatus;
this.thread = thread;
}
}
用一句話總結AQS框架就是線程通過CAS操作來改變state的值,如果成功說明這個線程拿到鎖了,如果不成功就放入隊列中掛起,等待它的前一個線程來喚醒它重新對state進行CAS操作來獲取鎖
2.ReentrantLock經典題
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @Author wangbiao
* @Date 2019-11-17 16:59
* @Decripition TODO
**/
public class ReetrantLockTest {
private Lock lock = new ReentrantLock();
private Condition conditionA = lock.newCondition();
private Condition conditionB = lock.newCondition();
private Condition conditionC = lock.newCondition();
public void printA(){
try{
lock.lock();
for(int i=0;i<10;i++){
System.out.println(Thread.currentThread().getName()+"打印A");
conditionB.signal();
conditionA.await();
}
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
public void printB(){
try{
try{
lock.lock();
for(int i=0;i<10;i++){
System.out.println(Thread.currentThread().getName()+"打印B");
conditionC.signal();
conditionB.await();
}
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}catch (Exception e){
e.printStackTrace();
}
}
public void printC(){
try{
try{
lock.lock();
for(int i=0;i<10;i++){
System.out.println(Thread.currentThread().getName()+"打印C");
conditionA.signal();
conditionC.await();
}
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}catch (Exception e){
e.printStackTrace();
}
}
public static void main(String[] args) throws Exception{
ReetrantLockTest test = new ReetrantLockTest();
Thread threadA = new Thread(){
public void run(){
test.printA();
}
};
Thread threadB = new Thread(){
public void run(){
test.printB();
}
};
Thread threadC = new Thread(){
public void run(){
test.printC();
}
};
threadA.start();
Thread.sleep(100);
threadB.start();
Thread.sleep(100);
threadC.start();
}
}
最後輸出:
Thread-0打印A
Thread-1打印B
Thread-2打印C
Thread-0打印A
Thread-1打印B
Thread-2打印C
Thread-0打印A
Thread-1打印B
Thread-2打印C
Thread-0打印A
Thread-1打印B
Thread-2打印C
Thread-0打印A
Thread-1打印B
Thread-2打印C
Thread-0打印A
Thread-1打印B
Thread-2打印C
Thread-0打印A
Thread-1打印B
Thread-2打印C
Thread-0打印A
Thread-1打印B
Thread-2打印C
Thread-0打印A
Thread-1打印B
Thread-2打印C
Thread-0打印A
Thread-1打印B
Thread-2打印C
3.ReentrantLock源碼分析
3.1ReentrantLock結構圖
ReentrantLock實現了Lock接口,並且內部有三個內部類Sync,NonFairSyn和FairSync,這裏運用了模板模式,在AbstractQueuedSynchronizer裏面實現了加鎖和釋放鎖的抽象操作,就是大體的操作流程,但是具體實現例如tryAcquire,tryRelease這些方法就要靠具體的實現類來實現了,我覺得還用到了適配器模式,通過實現Lock接口,但在實現方法裏面引用了Sync的具體方法。
3.2ReentrantLock的構造方法
可以發現ReentrantLock默認的是非公平鎖,如果在申明ReentrantLock的時候,向構造器裏面傳入一個true,那ReentrantLock就是一個公平鎖
public class ReentrantLock implements Lock, java.io.Serializable {
private static final long serialVersionUID = 7373984872572414699L;
// 同步器的引用
private final Sync sync;
//非公平鎖的構造函數
public ReentrantLock() {
sync = new NonfairSync();
}
// 公平鎖的構造函數
public ReentrantLock(boolean fair) {
// 三目運算符,如果爲true 則爲公平鎖,反之爲非公平鎖
sync = fair ? new FairSync() : new NonfairSync();
}
}
3.3獲取鎖lock()方法
public void lock() {
sync.lock();
}
可以看到lock方法實際調用的是NonFairSync裏面的lock方法,先直接利用CAS操作嘗試去改變state的值,如果成功,就說明獲取到了鎖,如果沒有成功再調用acquire,公平鎖的lock方法和這裏不太一樣,公平鎖的lock不會直接去嘗試改變state的值,而是直接調用acquire方法。
NonFairSync.lock方法
final void lock() {
//嘗試直接改變state的值,如果成功說明獲取到鎖
if (compareAndSetState(0, 1))
//將鎖的獨佔線程設置爲當前線程
setExclusiveOwnerThread(Thread.currentThread());
else
//如果直接嘗試改值失敗,就進行下面的操作
acquire(1);
}
AbstractQueuedSynchronizer.acuqre方法
/**
* tryAcquire(arg)方法
* 1.嘗試通過CAS操作改變state值來拿到鎖
* addWaiter(AbstractQueuedSynchronizer.Node.EXCLUSIVE)
* 2.操作失敗就將線程封裝成Node並放入隊尾
* acquireQueued(addWaiter(AbstractQueuedSynchronizer.Node.EXCLUSIVE), arg)
* 3。放入到隊尾後判斷上一個節點是否是隊頭,如果是的,就嘗試獲取鎖,獲取失敗就掛起
* 如果不是隊頭就掛起
*/
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(AbstractQueuedSynchronizer.Node.EXCLUSIVE), arg))
//如果沒有獲取到鎖,但是有中斷操作,就自己中斷
selfInterrupt();
}
到回到NonFairSync的tryAcquire方法,先判斷state是否等於0,等於0說明當前鎖沒有被佔用,利用CAS操作更改state的值,要是更改成功說明獲取鎖成功,如果state不等於0,就判斷佔有鎖的線程是不是當前線程,要是是的,就對state進行加一操作。
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
/**
* 先嚐試利用CAS操作更改state的值,如果更改成功,說明拿到鎖了,
* 如果不成功,說明這個鎖已經有線程佔有了,那就判斷佔有這個鎖的線程是否是當前線程
* 如果是的,state值進行加一操作,這裏也體現了ReentrantLock是可重入鎖
* @param acquires
* @return
*/
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
//獲取當前鎖的狀態值,如果是等於0,說明沒有線程佔有這個鎖
int c = getState();
if (c == 0) {
//通過cas改變狀態值,如果更改成功,說明取得了鎖
if (compareAndSetState(0, acquires)) {
//將擁有鎖的線程替換成當前線程
setExclusiveOwnerThread(current);
return true;
}
}
//如果擁有鎖的線程是當前線程,則將狀態值加一
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
AbstractQueuedSynchronized的addWaiter方法,這個方法就是將當前線程封裝成Node,並快速添加到隊尾,如果快速添加失敗,就通過for的無限循環插入到隊尾
private Node addWaiter(Node mode) {
//先將當前線程封裝成Node,由於mode是null,所以是獨佔模式
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
// 獲取隊尾,通過cas快速替換當前tail節點爲node節點
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//如果快速插入失敗,則利用無限for循環進行插入操作
enq(node);
return node;
}
/**
* 一種常見的CAS操作,就是無限for循環,直到CAS操作成功才跳出for循環
* @param node
* @return
*/
private Node enq(final Node node) {
for (;;) {
Node t = tail;
//如果隊尾是空的,就先初始化一個節點,並設置爲頭節點,隊尾也指向頭節點
//然後執行完後再循環一次,隊尾就不會爲空了
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
//將node設置爲隊尾
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
成功將Node插入到隊尾後,判斷這個隊尾的前一個Node是不是隊頭,如果是隊頭的話嘗試獲取鎖,如果它前面的節點不是隊頭的話就掛起,等待被喚醒後再次執行判斷它前面的Node是不是隊頭,如果是隊頭就嘗試獲取鎖,如果獲取成功,就將這個節點設置成頭節點。
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
// 死循環,直到滿足條件退出
// 首先將當前線程進行阻塞,等待其他線程進行喚醒
// 其他線程進行喚醒以後,判斷當前是否獲取資源。此時,可能有其他線程的加入,導致獲取失敗
for (;;) {
// 獲取當前節點的前驅節點
final Node p = node.predecessor();
// 如果當前節點的前驅節點是head節點,且嘗試獲取鎖成功
if (p == head && tryAcquire(arg)) {
// 設置當前的node節點爲head節點
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
// 判斷當前線程是否應該阻塞
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
// 如果當前線程被中斷,則 parkAndCheckInterrupt()返回爲true。interrupted = true
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
/**
* 如果Node的前節點的waitStatus是等於1的,那麼就直接可以確認將Node掛起的
* 如果waitStatus是大於0的,則代表線程被取消,重新設置Node的前驅節點
* 找到第一個ws<=0的節點,將這個節點設置爲Node的前驅節點
* 默認的Node的ws是null的,所以會將ws設置爲SIGNAL,也就是-1
*
* @param pred
* @param node
* @return
*/
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true;
if (ws > 0) {
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* waitStatus must be 0 or PROPAGATE. Indicate that we
* need a signal, but don't park yet. Caller will need to
* retry to make sure it cannot acquire before parking.
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
將當前線程掛起,掛起後如果被喚醒,就判斷當前線程是否被中斷了
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
總結一下ReentrantLock非公平鎖的獲取:
1.嘗試通過cas操作來改變state的值,如果能改變則獲取到鎖。
2.如果沒能改變state的值,則判斷state的值是否爲0,如果爲0則嘗試用cas操作來改變state值來拿鎖。
3.如果state的值不爲0,則判斷拿到鎖的線程是否是自己,如果是的,則state加一操作,如果獲取鎖的線程不是自己,則將自己封裝成一個node節點,放入到隊尾。
4.放入隊尾後,判斷自己是否是頭節點,如果是頭節點,則嘗試改變state的值獲取鎖,如果不是頭節點,則掛起來,等待前面的節點來喚醒。
3.4 釋放鎖
調用的AbstractQueuedSynchronizer的release方法,
public void unlock() {
sync.release(1);
}
/**
* 如果釋放資源成功,並且頭節點不爲空,並且頭節點的狀態值不等於0,
* 就喚醒下一個Node節點來獲取鎖
* @param arg
* @return
*/
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
tryRelease方法,鎖的狀態值進行減一操作,如果狀態值等於0,就說明這個鎖被完全釋放
/**
* 先對鎖的狀態值進行減一操作,如果當前線程不是獲取到鎖的線程,就拋出IllegaMonitorStateException異常
* 如果狀態值在減一後變成0,那麼說明這個鎖被完全釋放了,將佔有鎖的線程設置成空
* @param releases
* @return
*/
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;
}
釋放完鎖後就需要喚醒下一個節點來獲取鎖,AbstractQueuedSynchronizer的unparkSuccessor
private void unparkSuccessor(AbstractQueuedSynchronizer.Node node) {
//把狀態值設爲0
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
/**
* 獲取Node的後驅元素,如果後驅節點是空,或者狀態是取消,
* 要找到離隊頭最近的狀態是小於等於0的節點,然後就將這個節點喚醒
*/
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);
}
4.總結ReentrantLock的基本流程:
這裏是對於非公平鎖而言的流程
加鎖的過程
A.直接利用CAS更改同步狀態值,如果成功,就將鎖的獨佔線程設置成當前線程,說明獲取到鎖
B.更改狀態值失敗,在tryAcquire方法裏面獲取狀態值,如果狀態值等於0,就利用CAS操作更改狀態值,成功說明獲取到鎖
如果狀態值不等於0,判斷擁有鎖的線程是不是當前線程,如果是的,就對狀態值加一操作,拿到鎖
C.B過程中沒有拿到鎖,就將當前線程封裝成Node節點,並快速插入到隊尾,如果快速插入失敗的話,要是隊頭爲null的話,先初始化一個Node節點作爲隊尾,再然後將Node節點作爲隊尾插入。
D.插入隊尾完成後,利用無限for循環,先判斷當前Node節點的前驅節點是否是隊頭,如果是的,嘗試獲取鎖,如果獲取成功就直接返回,如果失敗或者前驅節點不是隊頭,判斷自己的前驅節點的狀態是否是SIGNAL,SIGNAL代表可以喚醒後驅節點,如果是的直接返回true,如果不是的,就找到最近的一個狀態是SIGNAL的節點作爲前驅節點返回true,再將自己掛起,等待喚醒。
E.掛起後,如果被喚醒,判斷自己是否被中斷了,如果被中斷就自己嘗試中斷自己。
解鎖的過程
A.更改狀態值也就是減一操作,判斷當前線程是否是持有鎖的線程,如果是的,則判斷釋放後的狀態值是否爲0,如果是的說明鎖被完全釋放,返回true,如果鎖沒有被完全釋放,返回false
B.如果鎖被完全釋放,就喚醒隊頭的後驅節點,喚醒的前提是後驅節點的waitstatus是小於等於0的,如果不是的,找到離隊頭最近的waitstatus是小於等於0的節點進行喚醒,喚醒調用LockSupport.unpark(s.thread);
5.ReentrantLock公平鎖
ReentrantLock默認的是非公平鎖,當申明ReentrantLock實例的時候向構造函數裏面傳參數true的時候,就是公平鎖了,非公平鎖和公平鎖最多的區別是在都調用lock方法的時候:
非公平鎖調用lock方法
首先是直接去更改鎖的狀態值來嘗試獲取鎖,如果獲取失敗了,再進行下面的操作
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
公平鎖加鎖的操作:
final void lock() {
acquire(1);
}
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
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;
}
}
在FairSync重寫tryAcquire方法的時候,去獲取鎖的前面加了一個hasQueuedPredecessors的判斷,這個判斷是判斷隊列中有沒有比自己排在更前面的節點,也就是頭節點的下一個節點是否存在。
public final boolean hasQueuedPredecessors() {
// The correctness of this depends on head being initialized
// before tail and on head.next being accurate if the current
// thread is first in queue.
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
總結一下公平鎖和非公平鎖在獲取鎖的過程中有什麼不同:
非公平鎖加鎖的時候是直接先去利用CAS操作更改鎖的狀態值來判斷有沒有拿到鎖,如果沒有拿到,再去判斷鎖的狀態值是否等於0,如果等於0再去嘗試獲取鎖。
公平鎖加鎖的時候會先去獲取鎖的狀態值是否等於0,如果等於0,還要去判斷當前隊列中是否有排在自己前面的節點,如果沒有才去獲取鎖。這也是在新東方面試的時候沒有回答上來的問題。
6.ReentrantLock和Synchronized的共同點和不同點
共同點:
兩者都是可重入的獨佔鎖,ReentrantLock在默認的情況下是非公平鎖,Synchronized也是非公平鎖
不同點:
1.Synchronized是在字節碼指令進行加鎖操作,ReentrantLock實在代碼層面進行加鎖操作
2.Synchronized鎖的釋放在執行完同步代碼塊或者同步方法的時候就會自動釋放,ReentrantLock加完鎖後得調用unlock方法手動釋放,所以一般將unlock方法在finally裏面來釋放的
3.線程在獲取Synchronized的同步鎖的時候如果被阻塞了,會一直阻塞,不能被中斷,不能幹別的,而ReentrantLock在加鎖的時候選擇tryLock加鎖,可以在沒有獲取到鎖後直接返回,不用阻塞,或者lockInterruptibly方法加鎖的話,可以在阻塞的過程中被中斷
4.如果在加鎖的代碼塊中運行發生異常,Synchronized會釋放鎖,ReentrantLock不會釋放鎖,所以一般將ReentrantLock鎖的釋放放在finally代碼塊裏面。
5.ReentrantLock結合condition使用會比Synchronized和wait,notify結合使用的話更加靈活,ReentrantLock可以結合condition來喚醒和鎖定特定的線程。