併發編程之AQS的原理

一.AQS原理

AQS全稱爲AbstractQueuedSynchronizer,它提供了一個FIFO隊列,可以看成是一個用來實現鎖以及其它涉及到同步功能的核心組件,常見的有:ReenTrantLock、CountDownLatch等。


AQS是一個抽象類,主要是通過繼承的方式來使用,它本身沒有實現任何的同步接口,僅僅是定義了同步狀態的獲取以及釋放的方法來提供自定義的同步組件。



AQS的主要字段:

/**
 * 頭節點指針,通過setHead進行修改
 */
private transient volatile Node head;

/**
 * 隊列的尾指針
 */
private transient volatile Node tail;

/**
 * 同步器狀態
 */
private volatile int state;

AQS需要子類實現的方法:

方法名 方法描述
tryAcquire 以獨佔模式嘗試獲取鎖,獨佔模式下調用acquire,嘗試去設置state的值,如果設置成功則返回,如果設置失敗則將當前線程加入到等待隊列,直到其他線程喚醒
tryRelease 嘗試獨佔模式下釋放狀態
tryAcquireShared 嘗試在共享模式獲得鎖,共享模式下調用acquire,嘗試去設置state的值,如果設置成功則返回,如果設置失敗則將當前線程加入到等待隊列,直到其他線程喚醒
tryReleaseShare 嘗試共享模式釋放狀態
isHeldExclusively 是否是獨佔模式,表示是否被當前線程佔用


head節點是隊列初始化的時候一個節點,只表示位置,不代表實際的等待線程。head節點之後的節點就是獲取鎖失敗進入等待隊列的線程。接下來,我們打開AQS源碼,看下Node節點都有哪些關鍵內容:

static final class Node {
        /** 共享模式 */
        static final Node SHARED = new Node();

        /**獨佔模式 */
        static final Node EXCLUSIVE = null;

        /** 節點狀態值,表示節點已經取消 */
        static final int CANCELLED =  1;

        /** 節點狀態值,在當前節點釋放或者取消的時候,會喚醒下一個節點 */
        static final int SIGNAL    = -1;

        /** waitStatus value to indicate thread is waiting on condition */
        static final int CONDITION = -2;

        /**
         * 這個值是在共享鎖的時候會用到,喚醒了一個節點,會嘗試喚醒下一個節點,
         * 如果當前節點未阻塞(阻塞前就獲得了鎖),那麼當前節點的狀態會被設置成-3
         */
        static final int PROPAGATE = -3;

        /**
         * 等待狀態
         */
        volatile int waitStatus;

        /**
         * 前驅節點
         */
        volatile Node prev;

        /**
         * 後繼節點
         */
        volatile Node next;

        /**
         * 等待的線程
         */
        volatile Thread thread;

        /**
         *此處可忽略,主要是模式的判斷
         */
        Node nextWaiter;

    }


通過上面的內容我們可以看到waitStatus其實是有5個狀態的,雖然這裏面0並不是什麼字段,但是他是waitStatus狀態的一種,表示不是任何一種類型的字段,上面也講解了關於AQS中子類實現的方法,AQS提供了獨佔模式和共享模式兩種

二.自定義AQS鎖

自定義鎖

public class MyLock implements Lock {

    private DiyLock diyLock = new DiyLock();
    static class DiyLock extends AbstractQueuedSynchronizer{

        /**
         * Creates a new {@code AbstractQueuedSynchronizer} instance
         * with initial synchronization state of zero.
         */
        protected DiyLock() {
            super();
        }


        //獲取鎖
        @Override
        protected boolean tryAcquire(int arg) {

            //1.獲取狀態
            int state = getState();

            if(state == 0){
                //利用CAS原理修改state
                if(compareAndSetState(0,arg)){
                    //可以修改,則設置當前線程佔有資源
                    setExclusiveOwnerThread(Thread.currentThread());
                    return true;
                }
            }else if(getExclusiveOwnerThread() == Thread.currentThread()){
                //當前線程重入
                setState(getState() + arg);
                return true;
            }
            //獲取失敗
            return false;
        }

        //釋放鎖
        @Override
        protected boolean tryRelease(int arg) {
            int state = getState() -arg;

            //判斷釋放後是否爲0
            if(state == 0){
                //釋放當前佔有線程
                setExclusiveOwnerThread(null);
                setState(state);
                return true;
            }
            setState(state);//重入性問題
            return false;
        }

        public Condition newCondtionObject(){
            return new ConditionObject();

        }

    }

    @Override
    public void lock() {
        diyLock.acquire(1);
    }


    @Override
    public void lockInterruptibly() throws InterruptedException {
        diyLock.acquireInterruptibly(1);
    }


    @Override
    public boolean tryLock() {
        return diyLock.tryAcquire(1);
    }


    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return diyLock.tryAcquireNanos(1,unit.toNanos(time));
    }


    @Override
    public void unlock() {
        diyLock.release(1);
    }


    @Override
    public Condition newCondition() {
        return diyLock.newCondtionObject();
    }
}

測試自定義鎖:

public class TestMyLock {
    private MyLock lock = new MyLock();

    private int m=0;

    public void increment(){
        lock.lock();
        m++;
        lock.unlock();
    }

    public static void main(String[] args) throws InterruptedException {
        TestMyLock testMyLock = new TestMyLock();

        for (int i = 0; i < 20000; i++) {
            new Thread(() -> {
                testMyLock.increment();
            }).start();
        }

        TimeUnit.SECONDS.sleep(2);
        System.out.println("m="+testMyLock.m);
    }

}

image.png

測試自定義鎖的重入性:

public class TestMyLock2 {
    private MyLock myLock = new MyLock();

    public void entry1(){
        myLock.lock();
        System.out.println("第一次加鎖");
        entry2();
        myLock.unlock();
        System.out.println("釋放第一次加鎖");
    }

    public void entry2(){
        myLock.lock();
        System.out.println("第二次加鎖");
        myLock.unlock();
        System.out.println("釋放第二次加鎖");
    }

    public static void main(String[] args) {
        TestMyLock2 test  = new TestMyLock2();

        new Thread(() ->{
            test.entry1();
        }).start();
    }
}

image.png

測試ReentrantLock鎖的重入性:

    private ReentrantLock lock=new ReentrantLock();
    public void entry1(){
        lock.lock();
        System.out.println("第一次加鎖");
        entry2();
        lock.unlock();
        System.out.println("釋放第一次加鎖");
    }

    public void entry2(){
        lock.lock();
        System.out.println("第二次加鎖");
        lock.unlock();
        System.out.println("釋放第二次加鎖");
    }

    public static void main(String[] args) {
        TestMyLock2 test  = new TestMyLock2();

        new Thread(() ->{
            test.entry1();
        }).start();
    }
}

image.png

三.AQS併發工具

1.CountDownLatch


概念:
CountDownLatch可以使一個或多個線程等待其他線程各自執行完畢後再執行。


CountDownLatch定義了一個計數器,和一個阻塞隊列,當計數器的值遞減0之前,阻塞隊列裏面的線程處於掛起狀態,當計數器遞減到0時會喚醒阻塞隊列所有線程,這裏的計數器是一個標誌,可以表示一個任務一個線程,也可以表示一個倒計時器,CountDownLatch可以解決那些一個或者多個線程執行之前依賴於某些必要的前提業務先執行的場景。


常用的方法說明:

CountDownLatch(int count);//構造方法, 創建一個值count的計數器

await();//阻塞當前線程,將當前線程加入阻塞隊列。

await(long timeout,TimeUnit unit);//在timeout的時間之內阻塞當前線程,時間一過則當前線程可以執行

countDown();//對計數器進行遞減1操作,當計數器遞減至0時,當前線程會去喚醒阻塞隊列的所有線程。

使用CountDownLatch模擬航空公司查詢機票:

public class TestCountDownLatch {

    private static List<String> airCompany = Stream.of("東方航空","南方航空","中國航空").collect(toList());
    private static List<String> fightList = new ArrayList<>();

    public static void main(String[] args) throws InterruptedException {
        String origin = "北京";
        String dest = "上海";

        Thread[] threads = new Thread[airCompany.size()];
        CountDownLatch latch = new CountDownLatch(airCompany.size());

        for (int i = 0; i < threads.length; i++) {
            String name = airCompany.get(i);

            threads[i] = new Thread(() ->{
                System.out.printf("%s查詢從   %s到%s的 機票\n",name,origin,dest);
                //隨機產生票數
                int val = new Random().nextInt(10);
                try {
                    TimeUnit.SECONDS.sleep(val);
                    fightList.add(name  + "---------" + val);
                    System.out.printf("%s公司查詢成功!\n",name);
                    latch.countDown();
                }catch (Exception e){
                    e.printStackTrace();
                }
            });
            threads[i].start();
        }

        latch.await();

        System.out.println("====================查詢結果如下====================");
        fightList.forEach(System.out::println);
    }
}

image.png

2.CyclicBarrier

現實生活中我們經常會遇到這樣的情景,在進行某個活動前需要等待人全部都齊了纔開始。例如喫飯時要等全家人都上座了才動筷子,旅遊時要等全部人都到齊了纔出發,比賽時要等運動員都上場後纔開始。

在JUC包中爲我們提供了一個同步工具類能夠很好的模擬這類場景,它就是CyclicBarrier類。利用CyclicBarrier類可以實現一組線程相互等待,當所有線程都到達某個屏障點後再進行後續的操作。

CyclicBarrier字面意思是“可重複使用的柵欄”,CyclicBarrier 相比 CountDownLatch 來說,要簡單很多,其源碼沒有什麼高深的地方,它是 ReentrantLock 和 Condition 的組合使用。

實例代碼,模擬跑步比賽:

public class TestCyclicBarrier {

    public static void main(String[] args) {
        CyclicBarrier cyclicBarrier = new CyclicBarrier(4);

        Thread[] racer = new Thread[4];

        for (int i = 0; i < 4; i++) {

           racer[i] = new Thread(()->{
                try {
                    TimeUnit.SECONDS.sleep(new Random().nextInt(10));
                    System.out.println(Thread.currentThread().getName() +"準備好了");
                    cyclicBarrier.await();
                } catch (Exception e) {
                    e.printStackTrace();
                }
                System.out.println("選手"+Thread.currentThread().getName()+"起跑");
            },"racer["+i+"]");
            racer[i].start();
        }
    }
}

執行結果:
image.png

3.Semaphore

適合資源有限的場景

Semaphore是一種在多線程環境下使用的設施,該設施負責協調各個線程,以保證它們能夠正確、合理的使用公共資源的設施,也是操作系統中用於控制進程同步互斥的量。Semaphore是一種計數信號量,用於管理一組資源,內部是基於AQS的共享模式。它相當於給線程規定一個量從而控制允許活動的線程數。

工作原理:
以一個停車場是運作爲例。爲了簡單起見,假設停車場只有三個車位,一開始三個車位都是空的。這時如果同時來了五輛車,看門人允許其中三輛不受阻礙的進入,然後放下車攔,剩下的車則必須在入口等待,此後來的車也都不得不在入口處等待。這時,有一輛車離開停車場,看門人得知後,打開車攔,放入一輛,如果又離開兩輛,則又可以放入兩輛,如此往復。這個停車系統中,每輛車就好比一個線程,看門人就好比一個信號量,看門人限制了可以活動的線程。假如裏面依然是三個車位,但是看門人改變了規則,要求每次只能停兩輛車,那麼一開始進入兩輛車,後面得等到有車離開纔能有車進入,但是得保證最多停兩輛車。對於Semaphore類而言,就如同一個看門人,限制了可活動的線程數。

semaphore主要方法:

Semaphore(int permits):構造方法,創建具有給定許可數的計數信號量並設置爲非公平信號量。

Semaphore(int permits,boolean fair):構造方法,當fair等於true時,創建具有給定許可數的計數信號量並設置爲公平信號量。

void acquire():從此信號量獲取一個許可前線程將一直阻塞。相當於一輛車佔了一個車位。

void acquire(int n):從此信號量獲取給定數目許可,在提供這些許可前一直將線程阻塞。比如n=2,就相當於一輛車佔了兩個車位。

void release():釋放一個許可,將其返回給信號量。就如同車開走返回一個車位。

void release(int n):釋放n個許可。

int availablePermits():當前可用的許可數。

實例說明:

public class TestSemaphore {

    public static void main(String[] args) {
        /**
         * 創建信號量
         */
        Semaphore sp = new Semaphore(3);

        Thread[] car = new Thread[5];

        for (int i = 0; i < 5; i++) {
            car[i] = new Thread(() -> {
                //請求許可  請求進入停車場
                try {
                    sp.acquire();
                    System.out.println(Thread.currentThread().getName() + "可以進入停車場");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                //使用資源
                int val = new Random().nextInt(10);
                try {
                    TimeUnit.SECONDS.sleep(val);
                    System.out.println(Thread.currentThread().getName()+"停留了" + val  + "秒");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                //釋放資源
                sp.release();
                System.out.println(Thread.currentThread().getName() +"離開停車場");
            },"car["+i+"]");

            car[i].start();

        }
    }

}

執行結果:
image.png

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章