深入解讀synchronized和ReentrantLock

故事起源于上次阿里電面的3個問題。問題1,jvm中線程分為哪些狀態(tài)。問題2,在執(zhí)行Thread.start()方法后,線程是不是馬上運行。問題3,java中的synchronized和ReentrantLock有什么不同。當時我的回答不是很好,就不說了,面試之后,在網上搜了很多文章,對照著jdk源碼(1.8),發(fā)現這3個問題存在著一些聯系,接下來,就從這3個問題入手,仔細解讀一下線程,synchronized和ReentrantLock。

問題1 jvm中的線程分為哪些狀態(tài)

這個可以看一下jdk中的Thread的State,里面有詳細的注釋,大概含義如下所示

    public enum State {
        NEW, // 初始化,還沒開始執(zhí)行
        RUNNABLE, // 執(zhí)行中
        BLOCKED, // 阻塞
        WAITING, // 等待
        TIMED_WAITING, // 超時等待
        TERMINATED; // 執(zhí)行完成
    }

需要注意的是,這個只是jvm中的線程狀態(tài),并不是操作系統(tǒng)中的線程狀態(tài)。操作系統(tǒng)的線程狀態(tài)中還有一個就緒狀態(tài)(ready)

關于線程狀態(tài)的轉換,盜用了這張圖。來源

statechange.png

線程(英語:thread)是操作系統(tǒng)能夠進行運算調度的最小單位。

問題2 在執(zhí)行Thread.start()方法后,線程是不是馬上運行。

先說答案,不是。

在調用Thread的start方法后,Thread的狀態(tài)變?yōu)?code>RUNNABLE。那么現在這個線程到底有沒有運行呢?查看jdk源碼可知,start方法中調用的是start0的native方法,由他調用底層真正地在操作系統(tǒng)創(chuàng)建一個線程。

操作系統(tǒng)創(chuàng)建的線程馬上就會運行嗎?答案是否定的。線程需要被cpu調度,分配了時間片之后才會真正的運行。因此jvm中的RUNNABLE狀態(tài)其實對應了兩個狀態(tài),readyrunnable。創(chuàng)建的新線程是ready狀態(tài),被cpu調度后成為runnale狀態(tài),這時候才是真正的運行狀態(tài)。

問題3 java中的synchronized和ReentrantLock有什么不同。

這個問題比較龐大,我們先從線程的狀態(tài)入手,來看一下這兩個鎖的區(qū)別。

線程狀態(tài)

首先,我們來思考一下,這兩個鎖都會阻止線程的運行,因此肯定會修改線程的狀態(tài),那么他們阻止之后的線程狀態(tài)是否一致呢?我們帶著這個疑問通過代碼來看一看。

創(chuàng)建一個使用ReentrantLock鎖的線程

public class ThreadStatusTest {
    static final ReentrantLock lock = new ReentrantLock();
    
    public static void main(String[] args) {

        Thread thread1 = new Thread(new Work("thread1"));
        Thread thread2 = new Thread(new Work("thread2"));

        thread1.start();
        thread2.start();

        try {
            // @1
            Thread.sleep(1);
        }catch(Exception ex){

        }

        System.out.println("thread1狀態(tài)" + thread1.getState().toString());
        System.out.println("thread2狀態(tài)" + thread2.getState().toString());

    }
    
    static class Work implements Runnable{
        private String name;
        public Work(String name){
            this.name = name;
        }
        @Override
        public void run(){
            try {
                lock.lock();
                // @2
                Thread.sleep(1 * 1000);
                System.out.println(name+"執(zhí)行完成");
            }
            catch(Exception ex){

            }
            finally {
                lock.unlock();
            }
        }
    }
}

保留@1 @2的sleep,我們測試一下,看看打印的情況,可以看到先執(zhí)行的線程1因為執(zhí)行了內部的sleep后為TIMED_WATTING狀態(tài),后執(zhí)行的線程2為WAITING狀態(tài)。

reentrantsleep.png

我們注釋掉@1 @2的sleep,經過多次測試,我們發(fā)現thread2有時會出現BLOCKEDWAITTING狀態(tài)(這里忽略掉RUNNANLE和TERMINATED狀態(tài))。我們帶著疑問繼續(xù)測試synchronized。

reentrantwithoutsleep

reentrantwithoutsleep

和上面方法相同,我們創(chuàng)建一個使用synchronized鎖的線程

public class ThreadStatusTest {
    
    public static void main(String[] args) {

        Thread thread1 = new Thread(new SyncWork("thread1"));
        Thread thread2 = new Thread(new SyncWork("thread2"));

        thread1.start();
        thread2.start();

        try {
            // @1
            Thread.sleep(1);
        }catch(Exception ex){

        }

        System.out.println("thread1狀態(tài)" + thread1.getState().toString());
        System.out.println("thread2狀態(tài)" + thread2.getState().toString());

    }
    
    static class SyncWork implements Runnable{
        private String name;
        public SyncWork(String name){
            this.name = name;
        }
        @Override
        public void run(){
            synchronized (SyncWork.class){
                try {
                    // @2
                    Thread.sleep(1 * 1000);
                    System.out.println(name+"執(zhí)行完成");
                }catch(Exception ex){

                }
            }
        }
    }
}

和上面相同,先保留@1 @2的sleep,我們測試一下,看看打印的情況,可以看到線程1因為執(zhí)行了內部的sleep后為TIMED_WATTING狀態(tài),但是和ReentrantLock不同的是,線程2為BLOCKED狀態(tài)。

syncwithoutsleep

我們注釋掉@1 @2的sleep,經過多次測試,我們發(fā)現thread2只有BLOCKED狀態(tài)(這里忽略掉RUNNANLETERMINATED狀態(tài))。

syncsleep

因此我們在不看源碼的情況下,可以大概得到結論:

  • synchronized阻塞的線程狀態(tài)為BLOCKED
  • ReentrantLock阻塞的線程狀態(tài)為BLOCKED或者WAITTING

為什么兩個鎖阻止的線程狀態(tài)是不同的呢?我們仔細看一下Thread.State的注釋,主要是BLOCKED,WAITINGTIMED_WATTING這三個

        /**
         * Thread state for a thread blocked waiting for a monitor lock.
         * A thread in the blocked state is waiting for a monitor lock
         * to enter a synchronized block/method or
         * reenter a synchronized block/method after calling
         * {@link Object#wait() Object.wait}.
         */
        BLOCKED,

        /**
         * Thread state for a waiting thread.
         * A thread is in the waiting state due to calling one of the
         * following methods:
         * <ul>
         *   <li>{@link Object#wait() Object.wait} with no timeout</li>
         *   <li>{@link #join() Thread.join} with no timeout</li>
         *   <li>{@link LockSupport#park() LockSupport.park}</li>
         * </ul>
         *
         * <p>A thread in the waiting state is waiting for another thread to
         * perform a particular action.
         *
         * For example, a thread that has called <tt>Object.wait()</tt>
         * on an object is waiting for another thread to call
         * <tt>Object.notify()</tt> or <tt>Object.notifyAll()</tt> on
         * that object. A thread that has called <tt>Thread.join()</tt>
         * is waiting for a specified thread to terminate.
         */
        WAITING,

        /**
         * Thread state for a waiting thread with a specified waiting time.
         * A thread is in the timed waiting state due to calling one of
         * the following methods with a specified positive waiting time:
         * <ul>
         *   <li>{@link #sleep Thread.sleep}</li>
         *   <li>{@link Object#wait(long) Object.wait} with timeout</li>
         *   <li>{@link #join(long) Thread.join} with timeout</li>
         *   <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li>
         *   <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li>
         * </ul>
         */
        TIMED_WAITING,

對照著上面線程狀態(tài)轉換的圖就大概了解了。但是其中一些細節(jié),例如什么是monitor,哪里有調用wait(),LockSupport.park()等方法。
這些細節(jié)問題就需要看synchronized對應的字節(jié)碼和jdk中的ReentrantLock源碼。

源碼實現
synchronized

synchronized是java中的關鍵字,它是在jvm層面實現的,所以我們可以查看一下字節(jié)碼看看它是如何實現的。

我們使用javap將上面SyncWork的字節(jié)碼顯示出來,如下所示

  public void run();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=4, args_size=1
         0: ldc           #3                  // class design/demo/ThreadStatusTest$SyncWork
         2: dup
         3: astore_1
         4: monitorenter
         5: ldc2_w        #4                  // long 1000l
         8: invokestatic  #6                  // Method java/lang/Thread.sleep:(J)V
        11: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
        14: new           #8                  // class java/lang/StringBuilder
        17: dup
        18: invokespecial #9                  // Method java/lang/StringBuilder."<init>":()V
        21: aload_0
        22: getfield      #2                  // Field name:Ljava/lang/String;
        25: invokevirtual #10                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        28: ldc           #11                 // String 執(zhí)行完成
        30: invokevirtual #10                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        33: invokevirtual #12                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        36: invokevirtual #13                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        39: goto          43
        42: astore_2
        43: aload_1
        44: monitorexit
        45: goto          53
        48: astore_3
        49: aload_1
        50: monitorexit
        51: aload_3
        52: athrow
        53: return

我們仔細觀察生成的字節(jié)碼會發(fā)現synchronized包裹的代碼塊在jvm中生成了monitorentermonitorenter這兩個命令。若想了解這兩個命令的實現需要一定的c知識,這篇文章講的很詳細,可以看一下。

synchronized關鍵字經過編譯之后,會在同步塊的前后分別形成monitorenter和monitorexit這兩個字節(jié)碼指令。當我們的JVM把字節(jié)碼加載到內存的時候,會對這兩個指令進行解析。這兩個字節(jié)碼都需要一個Object類型的參數來指明要鎖定和解鎖的對象。如果Java程序中的synchronized明確指定了對象參數,那么這個對象就是加鎖和解鎖的對象;如果沒有明確指定,那就根據synchronized修飾的是實例方法還是類方法,獲取對應的對象實例或Class對象來作為鎖對象

說的簡單點就是synchronized會對一個對象的監(jiān)視器(monitor)進行獲取,而這個獲取的過程是排他的,同一時刻只有一個線程能獲取到此監(jiān)視器。

而這個對象是什么呢?這就要看我們是如何使用的synchronized。

  • 普通同步方法的synchronized。此對象代表當前的實例對象
  • 靜態(tài)同步方法。此對象代表當前的類。即xx.class。
  • 同步方法快。此對象代表你指定的對象。

那么這個監(jiān)視器存放在哪里呢?存放在該對象的對象頭中。對象頭中有一個名為MarkWord的部分,該部分記錄了對象和鎖有關的信息。具體的可以看看這個文章。

由于synchronized是在jvm層的實現,因此閱讀它的源碼需要c的知識,這個理解起來還是有些難度的。但是ReentrantLock就不同了,這個鎖是在jdk中的實現,因此可以很方便的查看源碼。

ReentrantLock

還是使用上面的例子,ReentrantLock的使用很簡單,使用new ReentrantLock(boolean isFair)來創(chuàng)建一個公平或者非公平鎖,使用.lock()方法加鎖。使用.unlock()方法,因此我們就從這三個方法入手,來簡單的看一下它是如何實現鎖的。

它的構造方法有兩個,默認是構造一個非公平鎖,這個也是我們經常用的,如下所示

    public ReentrantLock() {
        sync = new NonfairSync();
    }

    /**
     * Creates an instance of {@code ReentrantLock} with the
     * given fairness policy.
     *
     * @param fair {@code true} if this lock should use a fair ordering policy
     */
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

無論是NonfairSync還是FairSync他們都是繼承了Sync,并且最終繼承了AbstractQueuedSynchronizer,這就是傳說中的AQS。

AQS中的同步等待隊列提供了線程狀態(tài)管理的基本組件,他提供了一些鉤子函數供子類繼承時擴展。而這個同步隊列主要是由一個帶頭尾指針的雙向鏈表組成,jdk中有詳細的注釋。

static final class Node {

        // 等待狀態(tài),主要有3個值。0初始化。-1(SIGNAL)需要喚醒后續(xù)節(jié)點線程。1(CANCELLED)取消等待。
        volatile int waitStatus;

        volatile Node prev;

        volatile Node next;
        
        // 當前節(jié)點對應的線程
        volatile Thread thread;
...

    private transient volatile Node head;

    private transient volatile Node tail;

先大體說一下同步等待隊列的特點:先進先出。獲取鎖失敗的線程構造節(jié)點加入隊列尾部,阻塞自己,等待喚醒。執(zhí)行完成的線程從頭部移出隊列,并喚醒后續(xù)節(jié)點的線程。頭結點是當前獲取到鎖的線程。

還有兩個較為重要的參數

// 位于AbstractOwnableSynchronizer類(AQS的父類),只有獨占鎖使用,代表當前獲取鎖的線程
private transient Thread exclusiveOwnerThread;

// 線程被重入的次數,可能大于0,由于可見性問題使用volatile修飾
private volatile int state;

構造方法先看到這里,我們繼續(xù)解析下一步,.lock()
查看代碼可知真正的lock實現在NonfairSync中,如下所示

final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }

if代碼塊的上面部分很簡單,使用CAS判斷該鎖有沒有占用,沒有占用的話將state置為1,并修改鎖的當前線程。重點看看acquire(1)方法,這個方法在AQS類中

    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

主要是三個方法,tryAcquire(arg)方法是在Sync類中實現的,非公平鎖為nonfairTryAcquire

final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                // 當前線程重入
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

可以看到這個方法中有部分代碼是和上面重復的,這也是出于性能考慮,特殊情況早點返回而不再向下執(zhí)行。
若是返回true表示獲取到鎖,若是返回false表示為獲取到鎖,繼續(xù)向下執(zhí)行。接下來先看addWaiter(Node.EXCLUSIVE)方法,這個方法在AQS類中

   /**
     * Creates and enqueues node for current thread and given mode.
     *
     * @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared
     * @return the new node
     */
    private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode); // 創(chuàng)建一個當前線程的節(jié)點
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail; 
        if (pred != null) {
            // 尾節(jié)點不為空,將該節(jié)點添加到隊尾
            node.prev = pred; // 這里先確定次序,再使用原子操作更新尾節(jié)點
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        enq(node);
        return node;
    }

若隊列為空,將進入enq(node)方法,這個方法在AQS類中,如下所示

    private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            if (t == null) { // Must initialize
                // 隊列為空時,原子操作構造頭結點
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                // 和上層相同代碼,添加到隊尾
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

這是一個死循環(huán),保證節(jié)點添加到隊列中,和上面類似,包含了和上層方法重復的代碼。注意一下,當前的頭結點是new Node(),里面的線程是空的。

現在節(jié)點已經添加到隊列中了,接下來怎么做,
我們繼續(xù)看下一個方法acquireQueued(final Node node, int arg),這個方法在AQS中,如下所示

    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node); // 當前正在執(zhí)行的節(jié)點置為頭結點,這里沒有使用原子操作。
                    p.next = null; // help GC
                    failed = false;
                    return interrupted; // 唯一出口
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt()) // 這里會阻塞,直到前面的節(jié)點線程執(zhí)行完成
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

看到這里發(fā)現頭結點有點類似哨兵節(jié)點。頭結點為正在執(zhí)行的節(jié)點,頭結點執(zhí)行完成后通知后續(xù)節(jié)點解除阻塞,后續(xù)節(jié)點置為頭結點,開始執(zhí)行。有一種特殊的情況,看一下上面代碼的第二個if代碼段,主要是shouldParkAfterFailedAcquire方法,如下所示

    /**
     * Checks and updates status for a node that failed to acquire.
     * Returns true if thread should block. This is the main signal
     * control in all acquire loops.  Requires that pred == node.prev.
     *
     * @param pred node's predecessor holding status
     * @param node the node
     * @return {@code true} if thread should block
     */
    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            /*
             * This node has already set status asking a release
             * to signal it, so it can safely park.
             */
            return true;
        if (ws > 0) {
            /*
             * Predecessor was cancelled. Skip over predecessors and
             * indicate retry.
             */
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            /*
             * waitStatus must be 0 or PROPAGATE.  Indicate that we
             * need a signal, but don't park yet.  Caller will need to
             * retry to make sure it cannot acquire before parking.
             */
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

Node.SIGNAL的定義如下

waitStatus value to indicate successor's thread needs unparking

即只有當前節(jié)點的狀態(tài)為SIGNAL時才會喚醒后續(xù)節(jié)點,因此節(jié)點若想喚醒,必須保證前驅節(jié)點狀態(tài)為SIGNAL。但是有些節(jié)點可能取消了等待,因此需要從后向前遍歷,直到確定前驅節(jié)點為SIGNAL狀態(tài),然后就可以安心阻塞了。阻塞使用的是LockSupport,一種類似wait,notify的方法。

    private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }

執(zhí)行了該方法后,線程就持續(xù)阻塞直到被喚醒。lock()方法到此就結束了,我們接下來看unlock()。

因為已經進行了加鎖,阻塞了其他線程,所以解鎖時相對簡單。

    public void unlock() {
        sync.release(1);
    }

方法很簡單,直接看release方法,該方法在AQS中實現

    public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

我們先看tryRelease方法,使用的是Sync子類中的實現

        protected final boolean tryRelease(int releases) {
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }

考慮線程重入的情況,當state為0時,說明當前沒有線程占用鎖,可以進行下一步操作?;氐缴弦粚拥膔elease方法。操作隊列頭結點,通知后繼節(jié)點。

重點看一下通知的方法,即unparkSuccessor

   private void unparkSuccessor(Node node) {
        /*
         * If status is negative (i.e., possibly needing signal) try
         * to clear in anticipation of signalling.  It is OK if this
         * fails or if status is changed by waiting thread.
         */
        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);

        /*
         * Thread to unpark is held in successor, which is normally
         * just the next node.  But if cancelled or apparently null,
         * traverse backwards from tail to find the actual
         * non-cancelled successor.
         */
        Node s = node.next;
        // 后繼節(jié)點為空或者為取消狀態(tài),從隊尾向前遍歷,找到相鄰的“正?!惫?jié)點
        if (s == null || s.waitStatus > 0) {
            s = null;
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        if (s != null)
            // 喚醒后繼節(jié)點的線程
            LockSupport.unpark(s.thread);
    }

解鎖到這里也就結束了??偨Y一下:ReentrantLock主要是用了一個巧妙的數據結構(帶頭尾指針的雙鏈表)和CAS加自旋以及使用LockSupport的park,unpark(類似wait,notify)來實現加鎖和解鎖。

遺留問題

我們上面的測試代碼中,使用ReentrantLock時,被阻塞的線程出現了BLOCKED狀態(tài),但是如果按照ReentrantLock的源碼,出現WAITTING狀態(tài)是正常的,但并不應該出現BLOCKED狀態(tài),若是有讀者明白原因,請在評論中告知,謝謝了。

寫在后面

對照著源碼看,其實流程很清晰。查看源碼的過程中也會發(fā)現,源碼中有很詳細的注釋,我們一定要仔細的看注釋,這樣才能理解的更透徹。

到這里全文就結束了,文中一些內容參考了大佬的這篇文章,大佬寫的比我詳細,但是自己還想結合碰到的問題梳理一遍,自認為能加強理解,若是感覺作者寫的不好可以去看一下原文,相信一定能幫助到您。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內容