ReentrantLock实现原理

1.关系图

   

 

 对象说明:AbstractQueuedSynchronizer(简称AQS)--队列同步器

                   AbstractOwnableSynchronizer(AOS),AOS主要是保存获取当前锁的线程对象

                   FairSync 与 NonfairSync的区别在于,是不是保证获取锁的公平性,默认是NonfairSync(非公平性

 

2.锁的存储结构(基于AQS)--双向链表+int类型状态

   

 

  

 

   注:变量都被"transientvolatile修饰

   

 

 

 3.介绍下ReentrantLock功能

   ReenTrantLock的实现是一种自旋锁,通过循环调用CAS操作来实现加锁。

   它的性能比较好也是因为避免了使线程进入内核态的阻塞状态。

    

4.获取锁(过程以上而下)

   Lock lock = new ReentrantLock();
   lock.lock();

   sync.lock();

   --默认走非公平性(NonfairSync)

   

 

     说明:setExclusiveOwnerThread(Thread.currentThread());--是将当前线程放到锁属性里面

     

 5.等待获取锁(acquire(1))

    

 

    

 

 

   三个方法的含义:

      tryAcquire:会尝试再次通过CAS获取一次锁。

      addWaiter:将当前线程加入上面锁的双向链表(等待队列)中

       acquireQueued:通过自旋,判断当前队列节点是否可以获取锁。

 

6.tryAcquire源码

   

 

 

7.addWaiter() 添加当前线程到等待链表中

   

 

 

 

8.acquireQueued() 自旋+CAS尝试获取锁

   

 

    线程自旋+CAS图解

    

 

     获取锁图解

     

 

 

  

9.释放锁(过程自上而下)

    lock.unlock();

    sync.release(1);

    

 

    

     尝试唤醒等待线程:

           在acquireQueued代码中只用红框圈出了上面一部分,

           这部分执行的结果是会在第二次for循环进来的时候park,即让出cpu,等待被唤醒。

            

 

 

           配合看unlock的代码就会发现释放锁时会执行unparksuccessor,

           用unpark唤醒队列head的下一个节点(head是个辅助节点),

           让这个节点再次进入图中圈出的for循环自旋拿锁,

           当然这个线程此时的竞争者是刚好这个时间点来抢锁的其他线程,这也是不公平锁的体现。

10.总结

    lock的存储结构:一个int类型状态值(用于锁的状态变更),一个双向链表(用于存储等待中的线程)

    lock获取锁的过程:本质上是通过CAS来获取状态值修改,如果当场没获取到,会将该线程放在线程等待链表中。

    lock释放锁的过程:修改状态值,调整等待链表。

 

11.分析

    可以看到在整个实现过程中,lock大量使用CAS+自旋。

    因此根据CAS特性,lock建议使用在低锁冲突的情况下。

    目前java1.6以后,官方对synchronized做了大量的锁优化(偏向锁、自旋、轻量级锁)。

    因此在非必要的情况下,建议使用synchronized做同步操作。

 

12.锁实现

     简单说来,AbstractQueuedSynchronizer会把所有的请求线程构成一个CLH队列,

     当一个线程执行完毕(lock.unlock())时会激活自己的后继节点,

     但正在执行的线程并不在队列中,而那些等待执行的线程全 部处于阻塞状态

     经过调查线程的显式阻塞是通过调用LockSupport.park()完成,

     而LockSupport.park()则调用 sun.misc.Unsafe.park()本地方法,

     再进一步,HotSpot在Linux中中通过调用pthread_mutex_lock函数

     把 线程交给系统内核进行阻塞。

     与synchronized相同的是,这也是一个虚拟队列,不存在队列实例,

     仅存在节点之间的前后关系。令人疑惑的是为什么采用CLH队列呢?

     原生的CLH队列是用于自旋锁,但Doug Lea把其改造为阻塞锁。

     当有线程竞争锁时,该线程会首先尝试获得锁,

     这对于那些已经在队列中排队的线程来说显得不公平,这也是非公平锁的由来,

     与synchronized实现类似,这样会极大提高吞吐量。

     如果已经存在Running线程,则新的竞争线程会被追加到队尾,

     具体是采用基于CAS的Lock-Free算法,

     因为线程并发对Tail调用CAS可能会 导致其他线程CAS失败,

     解决办法是循环CAS直至成功。

     AQS的实现非常精巧,令人叹为观止,

     不入细节难以完全领会其精髓,下面详细说明实现过程:

     

 

      总结:AbstractQueuedSynchronizer通过构造一个基于阻塞的CLH队列容纳所有的阻塞线程,

                而对该队列的操作均通过Lock-Free(CAS)操作,

                但对已经获得锁的线程而言,ReentrantLock实现了偏向锁的功能。

                synchronized 的底层也是一个基于CAS操作的等待队列,

                但JVM实现的更精细,把等待队列分为ContentionList和EntryList,

                目的是为了降低线程的出列速度;

                当然也实现了偏向锁,从数据结构来说二者设计没有本质区别。

                但synchronized还实现了自旋锁,并针对不同的系统和硬件体系进行了优 化,

                而Lock则完全依靠系统阻塞挂起等待线程。

                当然Lock比synchronized更适合在应用层扩展,

                可以继承 AbstractQueuedSynchronizer定义各种实现,

                比如实现读写锁(ReadWriteLock),公平或不公平锁;

                同时,Lock对 应的Condition也比wait/notify要方便的多、灵活的多。

                state值,若为0,意味着此时没有线程获取到资源

         简述总结(非公平):

               第一点:调用lock方法,会先进行cas操作看下可否设置同步状态1成功,如果成功执行临界区代码

               第二点:如果不成功获取同步状态,如果状态是0那么cas设置为1.

               第三点:如果同步状态既不是0也不是自身线程持有会把当前线程构造成一个节点。

               第四点:把当前线程节点CAS的方式放入队列中,行为上线程阻塞,内部自旋获取状态。

                注:acquireQueued的主要作用是把已经追加到队列的线程节点进行阻塞

                        但阻塞前又通过tryAccquire重试是否能获得锁,如果重试成功能则无需阻塞,直接返回。              

               第五点:线程释放锁,唤醒队列第一个节点,参与竞争。重复上述。

   

13.synchronized和lock的底层区别

    第一點: synchronized的底层也是一个基于CAS操作的等待队列,但JVM实现的更精细,

                  把等待队列分为ContentionList和EntryList,

                  目的是为了降低线程的出列速度;

                   当然也实现了偏向锁,从数据结构来说二者设计没有本质区别。

                   但synchronized还实现了自旋锁,并针对不同的系统和硬件体系进行了优化,

                   而Lock则完全依靠系统阻塞挂起等待线程。

      

         第二點:当然Lock比synchronized更适合在应用层扩展,

                        可以继承AbstractQueuedSynchronizer定义各种实现,

                        比如实现读写锁(ReadWriteLock),公平或不公平锁;

                        同时,Lock对应的Condition也比wait/notify要方便的多、灵活的多。

         第三點: ReentrantLock是一个可重入的互斥锁,

                        ReentrantLock由最近成功获取锁,还没有释放的线程所拥有:

                        ReentrantLock的lock机制有2种,忽略中断锁和响应中断锁

                        synchronized实现的锁机制是可重入的,主要区别是中断控制和竞争锁公平策略

        

 14.synchronized和lock其他区别

      一.首先synchronized是java内置关键字,在jvm层面,Lock是个java类; 

     二.synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁;

     三.synchronized会自动释放锁(a 线程执行完同步代码会释放锁 ;b 线程执行过程中发生异常会释放锁),

         Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁;

     四.用synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2线程等待。

          如果线程1阻塞,线程2则会一直等待下去,而Lock锁就不一定会等待下去,

          如果尝试获取不到锁,线程可以不用一直等待就结束了;

     五.synchronized的锁可重入、不可中断、非公平,而Lock锁可重入、

          可判断、可公平(两者皆可)

     六.Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题。

 

15.synchronized底层实现

     synchronized 属于重量级锁,效率低下,

     因为监视器锁(monitor)是依赖于底层操作系统的 Mutex Lock 来实现的,

     而操作系统实现线程之间的切换时需要从用户态转换到核心态,

     这个状态之间的转换需要相对比较长的时间,时间成本相对较高,

     这也是为什么早期的 synchronized 效率低的原因。

     庆幸的是在 Java 6 之后 Java 官方从 JVM 层面对 synchronized 进行了较大优化,

     所以现在的 synchronized 锁效率也优化得很不错了。

     Java 6 之后,为了减少获得锁和释放锁所带来的性能消耗,引入了轻量级锁和偏向锁

 

16.Lock底层实现

     Lock底层实现基于AQS实现,采用线程独占的方式,

     在硬件层面依赖特殊的CPU指令(CAS)。

     简单来说,ReenTrantLock的实现是一种自旋锁

     通过循环调用CAS操作来实现加锁。

     它的性能比较好也是因为避免了使线程进入内核态的阻塞状态。

     想尽办法避免线程进入内核的阻塞状态是我们去分析和理解锁设计的关键钥匙。

 

17.volatile底层实现

     在JVM底层volatile是采用“内存屏障”来实现的。

 

18.lock和Monitor的区别

    lock的底层本身是Monitor来实现的,所以Monitor可以实现lock的所有功能。

    Monitor有TryEnter的功能,可以防止出现死锁的问题,lock没有。

 

 

学习来源:https://blog.csdn.net/qq_29373285/article/details/85964460

     

         

                          

posted @ 2020-09-04 09:37  小窝蜗  阅读(803)  评论(1)    收藏  举报