Depth interpretation synchronized and ReentrantLock

The story originated in the last three issues Ali electrical surface. Issue 1, jvm which threads into the state. Question 2, after performing Thread.start () method, the thread is not running right away. Issue 3, java in a synchronized and ReentrantLock any different. My answer was not very good, not to say, after the interview, the Internet found a lot of articles on the shining jdk source (1.8), found that there are some links these three questions, then, on this issue 3 starting carefully interpret what threads, synchronized and ReentrantLock.

Issue 1 jvm threads in which the state is divided into

This can look at the Thread of State jdk in, there are detailed notes about the meanings as follows

    public enum State {
        NEW, // 初始化,还没开始执行
        RUNNABLE, // 执行中
        BLOCKED, // 阻塞
        WAITING, // 等待
        TIMED_WAITING, // 超时等待
        TERMINATED; // 执行完成
    }

It should be noted that this is only a thread in the state jvm thread state is not the operating system. Thread state of the operating system also has a ready state (ready)

About conversion thread state, I stole this picture. source
statechange.png

Thread (English: thread) is the smallest unit of an operating system capable of operation scheduling.

Question 2 after performing Thread.start () method, the thread is not running right away.

Let me answer, no.

Thread after calling the start method, Thread of state to RUNNABLE. So now there is this thread in the end it did not run? View jdk source code can be seen, start method is called in the native method start0, which he calls the underlying operating system really create a thread.

Multithreaded operating system created will soon run it? the answer is negative. Will really need to be run after thread cpu scheduling, allocation of time slices. Thus jvm the RUNNABLEstate actually corresponds to two states, readyand runnable. New thread is created by readythe state, the cpu is scheduled to become runnalea state, which is the real time operation.

3 java problem in synchronized and ReentrantLock any different.

This problem is rather large, we start with the state of the thread to start, look at the difference between these two locks.

Thread State

First, let's think about these two lock will stop running threads, and therefore will certainly modify the state of the thread, the thread state after they stop it are the same? With this question we have to take a look through the code.

Create a thread lock using 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状态" + thread1.getState().toString());
        System.out.println("thread2状态" + 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+"执行完成");
            }
            catch(Exception ex){

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

Retention @1 @2of sleep, we test to see if the print, you can see the first threads of execution carried out since 1 after sleep inside for the TIMED_WATTINGstate, after the execution of the thread 2 WAITINGstate.
reentrantsleep.png

We comment out @1 @2of sleep, after several tests, we found thread2 sometimes BLOCKEDand WAITTINGstate (here ignored RUNNANLE and TERMINATED state). We continue to test with questions synchronized.
reentrantwithoutsleep

reentrantwithoutsleep

The same method as above, we use synchronized to create a thread lock

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状态" + thread1.getState().toString());
        System.out.println("thread2状态" + 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+"执行完成");
                }catch(Exception ex){

                }
            }
        }
    }
}

Same as above, the first reservation @1 @2of sleep, we test to see if the print, you can see the thread 1 because the execution of the sleep inside the post for the TIMED_WATTINGstate, but the difference is ReentrantLock and thread 2 BLOCKEDstate.
syncwithoutsleep

We comment out @1 @2of sleep, after several tests, we found thread2 only BLOCKEDstate (here ignored RUNNANLEand TERMINATEDstate).
syncsleep

So we do not look at the case of the source code, we can probably conclude that:

  • synchronized blocked thread stateBLOCKED
  • ReentrantLock blocked thread state BLOCKEDorWAITTING

Why two locks prevent the thread state is it different? We take a closer look Thread.State comments, mainly BLOCKED, WAITING, TIMED_WATTINGthree

        /**
         * 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,

对照着上面线程状态转换的图就大概了解了。但是其中一些细节,例如什么是monitor,哪里有调用wait(),LockSupport.park()等方法。
这些细节问题就需要看synchronized对应的字节码和jdk中的ReentrantLock源码。

源码实现
synchronized

synchronized是java中的关键字,它是在jvm层面实现的,所以我们可以查看一下字节码看看它是如何实现的。

我们使用javap将上面SyncWork的字节码显示出来,如下所示

  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 执行完成
        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

我们仔细观察生成的字节码会发现synchronized包裹的代码块在jvm中生成了monitorentermonitorenter这两个命令。若想了解这两个命令的实现需要一定的c知识,这篇文章讲的很详细,可以看一下。

synchronized关键字经过编译之后,会在同步块的前后分别形成monitorenter和monitorexit这两个字节码指令。当我们的JVM把字节码加载到内存的时候,会对这两个指令进行解析。这两个字节码都需要一个Object类型的参数来指明要锁定和解锁的对象。如果Java程序中的synchronized明确指定了对象参数,那么这个对象就是加锁和解锁的对象;如果没有明确指定,那就根据synchronized修饰的是实例方法还是类方法,获取对应的对象实例或Class对象来作为锁对象

说的简单点就是synchronized会对一个对象的监视器(monitor)进行获取,而这个获取的过程是排他的,同一时刻只有一个线程能获取到此监视器。

而这个对象是什么呢?这就要看我们是如何使用的synchronized。

  • 普通同步方法的synchronized。此对象代表当前的实例对象
  • 静态同步方法。此对象代表当前的类。即xx.class。
  • 同步方法快。此对象代表你指定的对象。

那么这个监视器存放在哪里呢?存放在该对象的对象头中。对象头中有一个名为MarkWord的部分,该部分记录了对象和锁有关的信息。具体的可以看看这个文章

由于synchronized是在jvm层的实现,因此阅读它的源码需要c的知识,这个理解起来还是有些难度的。但是ReentrantLock就不同了,这个锁是在jdk中的实现,因此可以很方便的查看源码。

ReentrantLock

还是使用上面的例子,ReentrantLock的使用很简单,使用new ReentrantLock(boolean isFair)来创建一个公平或者非公平锁,使用.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中的同步等待队列提供了线程状态管理的基本组件,他提供了一些钩子函数供子类继承时扩展。而这个同步队列主要是由一个带头尾指针的双向链表组成,jdk中有详细的注释。

static final class Node {

        // 等待状态,主要有3个值。0初始化。-1(SIGNAL)需要唤醒后续节点线程。1(CANCELLED)取消等待。
        volatile int waitStatus;

        volatile Node prev;

        volatile Node next;
        
        // 当前节点对应的线程
        volatile Thread thread;
...

    private transient volatile Node head;

    private transient volatile Node tail;

先大体说一下同步等待队列的特点:先进先出。获取锁失败的线程构造节点加入队列尾部,阻塞自己,等待唤醒。执行完成的线程从头部移出队列,并唤醒后续节点的线程。头结点是当前获取到锁的线程。

还有两个较为重要的参数

// 位于AbstractOwnableSynchronizer类(AQS的父类),只有独占锁使用,代表当前获取锁的线程
private transient Thread exclusiveOwnerThread;

// 线程被重入的次数,可能大于0,由于可见性问题使用volatile修饰
private volatile int state;

构造方法先看到这里,我们继续解析下一步,.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;
        }

可以看到这个方法中有部分代码是和上面重复的,这也是出于性能考虑,特殊情况早点返回而不再向下执行。
若是返回true表示获取到锁,若是返回false表示为获取到锁,继续向下执行。接下来先看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); // 创建一个当前线程的节点
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail; 
        if (pred != null) {
            // 尾节点不为空,将该节点添加到队尾
            node.prev = pred; // 这里先确定次序,再使用原子操作更新尾节点
            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;
                }
            }
        }
    }

这是一个死循环,保证节点添加到队列中,和上面类似,包含了和上层方法重复的代码。注意一下,当前的头结点是new Node(),里面的线程是空的。

现在节点已经添加到队列中了,接下来怎么做,
我们继续看下一个方法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); // 当前正在执行的节点置为头结点,这里没有使用原子操作。
                    p.next = null; // help GC
                    failed = false;
                    return interrupted; // 唯一出口
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt()) // 这里会阻塞,直到前面的节点线程执行完成
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

看到这里发现头结点有点类似哨兵节点。头结点为正在执行的节点,头结点执行完成后通知后续节点解除阻塞,后续节点置为头结点,开始执行。有一种特殊的情况,看一下上面代码的第二个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

即只有当前节点的状态为SIGNAL时才会唤醒后续节点,因此节点若想唤醒,必须保证前驱节点状态为SIGNAL。但是有些节点可能取消了等待,因此需要从后向前遍历,直到确定前驱节点为SIGNAL状态,然后就可以安心阻塞了。阻塞使用的是LockSupport,一种类似wait,notify的方法。

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

执行了该方法后,线程就持续阻塞直到被唤醒。lock()方法到此就结束了,我们接下来看unlock()

因为已经进行了加锁,阻塞了其他线程,所以解锁时相对简单。

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

The method is simple, direct look at the release method, the method is implemented in the 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;
    }

We look at tryRelease method used is to achieve Sync subclasses

        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;
        }

Considering reentrant thread, when the state is 0, indicating that no thread is currently holding the lock, the next step may be performed. Back on the release layer method. Operating the first node queue, notify the subsequent node.

Key to look at the notification method that 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;
        // 后继节点为空或者为取消状态,从队尾向前遍历,找到相邻的“正常”节点
        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)
            // 唤醒后继节点的线程
            LockSupport.unpark(s.thread);
    }

Unlock it to end here. To summarize: ReentrantLock mainly used an ingenious data structure (the lead tail pointer doubly-linked list) and CAS plus spin and use LockSupport the park, unpark (similar wait, notify) to achieve locking and unlocking.

Remaining problem

We tested the above code, the use of ReentrantLock, blocked thread emerged BLOCKEDstate, but if a ReentrantLock source, appears WAITTINGthe state is normal, but it does not appear to be BLOCKEDa state, if readers understand why, please advise in the comments ,Thank you.

Written on the back

Look for shining source, in fact, the process is very clear. View source process will also find that there are very detailed source notes, we must look carefully annotated, a more thorough understanding of the order.

Here the full text over, some elements of the text reference to the chiefs of the article , written by detailed than my big brother, but that I would like to combine problems encountered comb it again, since that can enhance understanding, if you feel the authors write I can not see what the original text, I believe we can help you.

Guess you like

Origin www.cnblogs.com/AceZhai/p/12024017.html