Java多執行緒(6):鎖與AQS(下)

您好,我是湘王,這是我的部落格園,歡迎您來,歡迎您再來~

 

之前說過,AQS(抽象隊列同步器)Java鎖機制的底層實現。既然它這麼優秀,是騾子是馬,就拉出來溜溜吧。

首先用重入鎖來實現簡單的累加,就像這樣:

/**
 * 用重入鎖實現累加
 *
 * @author 湘王
 */
public class MyLockTest {
    private final Lock lock = new ReentrantLock();
    private int value;
    public int getNext() {
        lock.lock();
        try {
            value++;
        } finally {
            lock.unlock();
        }
        return value;
    }
    public static void main(String[] args) {
        MyLockTest myLock = new MyLockTest();
        for (int i = 0; i < 5; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 5; i++) {
                        System.out.println(myLock.getNext());
                    }
                }
            }).start();
        }
    }
}

 

運行結果顯示數據有重複:

 

 

 

這麼簡單的計算都能出現重複,這肯定是無法接受的。

再用獨佔鎖來試試看:

/**
 * 利用AQS實現自定義獨佔鎖
 *
 * @author 湘王
 */
public class MyExclusiveLock implements Lock {
    @Override
    public void lock() {

    }

    @Override
    public void lockInterruptibly() throws InterruptedException {

    }

    @Override
    public boolean tryLock() {
        return false;
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return false;
    }

    @Override
    public void unlock() {

    }

    @Override
    public Condition newCondition() {
        return null;
    }
}

 

 

可以看到,實現lock介面,就需要實現若干自定義的介面。然後以內部類繼承AQS的方式,實現排他鎖,昨天也說過,AQS中tryAcquire()和tryRelease()是一一對應的,也就是也管獲取,一個管釋放,所以程式碼是:

/**
 * 內部類繼承AQS的方式,實現排他鎖
 */
private static class SyncHelper extends AbstractQueuedSynchronizer {
    private static final long serialVersionUID = -7666580981453962426L;

    /**
     * 第一個執行緒進來,拿到鎖就返回true;後面的執行緒進來,拿不到鎖就返回false
     */
    @Override
    protected boolean tryAcquire(int arg) {
        // 獲取資源狀態
        int state = getState();
        if (0 == state) {// 如果沒有執行緒拿到資源的鎖
            if (compareAndSetState(0, arg)) {
                // 保存當前持有同步鎖的執行緒
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
        } else if (Thread.currentThread() == getExclusiveOwnerThread()) {
            // 如果當前執行緒再次進來,state + 1,可重入
            // 如果這裡沒有這個判斷,那麼程式會卡死
            setState(state + arg);
            return true;
        }
        return false;
    }

    /**
     * 鎖的獲取和釋放需要一一對應
     */
    @Override
    protected boolean tryRelease(int arg) {
        // 獲取資源狀態
        int state = getState();
        // 返回最後一個通過setExclusiveOwnerThread()方法設置過的執行緒,或者null
        if (Thread.currentThread() != getExclusiveOwnerThread()) {
            throw new RuntimeException();
        }
        setState(state - arg);
        if (0 == state) {
            setExclusiveOwnerThread(null);
            return true;
        }
        return false;
    }

    protected Condition newCondition() {
        return new ConditionObject();
    }
}

 

 

然後再用AQS實現lock介面的方法:

/**
 * 利用AQS實現自定義獨佔鎖
 *
 * @author 湘王
 */
public class MyExclusiveLock implements Lock {
    private final SyncHelper synchepler = new SyncHelper();

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

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

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

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

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

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

    /**
     * 內部類繼承AQS的方式,實現排他鎖
     */
    private static class SyncHelper extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = -7666580981453962426L;
    
        /**
         * 第一個執行緒進來,拿到鎖就返回true;後面的執行緒進來,拿不到鎖就返回false
         */
        @Override
        protected boolean tryAcquire(int arg) {
            // 獲取資源狀態
            int state = getState();
            if (0 == state) {// 如果沒有執行緒拿到資源的鎖
                if (compareAndSetState(0, arg)) {
                    // 保存當前持有同步鎖的執行緒
                    setExclusiveOwnerThread(Thread.currentThread());
                    return true;
                }
            } else if (Thread.currentThread() == getExclusiveOwnerThread()) {
                // 如果當前執行緒再次進來,state + 1,可重入
                // 如果這裡沒有這個判斷,那麼程式會卡死
                setState(state + arg);
                return true;
            }
            return false;
        }
    
        /**
         * 鎖的獲取和釋放需要一一對應
         */
        @Override
        protected boolean tryRelease(int arg) {
            // 獲取資源狀態
            int state = getState();
            // 返回最後一個通過setExclusiveOwnerThread()方法設置過的執行緒,或者null
            if (Thread.currentThread() != getExclusiveOwnerThread()) {
                throw new RuntimeException();
            }
            setState(state - arg);
            if (0 == state) {
                setExclusiveOwnerThread(null);
                return true;
            }
            return false;
        }
    
        protected Condition newCondition() {
            return new ConditionObject();
        }
    }
}

 

 

然後再運行測試:

/**
 * 實現Lock介面方法並運行排他鎖測試
 *
 * @author 湘王
 */
public class MyExclusiveLockTester {
    // 用自定義AQS獨佔鎖實現
    private Lock lock = new MyExclusiveLock();
    private int value;

    public int accmulator() {
        lock.lock();
        try {
            ++value;
        } finally {
            lock.unlock();
        }

        return value;
    }

    public static void main(String[] args) throws InterruptedException {
        MyExclusiveLockTester test = new MyExclusiveLockTester();
        for (int i = 0; i < 5; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 5; i++) {
                        System.out.println(test.accmulator());
                    }
                }
            }).start();
        }
    }
}

 

 

可以看到,結果無論怎麼樣都不會再重複了。

 

這個只是簡單的累加,接下來用AQS來實現一個實際的生活場景。比如周末帶女票或男票去步行街吃飯,這時候人特別多,需要搖號,而且一次只能進去三張號(不按人頭算,按叫到的號來算),該怎麼實現呢?

可以順著這個思路:搖號機雖有很多號,但它本質上是個共享資源,很多人可以共享,但是每次共享的數量有限。這其實就是個可以指定數量的共享鎖而已。

既然有了思路,那接下來就好辦了。

/**
 * 利用AQS實現自定義共享鎖
 *
 * @author 湘王
 */
public class MyShareLock implements Lock {
    @Override
    public void lock() {
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {
    }

    @Override
    public boolean tryLock() {
        return false;
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return false;
    }

    @Override
    public void unlock() {
    }

    @Override
    public Condition newCondition() {
        return null;
    }
}

 

 

還是一樣實現Lock介面,但這次是用AQS實現共享鎖。

/**
 * 內部類繼承AQS實現共享鎖
 *
 */
private static class SyncHelper extends AbstractQueuedSynchronizer {
    private static final long serialVersionUID = -7357716912664213942L;

    /**
     * count表示允許幾個執行緒能同時獲得鎖
     */
    public SyncHelper(int count) {
        if (count <= 0) {
            throw new IllegalArgumentException("鎖資源數量必須大於0");
        }
        // 設置資源總數
        setState(count);
    }

    /**
     * 一次允許多少個執行緒進來,允許數量的執行緒都能拿到鎖,其他的執行緒進入隊列
     */
    @Override
    protected int tryAcquireShared(int acquires) {
        // 自旋
        for (;;) {
            int state = getState();
            int remain = state - acquires;
            // 判斷剩餘鎖資源是否已小於0或者CAS執行是否成功
            if (remain < 0 || compareAndSetState(state, remain)) {
                return remain;
            }
        }
    }

    /**
     * 鎖資源的獲取和釋放要一一對應
     */
    @Override
    protected boolean tryReleaseShared(int releases) {
        // 自旋
        for (;;) {
            // 獲取當前state
            int current = getState();
            // 釋放狀態state增加releases
            int next = current + releases;
            if (next < current) {// 溢出
                throw new Error("Maximum permit count exceeded");
            }
            // 通過CAS更新state的值
            // 這裡不能用setState()
            if (compareAndSetState(current, next)) {
                return true;
            }
        }
    }

    protected Condition newCondition() {
        return new ConditionObject();
    }
}

 

 

然後再來改造之前實現的介面:

/**
 * 利用AQS實現自定義共享鎖
 *
 * @author 湘王
 */
public class MyShareLock implements Lock {
    public static int count;
    private final SyncHelper synchepler = new SyncHelper(count);

    @Override
    public void lock() {
        synchepler.acquireShared(1);
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {
        synchepler.acquireSharedInterruptibly(1);
    }

    @Override
    public boolean tryLock() {
        return synchepler.tryAcquireShared(1) > 0;
    }

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

    @Override
    public void unlock() {
        synchepler.releaseShared(1);
    }

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

    /**
     * 內部類繼承AQS實現共享鎖
     *
     */
    private static class SyncHelper extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = -7357716912664213942L;

        /**
         * count表示允許幾個執行緒能同時獲得鎖
         */
        public SyncHelper(int count) {
            if (count <= 0) {
                throw new IllegalArgumentException("鎖資源數量必須大於0");
            }
            // 設置資源總數
            setState(count);
        }

        /**
         * 一次允許多少個執行緒進來,允許數量的執行緒都能拿到鎖,其他的執行緒進入隊列
         */
        @Override
        protected int tryAcquireShared(int acquires) {
            // 自旋
            for (;;) {
                int state = getState();
                int remain = state - acquires;
                // 判斷剩餘鎖資源是否已小於0或者CAS執行是否成功
                if (remain < 0 || compareAndSetState(state, remain)) {
                    return remain;
                }
            }
        }

        /**
         * 鎖資源的獲取和釋放要一一對應
         */
        @Override
        protected boolean tryReleaseShared(int releases) {
            // 自旋
            for (;;) {
                // 獲取當前state
                int current = getState();
                // 釋放狀態state增加releases
                int next = current + releases;
                if (next < current) {// 溢出
                    throw new Error("Maximum permit count exceeded");
                }
                // 通過CAS更新state的值
                // 這裡不能用setState()
                if (compareAndSetState(current, next)) {
                    return true;
                }
            }
        }

        protected Condition newCondition() {
            return new ConditionObject();
        }
    }
}

 

 

接下來就該測試咱們需要的效果是否能實現了:

public class MyShareLockTester {
    public static void main(String[] args) throws InterruptedException {
        // 用自定義AQS共享鎖實現
        // 一次允許發放三把鎖
        MyShareLock.count = 3;
        final Lock lock = new MyShareLock();

        // 模擬20個客戶端訪問
        for (int i = 0; i < 20; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        lock.lock();
                        System.out.println("持有 " + Thread.currentThread().getName() + " 的客人可以進餐廳就餐");
                        // 每兩次叫號之間間隔一段時間,模擬真實場景
                        Thread.sleep(3000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } finally {
                        // 使用完成釋放鎖
                        lock.unlock();
                    }
                }
            }).start();
        }
    }
}

 

這裡有20個號,每次只能發放3張,運行之後就可以看到確實如此。

AQS是個很神奇也很好玩的東西,就像它的作者(也是除了高司令就是對Java影響最大的那個人,整個Java的多執行緒juc包程式碼就是他編寫的Doug LeaAbstractQueuedSynchronizer的注釋中所說:AQS只是一個框架,至於怎麼玩,就是你的事了!

 

 


 

 

感謝您的大駕光臨!諮詢技術、產品、運營和管理相關問題,請關注後留言。歡迎騷擾,不勝榮幸~