前言:
最近看到有人說可以使用 CAS + volatile 實現同步代碼塊。
心想,確實是可以實現的呀!因爲 AbstractQueuedSynchronizer(簡稱 AQS)內部就是通過 CAS + volatile(修飾同步標誌位state) 實現的同步代碼塊。
並且ReentrantLock就是基於AQS原理來實現同步代碼塊的;ReentrantLock源碼學習和了解AQS原理可以參考:帶你探索ReentrantLock源碼的快樂
今天,咱們就通過 CAS + volatile 實現一個 迷你版的AQS ;通過這個迷你版的AQS可以使大家對AQS原理更加清晰。
本文****主線****:
- CAS操作和volatile簡述* CAS + volatile = 同步代碼塊(代碼實現)
CAS操作和volatile簡述:
通過了解CAS操作和volatile來聊聊爲什麼使用它們實現同步代碼塊。
CAS操作:
CAS是什麼?
CAS是compare and swap的縮寫,從字面上理解就是比較並更新;主要是通過 處理器的指令 來保證操作的原子性 。
CAS 操作包含三個操作數:
- 內存位置(V)* 預期原值(A)* 更新值(B)
簡單來說:從內存位置V上取到存儲的值,將值和預期值A進行比較,如果值和預期值A的結果相等,那麼我們就把新值B更新到內存位置V上,如果不相等,那麼就重複上述操作直到成功爲止。
例如:JDK中的 unsafe 類中的 compareAndSwapInt 方法:
unsafe.compareAndSwapInt(this, stateOffset, expect, update);
- stateOffset 變量值在內存中存放的位置;* expect 期望值;* update 更新值;
CAS的優點:
CAS是一種無鎖化編程,是一種非阻塞的輕量級的樂觀鎖;相比於synchronized阻塞式的重量級的悲觀鎖來說,性能會好很多 。
但是注意:synchronized關鍵字在不斷地優化下(鎖升級優化等),性能也變得十分的好。
volatile 關鍵字:
volatile是什麼?
volatile是java虛擬機提供的一種輕量級同步機制。
volatile的作用:
- 可以保證被volatile修飾的變量的讀寫具有原子性,不保證複合操作(i++操作等)的原子性;* 禁止指令重排序;* 被volatile修飾的的變量修改後,可以馬上被其它線程感知到,保證可見性;
通過了解CAS操作和volatile關鍵字後,纔可以更加清晰地理解下面實現的同步代碼的demo程序。
CAS + volatile = 同步代碼塊
總述同步代碼塊的實現原理:
- 使用 volatile 關鍵字修飾一個int類型的同步標誌位state,初始值爲0;
- 加鎖/釋放鎖時使用CAS操作對同步標誌位state進行更新; 加鎖成功,同步標誌位值爲 1,加鎖狀態; 釋放鎖成功,同步標誌位值爲0,初始狀態;
加鎖實現:
加鎖流程圖:
加鎖代碼:
**
* 加鎖,非公平方式獲取鎖
*/
public final void lock() {
while (true) {
// CAS操作更新同步標誌位
if (compareAndSetState(0, 1)) {
// 將獨佔鎖的擁有者設置爲當前線程
exclusiveOwnerThread = Thread.currentThread();
System.out.println(Thread.currentThread() + " lock success ! set lock owner is current thread . " +
"state:" + state);
try {
// 睡眠一小會,模擬更加好的效果
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 跳出循環
break;
} else {
// TODO 如果同步標誌位是1,並且鎖的擁有者是當前線程的話,則可以設置重入,但本方法暫未實現
if (1 == state && Thread.currentThread() == exclusiveOwnerThread) {
// 進行設置重入鎖
}
System.out.println(Thread.currentThread() + " lock fail ! If the owner of the lock is the current thread," +
" the reentrant lock needs to be set;else Adds the current thread to the blocking queue .");
// 將線程阻塞,並將其放入阻塞列表
parkThreadList.add(Thread.currentThread());
LockSupport.park(this);
// 線程被喚醒後會執行此處,並且繼續執行此 while 循環
System.out.println(Thread.currentThread() + " The currently blocking thread is awakened !");
}
}
}
鎖釋放實現:
釋放鎖流程圖:
釋放鎖代碼:
/**
* 釋放鎖
*
* @return
*/
public final boolean unlock() {
// 判斷鎖的擁有者是否爲當前線程
if (Thread.currentThread() != exclusiveOwnerThread) {
throw new IllegalMonitorStateException("Lock release failed ! The owner of the lock is not " +
"the current thread.");
}
// 將同步標誌位設置爲0,初始未加鎖狀態
state = 0;
// 將獨佔鎖的擁有者設置爲 null
exclusiveOwnerThread = null;
System.out.println(Thread.currentThread() + " Release the lock successfully, and then wake up " +
"the thread node in the blocking queue ! state:" + state);
if (parkThreadList.size() > 0) {
// 從阻塞列表中獲取阻塞的線程
Thread thread = parkThreadList.get(0);
// 喚醒阻塞的線程
LockSupport.unpark(thread);
// 將喚醒的線程從阻塞列表中移除
parkThreadList.remove(0);
}
return true;
}
完整代碼如下:
import sun.misc.Unsafe;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.LockSupport;
/**
* @PACKAGE_NAME: com.lyl.thread6
* @ClassName: AqsUtil
* @Description: 使用 CAS + volatile 同步標誌位 = 實現 迷你版AQS ;
* <p>
* <p>
* 注意:本類只簡單實現了基本的非公平方式的獨佔鎖的獲取與釋放; 像重入鎖、公平方式獲取鎖、共享鎖等都暫未實現
* <p/>
* @Date: 2021-01-15 10:55
* @Author: [ 木子雷 ] 公衆號
**/
public class AqsUtil {
/**
* 同步標誌位
*/
private volatile int state = 0;
/**
* 獨佔鎖擁有者
*/
private transient Thread exclusiveOwnerThread;
/**
* JDK中的rt.jar中的Unsafe類提供了硬件級別的原子性操作
*/
private static final Unsafe unsafe;
/**
* 存放阻塞線程的列表
*/
private static List<Thread> parkThreadList = new ArrayList<>();
/**
* 同步標誌位 的“起始地址”偏移量
*/
private static final long stateOffset;
static {
try {
unsafe = getUnsafe();
// 獲取 同步標誌位status 的“起始地址”偏移量
stateOffset = unsafe.objectFieldOffset(AqsUtil.class.getDeclaredField("state"));
} catch (NoSuchFieldException e) {
throw new Error(e);
}
}
/**
* 通過反射 獲取 Unsafe 對象
*
* @return
*/
private static Unsafe getUnsafe() {
try {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
return (Unsafe) field.get(null);
} catch (Exception e) {
return null;
}
}
/**
* 加鎖,非公平方式獲取鎖
*/
public final void lock() {
while (true) {
if (compareAndSetState(0, 1)) {
// 將獨佔鎖的擁有者設置爲當前線程
exclusiveOwnerThread = Thread.currentThread();
System.out.println(Thread.currentThread() + " lock success ! set lock owner is current thread . " +
"state:" + state);
try {
// 睡眠一小會,模擬更加好的效果
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 跳出循環
break;
} else {
// TODO 如果同步標誌位是1,並且鎖的擁有者是當前線程的話,則可以設置重入,但本方法暫未實現
if (1 == state && Thread.currentThread() == exclusiveOwnerThread) {
// 進行設置重入鎖
}
System.out.println(Thread.currentThread() + " lock fail ! If the owner of the lock is the current thread," +
" the reentrant lock needs to be set;else Adds the current thread to the blocking queue .");
// 將線程阻塞,並將其放入阻塞隊列
parkThreadList.add(Thread.currentThread());
LockSupport.park(this);
// 線程被喚醒後會執行此處,並且繼續執行此 while 循環
System.out.println(Thread.currentThread() + " The currently blocking thread is awakened !");
}
}
}
/**
* 釋放鎖
*
* @return
*/
public final boolean unlock() {
if (Thread.currentThread() != exclusiveOwnerThread) {
throw new IllegalMonitorStateException("Lock release failed ! The owner of the lock is not " +
"the current thread.");
}
// 將同步標誌位設置爲0,初始未加鎖狀態
state = 0;
// 將獨佔鎖的擁有者設置爲 null
exclusiveOwnerThread = null;
System.out.println(Thread.currentThread() + " Release the lock successfully, and then wake up " +
"the thread node in the blocking queue ! state:" + state);
if (parkThreadList.size() > 0) {
// 從阻塞列表中獲取阻塞的線程
Thread thread = parkThreadList.get(0);
// 喚醒阻塞的線程
LockSupport.unpark(thread);
// 將喚醒的線程從阻塞列表中移除
parkThreadList.remove(0);
}
return true;
}
/**
* 使用CAS 安全的更新 同步標誌位
*
* @param expect
* @param update
* @return
*/
public final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
}
測試運行:
測試代碼:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* @PACKAGE_NAME: com.lyl.thread6
* @ClassName: SynCodeBlock
* @Description: 簡單的測試
* @Date: 2021-01-15 10:26
* @Author: [ 木子雷 ] 公衆號
**/
public class SynCodeBlock {
public static void main(String[] args) {
// 10 個線程的固定線程池
ExecutorService logWorkerThreadPool = Executors.newFixedThreadPool(10);
AqsUtil aqsUtil = new AqsUtil();
int i = 10;
while (i > 0) {
logWorkerThreadPool.execute(new Runnable() {
@Override
public void run() {
test(aqsUtil);
}
});
--i;
}
}
public static void test(AqsUtil aqsUtil) {
// 加鎖
aqsUtil.lock();
try {
System.out.println("正常的業務處理");
} finally {
// 釋放鎖
aqsUtil.unlock();
}
}
}
運行結果:
例如上面測試程序啓動了10個線程同時執行同步代碼塊,可能此時只有線程 thread-2 獲取到了鎖,其餘線程由於沒有獲取到鎖被阻塞進入到了阻塞列表中;
當獲取鎖的線程釋放了鎖後,會喚醒阻塞列表中的線程,並且是按照進入列表的順序被喚醒;此時被喚醒的線程會再次去嘗試獲取鎖,如果此時有新線程同時嘗試獲取鎖,那麼此時也存在競爭了,這就是非公平方式搶佔鎖(不會按照申請鎖的順序獲取鎖)。
擴展:
上面的代碼中沒有實現線程自旋操作,下面看看該怎麼實現呢?
首先說說爲什麼需要自旋操作:
因爲在某些場景下,同步資源的鎖定時間很短,如果沒有獲取到鎖的線程,爲了這點時間就進行阻塞的話,就有些得不償失了;因爲進入阻塞時會進行線程上下文的切換,這個消耗是很大的;
使線程進行自旋的話就很大可能會避免阻塞時的線程上下文切換的消耗;並且一般情況下都會設置一個線程自旋的次數,超過這個次數後,線程還未獲取到鎖的話,也要將其阻塞了,防止線程一直自旋下去白白浪費CPU資源。
代碼如下: