之前我们已经介绍过synchronized的用法(https://blog.nowcoder.net/n/56f48592a1db4c0fb8d345eeb7d9009f)和CAS(https://blog.nowcoder.net/n/fe8e09af8724406c90136c0ce97ddc7d),这篇文章在谈谈synchronized的原理。以及介绍一下Lock的用法和稍微介绍一下Lock原理。


参考文章:
https://blog.csdn.net/scdn_cp/article/details/86491792(Java对象结构与锁实现原理及MarkWord详解
https://www.cnblogs.com/deltadeblog/p/9559035.html(主要参考自旋锁文段)
https://www.jianshu.com/p/2ba154f275ea主要参考synchronized原理文段
https://blog.csdn.net/u011212394/article/details/82228321主要参考wait和notify文段

https://www.jianshu.com/p/ab3e83609f8f(Lock用法详细讲解以及具体例子)



synchronized在JDK1.6中,进行了优化,引入了适应自旋锁,锁消除,锁粗化,轻量级锁,偏向锁。本文介绍synchronized先从各种优化讲起,到优化完会讲到原始重量级锁synchronized的原理。

要理解synchronized的原理,首先要了解一下对象头。

对象头

bit(即比特位,即位,一字节八位)
对象的几个部分的作用:

1.对象头中的Mark Word(标记字)主要用来表示对象的线程锁状态,另外还可以用来配合GC、存放该对象的hashCode;

2.Klass Word是一个指向方法区中Class信息的指针,意味着该对象可随时知道自己是哪个Class的实例;
3.数组长度也是占用64位(8字节)的空间,这是可选的,只有当本对象是一个数组对象时才会有这个部分;
4.对象体是用于保存对象属性和值的主体部分,占用内存空间取决于对象的属性数量和类型;
5.对齐字是为了减少堆内存的碎片空间(不一定准确)。

此处主要介绍Mark Word:Mark Word的存储结构并不是固定不变的,虚拟机会根据锁的状态调整存储,充分利用存储空间,以下是各个结构的示意图:
中文版:
英文版:

biased_lock:对象是否启用偏向锁标记,只占1个二进制位。为1时表示对象启用偏向锁,为0时表示对象没有偏向锁。lock和biased_lock共同表示对象处于什么锁状态。
age:4位的Java对象年龄。在GC中,如果对象在Survivor区复制一次,年龄增加1。当对象达到设定的阈值时,将会晋升到老年代。默认情况下,并行GC的年龄阈值为15,并发GC的年龄阈值为6。由于age只有4位,所以最大值为15,这就是-XX:MaxTenuringThreshold选项最大值为15的原因。
identity_hashcode:31位的对象标识hashCode,采用延迟加载技术。调用方法System.identityHashCode()计算,并会将结果写到该对象头中。当对象加锁后(偏向、轻量级、重量级),MarkWord的字节没有足够的空间保存hashCode,因此该值会移动到管程Monitor中。
thread:持有偏向锁的线程ID。
epoch:偏向锁的时间戳。
ptr_to_lock_record:轻量级锁状态下,指向栈中锁记录的指针。
ptr_to_heavyweight_monitor:重量级锁状态下,指向对象监视器Monitor的指针。

Mark Word的各种状态转换:
1.初期锁对象刚创建时,还没有任何线程来竞争,对象的Mark Word是下图的第一种情形,这偏向锁标识位是0,锁状态01,说明该对象处于无锁状态(无线程竞争它)。

2.当有一个线程来竞争锁时,先用偏向锁,表示锁对象偏爱这个线程,这个线程要执行这个锁关联的任何代码,不需要再做任何检查和切换,这种竞争不激烈的情况下,效率非常高。这时Mark Word会记录自己偏爱的线程的ID,把该线程当做自己的熟人。如下图第二种情形。

3.当有两个线程开始竞争这个锁对象,情况发生变化了,不再是偏向(独占)锁了,锁会升级为轻量级锁,两个线程公平竞争,哪个线程先占有锁对象并执行代码,锁对象的Mark Word就执行哪个线程的栈帧中的锁记录。如下图第三种情形。

4.如果竞争的这个锁对象的线程更多,导致了更多的切换和等待,JVM会把该锁对象的锁升级为重量级锁,这个就叫做同步锁,这个锁对象Mark Word再次发生变化,会指向一个监视器对象,这个监视器对象用集合的形式,来登记和管理排队的线程。





接下来分别介绍偏向锁、轻量级锁、自旋锁、和重量级锁:

偏向锁

偏向锁的意思就是假定对象锁偏向于某一个线程,即假设只有一个线程会用到这个锁,偏向锁的目标是,减少无竞争且只有一个线程使用锁的情况下,使用轻量级锁产生的性能消耗。轻量级锁每次申请、释放锁都至少需要一次CAS,但偏向锁只有初始化时CAS,当同一个线程获取该锁时不需要再进行CAS。

过程:在最开始属于无锁状态时,MarkWord里的偏向锁标识位为0,当某一线程申请锁时,检测到标识位为0(可偏向状态),则直接进行CAS操作获取该锁,获取到后就将标识位标为1,并在MarkWord中记录该线程的id。以后当再有线程获取该锁时,检测到标识位为1(已偏向状态),代表该锁已经偏向某一线程,此时会就会比较markword中的id与申请锁的这个线程id是否一致,如果一致就直接进入同步代码块,这种情况不需要CAS操作。若id与申请锁的id不一致,则代表有多个线程申请这个锁了,就会进行撤销偏向锁,然后升级为轻量级锁。


偏向锁的撤销(Revoke)

偏向锁的撤销(Revoke) 操作并不是将对象恢复到无锁可偏向的状态, 而是在偏向锁的获取过程中, 发生了竞争时, 直接将一个被偏向的对象“升级到” 被加了轻量级锁的状态(也有可能是变成不可偏的无锁状态)。 这个操作的具体完成方式如下:
在偏向锁 CAS 更新操作失败以后, 等待到达全局安全点((Safe Point), 此时间点所有的工作线程都停止了字节码的执行。)。
通过 MarkWord 中已经存在的 Thread Id 找到成功获取了偏向锁的那个线程, 然后在该线程的栈帧(线程独有)中补充上轻量级加锁时, 会保存的锁记录(Lock Record), 然后将被获取了偏向锁对象的 MarkWord 更新为指向这条锁记录的指针(此时markword由偏向锁状态变为轻量级锁状态)。至此, 锁撤销操作完成, 阻塞在安全点的线程可以继续执行。

偏向锁的批量再偏向(Bulk Rebias)机制

别的锁在执行完同步代码块后, 都会有释放锁的操作, 而偏向锁并没有直观意义上的“释放锁”操作。但当他执行完了同步代码后,其实是可以重新偏向别的线程的,这主要和偏向锁对象的MarkWord中的时间戳(epoch)有关,可以通过更新时间戳来进行再偏向。具体细节没过多了解,详情自己查。

轻量级锁

前面说当有多个线程来获取锁的时候偏向锁就会膨胀为轻量级锁。所以说轻量级锁的含义就是:当锁不止被一个线程用时,就会有轻量级锁,轻量级锁每次都会进行CAS来确定锁是否有别的线程来使用,如果当前锁没有被其他线程获取,则线程可以拿到锁,如果CAS失败,说明锁被别的线程拿去了,此时会进行适量的自旋(自旋锁下面介绍),如果自旋次数达到了某个数量还是不能获取到锁,就会膨胀为重量级锁。

过程:在当前线程的栈桢(Stack Frame)中创建用于存储锁记录(lock record)的空间,并将对象头中的Mark Word复制到此锁记录中,官方称为Displaced Mark Word。然后线程尝试使用 CAS 操作将对象头中的 Mark Word 替换为指向锁记录的指针。如果成功,当前线程获得锁。如果失败,表示该对象已经被加锁了, 先进行自旋操作, 再次尝试 CAS 争抢, 如果仍未争抢到, 则进一步升级锁至重量级锁。

自旋锁与自适应自旋【注:多核处理器才有意义,且会消耗CPU资源】

Java的线程是映射到操作系统的原生线程之上的,如果要阻塞或唤醒一个线程,都需要操作系统来帮忙完成,这就需要从用户态转换到核心态中,因此状态转换需要耗费很多的处理器时间,对于代码简单的同步块(如被synchronized修饰的getter()和setter()方法),状态转换消耗的时间有可能比用户代码执行的时间还要长。

虚拟机的开发团队注意到在许多应用上,共享数据的锁定状态只会持续很短的一段时间,为了这段时间去挂起和恢复线程并不值得。如果物理机器有一个以上的处理器,能让两个或以上的线程同时并行执行,我们就可以让后面请求锁的那个线程“稍等一下“,在等待时同时进行CAS,但不放弃处理器的执行时间,看看持有锁的线程是否很快就会释放锁。为了让线程等待,我们只需让线程执行一个忙循环(自旋),这项技术就是所谓的自旋锁。

自旋锁在JDK1.4.2中引入,使用-XX:+UseSpinning来开启。JDK1.6中已经变为默认开。自旋等待不能代替阻塞。自旋等待本身虽然避免了线程切换的开销,但它是要占用处理器时间的,因此,如果锁被占用的时间很短,自旋等待的效果就会非常好,反之,如果锁被占用的时间很长,那么自旋的线程只会浪费处理器资源。因此,自旋等待的时间必须要有一定的限度,如果自旋超过了限定次数(默认是10次,可以使用-XX:PreBlockSpin来更改)没有成功获得锁,就应当使用传统的方式去挂起线程了。

JDK1.6中引入自适应的自旋锁,自适应意味着自旋的时间不在固定。而是有虚拟机对程序锁的监控与预测来设置自旋的次数。虚拟机可根据上一次自旋时锁是否被获取来增加或减少自旋时间,如果获取几率大,自旋时间就长,如果基本没被获取,就有可能不自旋,直接阻塞线程了。

自旋是在轻量级锁中使用的。


重量级锁【synchronized是JVM内存级别的,编译后插入指令,通过具有排他性来实现加锁】

jdk1.6前的synchronized就完全就重量级锁,开销巨大。重量级锁通过对象内部的监视器(monitor)实现,其中monitor的本质是依赖于底层操作系统的Mutex Lock实现,操作系统实现线程之间的切换需要从用户态到内核态的切换,切换成本非常高。
轻量级锁是多个线程争取锁,但是不并行(允许少量并行,此时会进行自旋),如果并行,也就是同一时间争取锁的线程太多了,那轻量级锁就会膨胀为重量级锁。重量级锁使除了拥有锁的线程以外的线程都阻塞,防止CPU空转。

原理:
① synchronized 同步语句块的情况

JVM 是通过进入、退出对象监视器( Monitor )来实现对方法、同步块的同步的。

具体实现是在编译之后在同步方法调用前加入一个 monitorenter 指令,在退出方法和异常处插入 monitorexit 的指令。

其本质就是对一个对象监视器( Monitor )进行获取,而这个获取过程具有排他性从而达到了同一时刻只能一个线程访问的目的。

而对于没有获取到锁的线程将会阻塞到方法入口处,直到获取锁的线程 monitor.exit 之后才能尝试继续获取锁。

② synchronized 修饰方法的的情况
synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。


各种锁的优缺点:





wait和notify的作用
调用wait方法,首先会获取监视器锁,获得成功以后,会让当前线程进入等待状态进入等待队列并且释放锁
当其他线程调用notify后,会选择从等待队列中唤醒任意一个线程,而执行完notify方法以后,并不会立马唤醒线程,原因是当前的线程仍然持有这把锁,处于等待状态的线程无法获得锁。必须要等到当前的线程执行完按monitorexit指令以后,也就是锁被释放以后,处于等待队列中的线程就可以开始竞争锁了。

wait和notify为什么需要在synchronized里面?
wait方法的语义有两个,一个是释放当前的对象锁、另一个是使得当前线程进入阻塞队列,而这些操作都和监视器是相关的,所以wait必须要获得一个监视器锁。
而对于notify来说也是一样,它是唤醒一个线程,既然要去唤醒,首先得知道它在哪里,所以就必须要找到这个对象获取到这个对象的锁,然后到这个锁的等待队列中去唤醒一个线程。




Lock

Lock是java中锁操作接口,是JDK提供的,比synchronized使用上面更为灵活。其主要实现类分为ReentrantLock (重入锁)和ReentrantReadWriteLock(读写锁)。其中ReentrantLock(重入锁)构造时,由于布尔参数不同又分为公平重入锁和非公平重入锁,其中非公平的重入锁处理效率比公平重入锁高,所以在创建时,一般使用ReentrantLock(false)。 另一个ReentrantReadWriteLock专门用于对读写操作的加锁(两个读线程不会冲突,两个写线程会冲突,一个读一个写线程会冲突,但是两个读线程不会冲突)。


下面介绍一下几个概念:
公平锁:按照先来后到的顺序,先来的先拿到锁。就是公平。
可重入锁:synchronized与Lock都是可重入锁,意思是当你的锁中的代码块又引用了这个锁,可以直接直接用同一把钥匙开,而不用等锁释放了再获取锁。如果不可重入,那就会出现死锁。
读写锁:对资源读取和写入的时候拆分为2部分处理,读的时候可以多线程一起读,写的时候必须同步地写

Lock与synchronized的区别

如何选择:因为synchronized已经做了优化,所以和lock性能差别不大,而且synchronized语义清晰,大部分情况还是用它,除非要用到lock的特性,比如,超时获取锁、可以被中断获取锁(synchronized的同步是不能中断的)、等待唤醒机制的多个条件变量(Condition)等,因此当我们确实需要使用到这些功能是,可以选择ReentrantLock。

类别 synchronized Lock
存在层次 Java的关键字,在jvm层面上 是一个类
锁的释放 1、以获取锁的线程执行完同步代码,释放锁 2、线程执行发生异常,jvm会让线程释放锁 在finally中必须手动释放锁,不然容易造成线程死锁
锁的获取 假设A线程获得锁,B线程等待。如果A线程阻塞,B线程会一直等待 分情况而定,Lock有多个锁获取的方式,具体下面会说道,大致就是可以尝试获得锁,线程可以不用一直等待
锁状态 无法判断是否获取锁的状态 可以判断是否获取到锁
锁类型 可重入 不可中断 非公平 可重入 可中断 可公平(两者皆可)
性能 适合代码少量的同步问题 适合大量同步的代码的同步问题
线程通讯 直接使用wait、notify、notifyAll等方法 需要配合Condition类,通过调用它的await、signal、signalAll方法实现相同的功能,不同的是Condition可以有多个,而synchronized中的做法类似于只有一个Condition,所有类都调用同一个Condition,Lock中的用法更灵活。

Lock的主要方法:

  • lock():获取锁,如果锁被暂用则一直等待

  • unlock():释放锁

  • tryLock(): 注意返回类型是boolean,如果获取锁的时候锁被占用就返回false,否则返回true

  • tryLock(long time, TimeUnit unit):比起tryLock()就是给了一个时间期限,保证等待参数时间

  • lockInterruptibly():用该锁的获得方式,如果线程在获取锁的阶段进入了等待,那么可以中断此线程,先去做别的事

  • getHoldCount(),获得当前线程保持此锁的个数,即调用lock()的次数。未调用lock()时,为0,调用lock(),为1,调用unlock(),为0。
  • getQueueLength,获得等待该锁的线程个数。假设有5个线程都start(),线程1执行了lock(),一直未执行unlock(),此时其他四个线程都在等着线程1释放该锁,getQueueLength将会返回4。
  • hasQueuedThread(Thread thread),查询指定线程是否在等待该锁
  • hasQueuedThreads(),查询是否有线程在等待该锁
  • isFair(),判断该锁是否为公平锁
  • isHeldByCurrentThread(),查询当前线程是否持有该锁
  • isLocked(),查询是否有线程持有该锁


  • 下面主要介绍个别点:例子代码比较多,详细例子参考:https://www.jianshu.com/p/ab3e83609f8f

    Lock锁通讯:(可以用Locksupport与Condition)

    • synchronized关键字与wait()和notify()/notifyAll()方法相结合可以实现等待/通知机制,ReentrantLock类当然也可以实现,但是需要借助于Condition接口与newCondition() 方法。Condition是JDK1.5之后才有的,它具有很好的灵活性,比如可以实现多路通知功能也就是在一个Lock对象中可以创建多个Condition实例(即对象监视器),线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。 在使用notify()/notifyAll()方法进行通知时,被通知的线程是由 JVM 选择的,用ReentrantLock类结合Condition实例可以实现“选择性通知” ,这个功能非常重要,而且是Condition接口默认提供的。而synchronized关键字就相当于整个Lock对象中只有一个Condition实例,所有的线程都注册在它一个身上。如果执行notifyAll()方法的话就会通知所有处于等待状态的线程这样会造成很大的效率问题,而Condition实例的signalAll()方法 只会唤醒注册在该Condition实例中的所有等待线程。

    Locksupport 与 Condition(简介,具体自己查)

    LockSupport定义了一组的公共静态方法,这些方法提供了最基本的线程阻塞和唤醒功能,而LockSupport也成为构建同步组件的基础工具。
    LockSupport.park()/LockSupport.parkNanos(long nanos),LockSupport.parkUntil(long deadlines), 当前线程进入WAITING/TIMED_WAITING状态。对比wait方法,不需要获得锁就可以让线程进入WAITING/TIMED_WAITING状态,需要通过LockSupport.unpark(Thread thread)唤醒。

    Condition接口也提供了类似Object的监视器(比如wait()、wait(long timeout)、notify()以及notifyAll()方法等)方法,与Lock配合可以实现等待/通知模式,Condition是依赖Lock对象的。通过(代码:Lock lock = new ReentrantLock(); Condition condition = lock.newCondition();)lock锁来获取Condition,如果一个线程调用了Condition.await()方法,那么该线程将会释放锁、构造成节点加入等待队列并进入等待状态。

    线程中断(正在执行锁中的内容能否中断):

    线程的interrupt方法:告诉线程你应该中断了,但具体什么时候中断线程自己决定。

    synchronized锁在执行时无法中断。
    Lock中lockInterruptibly的作用为,如果当前线程未被中断,则获取锁,如果被中断,则抛出异常。



    ReentrantReadWriteLock

    假设有这样一个场景,线程1和线程2都需要读写某一文件,为了保证线程安全,我们可以使用synchronized关键字或ReentrantLock,通过加锁来保证线程安全,即读读互斥,读写互斥,写读互斥,写写互斥。但实际上对于1 2两个线程同时读取的操作,是不会出现线程安全问题的,加锁反而影响效率。我们可以使用ReentrantReadWriteLock,来实现读读共享,读写互斥,写读互斥,写写互斥的效果。

    ReentrantReadWriteLock将读和写拆成两部分,可以分开加锁,调用时会自动判断是共享还是互斥。





    最后再放一个Lock的最简单的例子:
    package test_lock;
    
    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
     
    public class LockTest {
        private Lock lock = new ReentrantLock();
        /*
         * 使用完毕释放后其他线程才能获取锁
         */
        public void lockTest(Thread thread) {
            lock.lock();//获取锁
            try {
                System.out.println("线程"+thread.getName() + "获取当前锁"); //打印当前锁的名称
                Thread.sleep(2000);//为看出执行效果,是线程此处休眠2秒
            } catch (Exception e) {
                System.out.println("线程"+thread.getName() + "发生了异常释放锁");
            }finally {
                System.out.println("线程"+thread.getName() + "执行完毕释放锁");
                lock.unlock(); //释放锁
            }
        }
         
        public static void main(String[] args) {
            LockTest lockTest = new LockTest();
            //声明一个线程 “线程一”
            Thread thread1 = new Thread(new Runnable() {
                @Override
                public void run() {
                    lockTest.lockTest(Thread.currentThread());
                }
            }, "thread1");
            //声明一个线程 “线程二”
            Thread thread2 = new Thread(new Runnable() {
     
                @Override
                public void run() {
                    lockTest.lockTest(Thread.currentThread());
                }
            }, "thread2");
            // 启动2个线程
            thread2.start();
            thread1.start();
     
        }
    }

    输出结果:


    Lock原理    【简单介绍一下,大神分析贴:https://blog.csdn.net/javazejian/article/details/75043422#commentsedit

    AQS(AbstractQueuedSynchronizer抽象类,又称为队列同步器)

    简介:Lock之所以能实现线程安全的锁,主要的核心是AQS抽象类,AQS提供了一个FIFO(先进先出)双向队列,其中链表的结点是node,可以看做是一个用来实现锁以及其他需要同步功能的框架。AQS的使用依靠继承来完成,子类通过继承自AQS并实现所需的方法来管理同步状态。例如常见的ReentrantLock,CountDownLatch等。

    从使用上来说,AQS的功能可以分为两种:独占和共享。
    独占锁模式下,每次只能有一个线程持有锁,ReentrantLock就是以独占方式实现的互斥锁。
    共享锁模式下,允许多个线程同时获取锁,并发访问共享资源,比如ReentrantReadWriteLock。


    AQS如何实现lock原理浅析:

    AbstractQueuedSynchronizer又称为队列同步器(后面简称AQS),它是用来构建锁或其他同步组件的基础框架,内部通过一个int类型的成员变量state来控制同步状态,当state=0时,则说明没有任何线程占有共享资源的锁(即没线程获取到锁),当state=1时,则说明有线程目前正在使用共享变量(即有线程获取到锁)如果是公平锁,直接放到队列尾,如果是非公平锁,先尝试加锁,没有抢到放到队尾,这个队列叫做同步队列,AQS内部通过内部类Node构成FIFO的同步队列来完成线程获取锁的排队工作,同时利用内部类ConditionObject构建等待队列当Condition调用wait()方法后,线程将会加入等待队列中,而当Condition调用signal()方法后,线程将从等待队列转移动到同步队列中进行锁竞争。注意这里涉及到两种队列,一种的同步队列,当线程请求锁而等待的后将加入同步队列等待,而另一种则是等待队列(可有多个),通过Condition调用await()方法释放锁后,将加入等待队列。

    大致队列图如下:


    已就绪的线程队列:线程进入ruannable可运行状态,等待cpu时间片来运行。
    等待锁的队列:同步队列。应该是直接处于线程阻塞状态(此处有点混乱)
    等待条件的队列:等待队列。应该是直接处于线程阻塞状态
    注意:Condition不止有一个,也就是说等待队列可以有多个,而每个锁只有一个同步队列


    运行流程图:


    AQS整体类图结构图:



    以ReentrantLock为例,简单讲解ReentrantLock与AQS的关系


    AbstractOwnableSynchronizer:抽象类,定义了存储独占当前锁的线程和获取的方法

    AbstractQueuedSynchronizer:抽象类,AQS框架核心类,其内部以虚拟队列的方式管理线程的锁获取与锁释放,其中获取锁(tryAcquire方法)和释放锁(tryRelease方法)并没有提供默认实现,需要子类重写这两个方法实现具体逻辑,目的是使开发人员可以自由定义获取锁以及释放锁的方式。

    Node:AbstractQueuedSynchronizer 的内部类,用于构建虚拟队列(链表双向链表),管理需要获取锁的线程。

    Sync:抽象类,是ReentrantLock的内部类,继承自AbstractQueuedSynchronizer,实现了释放锁的操作(tryRelease()方法),并提供了lock抽象方法,由其子类实现。

    NonfairSync:是ReentrantLock的内部类,继承自Sync,非公平锁的实现类。

    FairSync:是ReentrantLock的内部类,继承自Sync,公平锁的实现类。

    ReentrantLock:实现了Lock接口的,其内部类有Sync、NonfairSync、FairSync,在创建时可以根据fair参数决定创建NonfairSync(默认非公平锁)还是FairSync。

    Node中有一个属性waitStatus,上面的图直接列举了四个状态,下面分析一下这四个状态

    CANCELLED,值为1.由于在同步队列中等待的线程等待超时或者被中断,需要从同步队列中取消等待,节点进入该状态将不会变化。

    SIGNAL,值为-1,后继节点的线程处于等待状态,而当前节点的线程如果释放了同步状态或者被取消,将会通知后继节点,使后继节点得以运行。说白了,就是处于唤醒状态,只要前继结点释放锁,就会通知标识为SIGNAL状态的后继结点的线程执行。

    CONDITION,值为-2,节点在等待队列中,节点线程等待在Condition上,当其他线程对Condition调用了signal()方法后,该节点将会从等待队列中转移到同步队列中,加入到对同步状态的获取中。

    PROPAGATE,值为-3,表示下一次共享式同步状态获取将会无条件地被传播下去。与共享模式相关,在共享模式中,该状态标识结点的线程处于可运行状态。

    INITAL,值为0,初始状态。

    如何使用呢?线程获取不到锁后其实不会立刻进入同步队列中,而是有一个判断函数,返回布尔值:

    检查原则在于:

    • 规则1:如果前继的节点状态为SIGNAL,表明当前节点需要unpark,则返回成功,此时acquireQueued方法的第12行(parkAndCheckInterrupt)将导致线程阻塞

    • 规则2:如果前继节点状态为CANCELLED(ws>0),说明前置节点已经被放弃,则回溯到一个非取消的前继节点,返回false,acquireQueued方法的无限循环将递归调用该方法(找到一个ws<0的前置结点),直至规则1返回true,导致线程阻塞

    • 规则3:如果前继节点状态为非SIGNAL、非CANCELLED,则设置前继的状态为SIGNAL,返回false后进入acquireQueued的无限循环,与规则2同

    总体看来,shouldParkAfterFailedAcquire就是靠前继节点判断当前线程是否应该被阻塞,如果前继节点处于CANCELLED状态,则顺便删除这些节点重新构造队列。