不能錯過的CAS+volatile實現同步代碼塊

前言:

最近看到有人說可使用 CAS + volatile 實現同步代碼塊。

心想,確實是能夠實現的呀!由於 AbstractQueuedSynchronizer(簡稱 AQS)內部就是經過 CAS + volatile(修飾同步標誌位state) 實現的同步代碼塊。java

而且ReentrantLock就是基於AQS原理來實現同步代碼塊的;ReentrantLock源碼學習和了解AQS原理能夠參考:帶你探索ReentrantLock源碼的快樂 node

今天,我們就經過 CAS + volatile 實現一個 迷你版的AQS ;經過這個迷你版的AQS可使你們對AQS原理更加清晰。編程

本文主線:

  • CAS操做和volatile簡述
  • CAS + volatile = 同步代碼塊(代碼實現)

CAS操做和volatile簡述:

經過了解CAS操做和volatile來聊聊爲何使用它們實現同步代碼塊。安全

CAS操做:

CAS是什麼?

CAS是compare and swap的縮寫,從字面上理解就是比較並更新;主要是經過 處理器的指令 來保證操做的原子性 。ide

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關鍵字在不斷的優化下(鎖升級優化等),性能也變得十分的好。this

volatile 關鍵字:

volatile是什麼?

volatile是java虛擬機提供的一種輕量級同步機制。

volatile的做用:

  • 能夠保證被volatile修飾的變量的讀寫具備原子性,不保證複合操做(i++操做等)的原子性;
  • 禁止指令重排序;
  • 被volatile修飾的的變量修改後,能夠立刻被其它線程感知到,保證可見性;
經過了解CAS操做和volatile關鍵字後,才能夠更加清晰的理解下面實現的同步代碼的demo程序。

CAS + volatile = 同步代碼塊

總述同步代碼塊的實現原理:

  1. 使用 volatile 關鍵字修飾一個int類型的同步標誌位state,初始值爲0;
  2. 加鎖/釋放鎖時使用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資源。

代碼以下:

❤ 點贊 + 評論 + 轉發 喲

若是本文對您有幫助的話,請揮動下您愛發財的小手點下贊呀,您的支持就是我不斷創做的動力,謝謝啦!

您能夠VX搜索 【木子雷】 公衆號,大量Java學習乾貨文章,您能夠來瞧一瞧喲!

相關文章
相關標籤/搜索