java中幾種鎖

文章內容來自併發編程網的文章閱讀,部分示例代碼已修改。

1. 自旋鎖

 自旋鎖就是它的名字一樣,讓當前線程不停的在一個循環體內執行,當循環的條件被其他線程改變時才能進入臨界區。自旋鎖的示例代碼(僅以非公平鎖爲例)如下:

public class SpinLock {
    /**
     * 臨界區的owner
     */
    private AtomicReference<Thread> owner = new AtomicReference<>();

    public void lock() {
        Thread current = Thread.currentThread();
        // CAS原子操作預測原來的值爲空,在循環體內空循環
        while (!owner.compareAndSet(null, current)) {
        }
    }

    public void unlock() {
        Thread current = Thread.currentThread();
        // CAS原子操作預測原來的值爲當前線程
        owner.compareAndSet(current, null);
    }
}

當第一個線程調用lock()時,臨界區的owner爲空,沒有線程佔用,原值爲空,CAS將owner置爲當前線程A,此時第二個線程再來調用lock()時,由於臨界區的owner此時是第一個線程A,不爲空,因此while()中的條件owner.compareAndSet(null, current)由於預測(null)和實際(線程A)不符不會設置新的owner,返回false(取反爲true),所以一直爲在那裏空循環。直到第一個線程A調用unlock()方法將臨界區的owner置爲null,第二個線程會退出空循環成爲臨界區的新owner。

注:自旋鎖只是將當前線程不停的執行空循環,不進行線程狀態的改變,所以響應速度很快,但當線程數不斷增加時,性能會明顯下降,因爲每個線程都要自旋,佔用CPU時間。若線程競爭不激烈,並保持鎖的時間段,比較適合使用自旋鎖。

1.1 常見3類自旋鎖

 自旋鎖中有3種常見的鎖形式:TicketLock,CLHlock 和 MCSlock。TicketLock主要解決的訪問順序的問題;CLHlock和MCSlock則是兩類類似的公平鎖,以鏈表形式進行排序。其中CLHlock會不停的查詢前驅變量(隱式隊列),所以它不適合在NUMA架構(在該結構下,每個線程分佈在不同的物理內存區域)下使用,CLHLock的全稱爲Craig, Landin, and Hagersten lock,屬於自旋鎖的一種,它可以確保無飢餓,提供先入先出的公平按序服務,基於鏈表實現。MCSLock則是對本地變量的節點進行循環(顯式隊列),不存在CLHlock的問題。重入鎖JUC ReentrantLock默認就是使用的CLH鎖。3種鎖的示例代碼如下:

/**
 * @author liuwg-a
 * @date 2019/10/16 9:11
 * @description 自旋鎖的一種 由於每次都要查詢ServiceNum服務號會影響性能(必須要到主存讀取並阻止其他cpu修改)
 */
public class TicketLock {
    private AtomicInteger serviceNum = new AtomicInteger();
    private AtomicInteger ticketNum = new AtomicInteger();
    private static final ThreadLocal<Integer> LOCAL = new ThreadLocal<>();

    public void lock() {
        int myTicket = ticketNum.getAndIncrement();
        LOCAL.set(myTicket);
        while (myTicket != serviceNum.get()) {
        }
    }

    public void unlock() {
        int myTicket = LOCAL.get();
        serviceNum.compareAndSet(myTicket, myTicket + 1);
    }
}

/**
 * @author liuwg-a
 * @date 2019/10/16 19:54
 * @description CLHLock 會不停查詢前驅變量
 */
public class CLHLock {
    /**
    * 在創建節點時,初始化行爲就將標識符置爲 true,表示該節點需要鎖,
    * 只要新創建節點就默認表示它需要同步鎖
    */
    public static class CLHNode {
        private volatile boolean isLocked = true;
    }

    private volatile CLHNode tail;
    private static final ThreadLocal<CLHNode> LOCAL = new ThreadLocal<>();
    // 採用鏈表的形式
    private static final AtomicReferenceFieldUpdater<CLHLock, CLHNode> UPDATER = AtomicReferenceFieldUpdater.newUpdater(CLHLock.class, CLHNode.class, "tail");

    public void lock() {
        // 1. 創建一個CLHNode
        CLHNode node = new CLHNode();
        LOCAL.set(node);
        // 2. 線程對tail調用 getAndSet 方法使自己成爲 tail,同時獲取它前驅節點的引用 preNode
        CLHNode preNode = UPDATER.getAndSet(this, node);
        // 3. 在前驅節點 preNode 上進行旋轉,直至前驅節點釋放鎖
        if (preNode != null) {
            while (preNode.isLocked) {
            }
            // 4. 將前驅節點置爲 null
            preNode = null;
            LOCAL.set(node);
        }
    }

    public void unlock() {
        CLHNode node = LOCAL.get();
        // 釋放鎖時,將當前節點的 isLocked 標識符置爲false,表示此時該線程不需要鎖
        if (!UPDATER.compareAndSet(this, node, null))){
            node.isLocked = false;
        }
        // 將 node 節點置爲 null
        node = null;
    }
}

/**
 * @author liuwg-a
 * @date 2019/10/17 10:46
 * @description MCSLock是對本地變量的節點進行循環
 */
public class MCSLock {
    public static class MCSNode {
        volatile MCSNode next;
        volatile boolean isLocked = true;
    }

    private static final ThreadLocal<MCSNode> NODE = new ThreadLocal<>();
    @SuppressWarnings("unused")
    private volatile MCSNode queue;
    private static final AtomicReferenceFieldUpdater<MCSLock, MCSNode> UPDATER = AtomicReferenceFieldUpdater.newUpdater(MCSLock.class, MCSNode.class, "queue");

    public void lock() {
        MCSNode current = new MCSNode();
        NODE.set(current);
        MCSNode preNode = UPDATER.getAndSet(this, current);
        if (preNode != null) {
            preNode.next = current;
            while (current.isLocked) {

            }
        }
    }

    public void unlock() {
        MCSNode currentNode = NODE.get();
        if (currentNode.next == null) {
            if (UPDATER.compareAndSet(this, currentNode, null)) {
            } else {
                while (currentNode.next == null) {
                }
            }
        } else {
            currentNode.next.isLocked = false;
            currentNode.next = null;
        }
    }
}

2. 阻塞鎖

 阻塞鎖會讓線程進入阻塞狀態,當獲得響應的信號時才能進入線程的準備就緒狀態,能夠和阻塞相關的關鍵字和方法有:sychronizedReentrantLockObject.wait()以及LockSupport.park()/unpark。阻塞鎖的代碼示例:

public class BlockLock {
    private static class BlockNode {
        private volatile Thread isLocked;
    }

    private volatile BlockNode tail;
    private static final ThreadLocal<BlockNode> LOCAL = new ThreadLocal<>();
    private static final AtomicReferenceFieldUpdater<BlockLock, BlockNode> UPDATER = AtomicReferenceFieldUpdater.newUpdater(BlockLock.class, BlockNode.class, "tail");

    public void lock() {
        BlockNode node = new BlockNode();
        LOCAL.set(node);
        BlockNode preNode = UPDATER.getAndSet(this, node);
        if (preNode != null) {
            preNode.isLocked = Thread.currentThread();
            LockSupport.park(this);
            preNode = null;
            LOCAL.set(node);
        }
    }

    public void unLock() {
        BlockNode node = LOCAL.get();
        if (UPDATER.compareAndSet(this, node, null)) {
            System.out.println("unlock\t" + node.isLocked.getName());
            // 使用 LockSupport.unpark() 阻塞鎖
            LockSupport.unpark(node.isLocked);
        }
        node = null;
    }
}

阻塞鎖的優勢在於:阻塞的線程不會佔用CPU的時間,但進入時間和恢復時間比自旋鎖慢。在競爭激烈的情況下阻塞鎖的性能明顯高於自旋鎖(阻塞的線程不會額外佔用CPU的時間),理想場景下,競爭不激烈可以選用自旋鎖,響應比較及時,競爭激烈使用阻塞鎖。

3. 可重入鎖

 可重入鎖,也叫遞歸鎖,本意爲一個線程可以重複獲取鎖,指同一個線程外層函數獲取鎖後,內層遞歸函數仍然有獲取該鎖的代碼,但不受影響。在java語言中ReentrantLocksychronized都是可重入鎖。

TestLock testLock;

void test1() {
    testLock.lock();
    doSth();
    testLock.unLock();
}

void test2() {
    testLock.lock();
    test1();
    doSth();
    testLock.unLock();
}

在方法test1()test2()方法中都有對同一個鎖testLock獲取的行爲,如果上述的TestLock不是重入鎖,調用test2()方法時肯定會出現死鎖問題,但如果它是可重入鎖,那沒有問題。但不要認爲所有的死鎖問題都可以使用重入鎖來代替非重入鎖的方式來解決。給出問題場景:如果test2()test()1都在操作同一個共享變量,此時 testLock 是可重入鎖,在上述2個方法中同時更改該變量,此時就會出現問題。下面是ReentrantLocksychronized使用的示例代碼:

/**
 * 可重入鎖測試,結果是每個線程的getter和setter方法都可以輸出,沒有出現死鎖的情況,得出可重入的結論
 * @author liuwg-a
 * @date 2019/11/1 14:22
 * @description
 */
public class ReentrantTest {
    private static final int THREAD_COUNT = 3;

    public static void main(String[] args) {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 10, 10, TimeUnit.SECONDS, new LinkedBlockingQueue<>(10), new MyFactoryBuilder().build());

        // 爲了讓2種場景下的結果觀測更明顯,使用串行的方式進行測試(因爲懶,所以寫到一個main方法內)
        CountDownLatch synchronizedCount = new CountDownLatch(THREAD_COUNT);
        SynchronizedRunnable synchronizedRunnable = new SynchronizedRunnable(synchronizedCount);
        for (int i = 0; i < THREAD_COUNT; i++) {
            executor.execute(synchronizedRunnable);
        }
        try {
            synchronizedCount.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        ReentrantLockRunnable reentrantLockRunnable = new ReentrantLockRunnable();
        for (int i = 0; i < THREAD_COUNT; i++) {
            executor.execute(reentrantLockRunnable);
        }
    }
}

/**
 * synchronized 關鍵字可重入測試類
 */
class SynchronizedRunnable implements Runnable {

    private CountDownLatch countDownLatch;

    public SynchronizedRunnable(CountDownLatch countDownLatch) {
        this.countDownLatch = countDownLatch;
    }

    @Override
    public void run() {
        get();
        countDownLatch.countDown();
    }

    public synchronized void get() {
        System.out.println("SynchronizedRunnable.get(): " + Thread.currentThread());
        set();
    }

    public synchronized void set() {
        System.out.println("SynchronizedRunnable.set(): " + Thread.currentThread());
    }

}

/**
 * ReentrantLock 可重入測試類
 */
class ReentrantLockRunnable implements Runnable {

    // 可重入鎖
    private ReentrantLock myLock = new ReentrantLock();
//     自旋鎖,不可重入鎖,測試自旋鎖的不可重入性使用這個Lock,會出現死鎖的現象,在getter獲取鎖後,其內部函數無法再次獲取lock產生自旋,而外層的getter則始終持有鎖
//    private SpinLock myLock = new SpinLock();

    @Override
    public void run() {
        get();
    }

    public void get() {
        myLock.lock();
        System.out.println("ReentrantLockRunnable.get(): " + Thread.currentThread());
        set();
        myLock.unlock();
    }

    public void set() {
        myLock.lock();
        System.out.println("ReentrantLockRunnable.set(): " + Thread.currentThread());
        myLock.unlock();
    }
}

可重入鎖重點解決的問題是死鎖。

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