JAVA JDK并發(fā)包里面提供了 synchronized關鍵字和Lock接口叽奥,synchronized關鍵字從語言層面為開發(fā)者提供了鎖杈抢,是隱式鎖,鎖的獲取和釋放全部被封裝起來了缘挽,很笨重掖肋,不能提供可中斷的獲取鎖屡穗,也不能實現等待超時模式獲取鎖撒蟀,而且是獨占鎖虽风,如果我們有可中斷的獲取咆瘟、等待超時模式獲取嚼隘、獲取共享鎖的需求,可以自己實現Lock接口來改寫袒餐。Lock鎖是顯示鎖飞蛹,使用需要遵循一些范式;
lock.lock();
try{
}finally{
lock.unlock;
}
這里有個要點灸眼,解鎖操作必須要放在finally代碼塊里面卧檐,加鎖操作不能放在try代碼塊里面;前者是防止出錯之后鎖不能正常釋放焰宣,后者是防止出錯不能正常加鎖霉囚,反而解鎖了;
JDK顯示鎖 提供了 ReentrantLock(可重入鎖)匕积,ReentrantReadWriteLock(讀寫鎖)兩個常用的實現類盈罐;
鎖的可重入:當鎖作用的代碼塊發(fā)生遞歸的時候,已經加鎖的線程還是可以獲取原來的鎖闪唆,不會因為是遞歸就要繼續(xù)等待盅粪,JDK提供的所有鎖都是支持可重入的;
ReentrantReadWriteLock:讀寫鎖悄蕾,內部維護了readLock票顾,writeLock兩個鎖,讀鎖是共享鎖帆调,和其他讀鎖共享库物,和寫鎖互斥;寫鎖是獨占鎖贷帮,和其他線程的寫鎖戚揭、所有讀鎖互斥;當線程拿到讀鎖時撵枢,其他線程可以繼續(xù)獲取讀鎖民晒;當讀鎖存在時精居,寫鎖阻塞,等到所有讀鎖釋放之后才可以拿到寫鎖潜必,而這時其他線程獲取鎖的時候阻塞靴姿,拿不到任何鎖;當寫鎖釋放時磁滚,讀鎖喚醒佛吓,循環(huán)這一操作;讀寫鎖是針對讀多寫少的時候垂攘,可以大大提高服務器響應的時間维雇;因為讀狀態(tài)是共享鎖,只有寫的時候需要等待寫線程完成晒他,然后其他讀線程可以讀到最新狀態(tài)吱型;
什么是AQS?
AQS是一種隊列同步器,是用來構建鎖或者其他同步組件的基礎框架陨仅,它使用一個int成員變量來表示同步狀態(tài)津滞,通過內置的FIFO隊列來完成資源獲取線程的排隊工作。
簡單來說:AQS就是一種實現同步狀態(tài)的基礎框架灼伤,我們通過它來實現鎖或者其他同步組件(CountDownLatch触徐、CyclicBarrier)。
仔細看AQS源碼我們發(fā)現其內部維護了一個雙向鏈表狐赡,Node锌介,在Node里面存了前置后置節(jié)點,各種狀態(tài)----CANCEL(取消)猾警、CONDITION孔祸、EXCLUSIVE(獨占)、PROPAGATE(傳播)发皿,總體上都是對隊列的操作崔慧,如下圖:
在同步器里面維護了一個head(頭部節(jié)點),tail(尾部節(jié)點),每次一個新的線程進來穴墅,都會將當前線程封裝成一個Node節(jié)點加入到尾部節(jié)點惶室,節(jié)點加入同步器的變化如下:
因為不止一個線程要進行這個操作,所以添加到尾部節(jié)點需要使用循環(huán)的CAS操作來實現玄货;當線程釋放鎖之后皇钞,AQS會將當前節(jié)點的后置節(jié)點設置成首節(jié)點,然后嘗試去獲取狀態(tài)松捉,并將前置節(jié)點設置為空夹界,如果后置節(jié)點為取消狀態(tài),則會從隊列的尾部開始循環(huán)隘世,找到一個最近的沒有被取消的節(jié)點喚醒可柿;
在看看Condition的隊列實現
在Condition內部鸠踪,是一個單向鏈表,在同步器里面复斥,維護了首節(jié)點和尾節(jié)點营密,而其他節(jié)點都指向了后面的節(jié)點;再看看Condition同步器和同步隊列之間的關系
可以看到目锭,我們可以維護多個Condition對象掛載到同步隊列里面评汰,所以每次喚醒的時候,我們只需要喚醒一個就可以了痢虹,使用signal()方法被去,當然,如果是有特殊要求世分,還是需要喚醒當前Condition下面的所有節(jié)點signalAll(),不能跨Condition喚醒缀辩。
了解了AQS的流程之后臭埋,我們需要自己來通過AQS來實現一個自己的鎖。
public class SelfLockDemo implements Lock {
private static class Sync extends AbstractQueuedSynchronizer {
//3
@Override
protected boolean isHeldExclusively() {
return getState() > 0;
}
//4
public Condition newCondition() {
return new ConditionObject();
}
//1
@Override
protected boolean tryAcquire(int arg) {
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
} else if (getExclusiveOwnerThread() == Thread.currentThread()) {
setState(getState() + 1);
return true;
}
return false;
}
//2
@Override
protected boolean tryRelease(int arg) {
if (getExclusiveOwnerThread() != Thread.currentThread()) {
throw new IllegalMonitorStateException();
}
if (getState() == 0) {
throw new IllegalMonitorStateException();
}
setState(getState() - 1);
if (getState() == 0) {
setExclusiveOwnerThread(null);
}
return true;
}
}
private Sync sync = new Sync();
//5
@Override
public void lock() {
System.out.println(Thread.currentThread().getName() + " ready get lock");
sync.acquire(1);
System.out.println(Thread.currentThread().getName() + " already got lock");
}
@Override
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
@Override
public boolean tryLock() {
return sync.tryAcquire(1);
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(time));
}
//6
@Override
public void unlock() {
System.out.println(Thread.currentThread().getName() + " ready release lock");
sync.release(1);
System.out.println(Thread.currentThread().getName() + " already released lock");
}
//7
@Override
public Condition newCondition() {
return sync.newCondition();
}
上面的1臀玄、2瓢阴、3、4健无、5荣恐、6、7都是我們要實現的方法累贤,上面是實現了一個獨占鎖的例子叠穆,首先實現Lock接口,然后做一個內部類來實現AQS抽象類臼膏,重寫了tryAcquire(int arg) 硼被、tryRelease(int arg)、isHeldExclusively() 方法渗磅,看AQS源碼嚷硫,發(fā)現其內部實現了一些模板方法,所以我們要實現自己的鎖始鱼,需要重寫這些方法仔掸;tryAcquire()方法是去獲取鎖,嘗試用一次CAS操作改變狀態(tài)医清,如果成功起暮,就將當前線程設置成獨占模式,如果不成功会烙,查看當前線程是不是和獨占模式的線程是同一個(可重入的實現)鞋怀,如果都不是双泪,則會調用AQS內部的方法,將當前線程加入同步隊列的尾部密似,阻塞焙矛,等待鎖釋放之后喚醒;tryRelease 方法是 釋放鎖残腌,里面沒有任何CAS操作村斟,是因為需要先判斷是不是拿鎖的線程,如果是抛猫,就對自己進行操作蟆盹,如果狀態(tài)為0,就釋放鎖闺金;isHeldExclusively 判斷線程是否被占用逾滥,這個實現很簡單,就是看同步狀態(tài)败匹;newCondition 這個就類似JAVA提供的wait()寨昙、notify()的實現,其內部維護的是一個Condition同步隊列掀亩,當調用await()方法時候舔哪,當前線程釋放鎖,然后進入阻塞狀態(tài)槽棍;當被signal()方法喚醒時捉蚤,繼續(xù)去獲取鎖,然后執(zhí)行下一步操作炼七;5缆巧、6、7豌拙、8就很簡單了盅蝗,調用AQS內部的方法來實現的;
下面再寫一個實現共享鎖的實現
public class TrinityLockDemo implements Lock {
private Sync sync = new Sync(3);
private static class Sync extends AbstractQueuedSynchronizer {
Sync(int count) {
if (count <= 0) {
throw new IllegalMonitorStateException();
}
setState(count);
}
@Override
protected int tryAcquireShared(int arg) {
for (;;) {
int current = getState();
int newCount = current - arg;
if (newCount < 0 || compareAndSetState(current, newCount)) {
return newCount;
}
}
}
@Override
protected boolean tryReleaseShared(int arg) {
for (;;) {
int current = getState();
int newCount = current + arg;
if (compareAndSetState(current, newCount)) {
return true;
}
}
}
final Condition newCondition() {
return new ConditionObject();
}
}
@Override
public void lock() {
sync.acquireShared(1);
}
@Override
public void lockInterruptibly() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
@Override
public boolean tryLock() {
return sync.tryAcquireShared(1) >= 0;
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireSharedNanos(1, unit.toNanos(time));
}
@Override
public void unlock() {
sync.releaseShared(1);
}
@Override
public Condition newCondition() {
return sync.newCondition();
}
}
共享鎖的區(qū)別就是實現的方法不同姆蘸,tryAcquireShared墩莫、tryReleaseShared,這個類容許一定數量的線程獲取鎖逞敷。而這些方法內部的實現都是循環(huán)調用CAS來操作的狂秦;和獨占鎖有所不同,是因為AQS源碼里面推捐,對獨占鎖的內部實現里面是循環(huán)調用CAS來操作的裂问;
下面來說下JAVA實現的ReentrantLock(里面包含的公平鎖和非公平鎖)ReentrantReadWriteLock,讀寫鎖的實現;
第一個:在AQS源碼里面堪簿,是沒有公平和非公平概念的痊乾。公平鎖的概念是 先等待的線程先獲取到鎖,所以要實現公平鎖椭更,就需要在tryAcquire方法里面哪审,對每一個線程都需要檢查是否在隊列里面,如果不在虑瀑,需要將后來的線程加入到隊列內部來實現公平湿滓;而對于非公平鎖,先去嘗試獲取一次鎖舌狗,如果獲取到了叽奥,就直接使用,獲取不到痛侍,就進入隊列繼續(xù)進行朝氓;
第二個:讀寫鎖
因為AQS只提供了一個int成員變量來同步狀態(tài),而讀寫鎖有兩個鎖主届,需要兩種狀態(tài)赵哲,所以JDK里面就用int 32位的高16位來代表讀鎖,低16位代表寫鎖岂膳;但是有個問題誓竿,讀鎖可以同時有多個線程進入磅网,而int 的 16位不能代表各個線程進入的次數的谈截,這里就引入了ThreadLocal線程本地變量,每個線程獲得讀鎖之后涧偷,就在ThreadLocal里面維護數據簸喂,這樣保證了讀鎖的正確計算;而對于寫鎖燎潮,因為是獨占的喻鳄,所以每次只有一個線程可以獲取,直接進行位運算就可以了确封。
至于鎖的升降級除呵,寫鎖可以退化為讀鎖,而讀鎖不能升級為寫鎖爪喘。因為寫鎖的操作需要對所有讀鎖可見颜曾,如果讀鎖升級為寫鎖,那么某一數據被更改之后秉剑,是不能及時通知其他線程的泛豪,所以 讀寫鎖只能支持寫鎖降級,不支持讀鎖升級。