一、前言
AQS是AbstractQueuedSynchronizer類的簡稱,是JAVA中用來構建同步鎖的基礎框架。他內部通過使用了一個int類型的 state成員變量來表示同步鎖的狀態,通過內置的FIFO隊列來完成資源獲取線程的排隊工作。
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
private volatile int state;
二、AQS使用的設計模式
AQS是一個抽象類,使用方式通過繼承AQS類,並實現其相關的抽象方法。在AQS中通過state成員變量來表示同步鎖的狀態,因此在使用AQS構建同步鎖的時候就需要實現其getState、setState、compareAndSetState三個方法,來進行操作。
protected final int getState() {
return state;
}
protected final void setState(int newState) {
state = newState;
}
protected final boolean compareAndSetState(int expect, int update) {
return U.compareAndSetInt(this, STATE, expect, update);
}
在實現上,我們的通過繼承AbstractQueuedSynchronizer類派生的子類,一般作爲同步鎖組件的靜態內部類,AQS本身沒有實現任何同步鎖的相關接口,僅僅是定義了若干同步狀態獲取和釋放的抽象方法。自定義同步鎖組件可以通過獨佔式或者共享式的獲取同步狀態實現相應不同類型的鎖。如ReentrantLock,ReentrantReadWriteLock等。
在AQS中使用了模板的設計模式,AQS定義了同步鎖組件實現的一系列模板骨架,而具體的實現延遲到子類中。模板方法使得子類不能更改一個算法的結構即可重定義該算法的某些特定步驟。
1、AQS模板設計模式舉例
(1)自定義一個做蛋糕的模板
package cn.enjoyedu.concurrent.cake;
/**
* 蛋糕類中定義了
* 做蛋糕的三個步驟
*/
public abstract class Cake {
abstract void first();
abstract void two();
abstract void third();
public void run() {
first();
two();
third();
}
}
(2)水果和蛋糕各自的實現
package cn.enjoyedu.concurrent.cake;
/**
* 水果蛋糕的具體實現
*/
public class FruitCake extends Cake{
@Override
void first() {
System.out.println("FruitCake first");
}
@Override
void two() {
System.out.println("FruitCake two");
}
@Override
void third() {
System.out.println("FruitCake third");
}
}
package cn.enjoyedu.concurrent.cake;
/**
* 巧克力蛋糕的具體實現
*/
public class ChocolateCake extends Cake {
@Override
void first() {
System.out.println("ChocolateCake first");
}
@Override
void two() {
System.out.println("ChocolateCake two");
}
@Override
void third() {
System.out.println("ChocolateCake third");
}
}
(3)測試
package cn.enjoyedu.concurrent.cake;
public class CakeTest {
public static void main(String[] args) {
Cake fruitCake = new FruitCake();
fruitCake.run();
Cake chocolateCake = new ChocolateCake();
chocolateCake.run();
}
}
FruitCake first
FruitCake two
FruitCake third
ChocolateCake first
ChocolateCake two
ChocolateCake third
三、AQS中的一些模板方法
在實現同步鎖組件的時候我們將通過實現AQS中的模板方法。
-
AQS中的模板方法大概有如下:
-
可重寫的模板方法:
訪問或者修改同步狀態的方法
在我們自定義同步鎖組件的時候,需要提供如下3個方法來訪問或者修改同步狀態。
(1)getState():獲取當前同步狀態。
(2)setState(int newState):設置當前同步狀態。
(3)compareAndSetState(int expect,int update):使用CAS設置當前狀態,該方法能夠保證狀態設置的原子性。
四、自定義一個同步鎖組件
我們通過實現AQS中的方法,來構建一個簡單的拿鎖和釋放鎖的同步器,並測試
(1)同步器作爲同步鎖組件的內部類,繼承AbstractQueuedSynchronizer ,實現幾個核心方法isHeldExclusively、tryAcquire、tryRelease
package cn.enjoyedu.concurrent.asq;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
public class MyLock implements Lock {
/**
* 自定義同步器
*/
private static class Sys extends AbstractQueuedSynchronizer {
/**
* 判斷是否處於佔用狀態
*
* @return
*/
@Override
protected boolean isHeldExclusively() {
return getState() == 1;
}
/**
* 嘗試拿鎖
*
* @param arg
* @return
*/
@Override
protected boolean tryAcquire(int arg) {
//原子操作
if (compareAndSetState(0, 1)) {
//設置擁有鎖的線程
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
/**
* 嘗試釋放鎖
*
* @param arg
* @return
*/
@Override
protected boolean tryRelease(int arg) {
if (getState() == 0) {
throw new IllegalMonitorStateException();
}
//設置當前線程爲空
setExclusiveOwnerThread(null);
//修改同步狀態
setState(0);
return true;
}
}
private Sys sys = new Sys();
@Override
public void lock() {
System.out.println(Thread.currentThread().getName()+"正在拿鎖");
sys.acquire(1);
System.out.println(Thread.currentThread().getName()+"已經拿鎖");
}
@Override
public void lockInterruptibly() throws InterruptedException {
}
@Override
public boolean tryLock() {
return sys.tryAcquire(1);
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return false;
}
@Override
public void unlock() {
System.out.println(Thread.currentThread().getName()+"正在釋放鎖");
sys.release(0);
System.out.println(Thread.currentThread().getName()+"已經釋放鎖");
}
@Override
public Condition newCondition() {
return null;
}
}
(2)測試
package cn.enjoyedu.concurrent.cas;
import cn.enjoyedu.concurrent.asq.MyLock;
public class LockTest {
public static void main(String[] args) {
Test test = new Test();
for (int i = 0; i <5 ; i++) {
new Thread(){
@Override
public void run() {
test.test();
}
}.start();
}
}
static class Test {
MyLock lock = new MyLock();
public void test() {
lock.lock();
System.out.println(Thread.currentThread().getName()+" hello wold ");
lock.unlock();
}
}
}
Thread-0正在拿鎖
Thread-3正在拿鎖
Thread-2正在拿鎖
Thread-1正在拿鎖
Thread-0已經拿鎖
Thread-4正在拿鎖
Thread-0 hello wold
Thread-0正在釋放鎖
Thread-0已經釋放鎖
Thread-3已經拿鎖
Thread-3 hello wold
Thread-3正在釋放鎖
Thread-3已經釋放鎖
Thread-2已經拿鎖
Thread-2 hello wold
Thread-2正在釋放鎖
Thread-2已經釋放鎖
Thread-4已經拿鎖
Thread-4 hello wold
Thread-4正在釋放鎖
Thread-4已經釋放鎖
Thread-1已經拿鎖
Thread-1 hello wold
Thread-1正在釋放鎖
Thread-1已經釋放鎖
測試發現我們成功的自定義了一個簡單的同步鎖,打印的時候,只有拿到鎖的線程,纔會打印hello wold 。
可重入鎖的概念
當我們在鎖的方法中在去執行加鎖的方法,那麼就會再次拿鎖,在第一次拿鎖的時候,我們將state狀態從0改成1,第二次拿鎖的時候我們將狀態從改成++1。當我們要釋放鎖的時候那麼也要釋放兩次-1,使得state變爲0。其次我們在修改state的狀態的時候是一個cas原子操作,當一個線程將狀態從0改成1的時候,說明拿到鎖,其他線程再從0改成1就會失敗。
五、CLH隊列鎖
我們說AQS是基於CLH隊列鎖的一種變體的實現,我們來了解一下CLH隊列鎖的原理。
CLH隊列鎖是基於鏈表的一個數據結構,每一個申請拿鎖的線程都在本地變量中自旋。通過不斷輪詢前驅節點鎖的狀態,一旦發現前驅節點的鎖的狀態爲false,說明前驅節點釋放了鎖。自旋結束。
(1)當一個線程需要拿鎖的時候,創建一個節點,其中包含了鎖的狀態locked設置爲true,myPred表示對前驅節點的引用
(2)第二個線程需要拿鎖的時候,對tail域調用getAndSet方法,使自己成爲了隊列的尾部,其中同樣包含了locked狀態和指向前一個節點的myPred,第三個線程需要拿到鎖的時候,同樣如此
就這樣每個線程就在前驅節點的locked字段上自旋
(3)一旦一個線程需要釋放鎖的時候,將節點的locked的值設置爲false,同時回收前驅節點。
如圖節點A的myPred指向的 前驅節點的locked狀態設置爲了false,說明它釋放了鎖,A就可以拿到鎖。
六、ReentrantLock的實現 (可重入鎖)
ReentrantLock是基於AQS的實現。
1、鎖的可重入
在以上我們也提到,當一個線程拿到鎖之後,再去執行鎖的方法,將會再次拿到鎖。在AQS中state變量在第一次拿到鎖的時候從0變爲1,第二次拿鎖的時候從1變爲2。當我們要釋放鎖的時候,也要釋放兩次,從2變爲1,從1在變爲0。ReentrantLock就是如此。
2、公平鎖和非公平鎖
ReentrantLock可以實現公平鎖和非公平鎖,ReentrantLock鎖的包含了兩個同步器NonfairSync、FairSync。從字面意思就知道是非公平鎖和公平鎖的實現。在NonfairSync非公平同步器實現中,tryAcquire方法裏只要CAS設置同步狀態成功,表示拿到鎖。而FairSync公平同步器中,tryAcquire中多了一個判斷,hasQueuedPredecessors()方法,該方法表示加入同步隊列中的當前節點,也就是當前線程,是否有前驅節點。如果有前驅節點,說明前一個節點也在請求拿鎖。當前節點只能排隊。也就是之前我們說的CLH的思想。因此需要等待前驅節點釋放鎖,自己才能獲取鎖。
3、ReentrantLock小結
ReentrantLock是一種可重入鎖,可以實現公平鎖和非公平鎖,ReentrantLock是基於AQS和CAS來實現的鎖,AQS自定義了實現鎖的基本骨架,CAS用於對鎖狀態state的原子操作。而公平鎖的實現又是基於CLH隊列鎖的原理,每一個線程都是鏈表中的一個節點,每個節點的包含了myPred和locked。myPred指向了前驅節點,locked表示當前線程拿鎖的狀態。在拿鎖的過程,就是每個節點自旋的過程,通過不斷輪詢前驅節點的locked屬性,來判斷前一個節點是否釋放鎖,一旦釋放鎖,自己就可以獲取拿鎖。