Java 锁全集

简介:AQS(AbstractQueuedSynchronizer)抽象式的队列同步器是一个用来构建锁和同步器的框架,核心思想是如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为无锁状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS使用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。AQS是将每一条请求共享资源的线程封装成一个CLH锁队列的一个结点,来实现锁的分配

一、锁分类

  1. 公平锁/非公平锁
    A. 公平锁:指多个线程按照申请锁的顺序来获取锁,即先到先得;

    B. 非公平锁:指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程先获得锁。

  2. 独占锁/共享锁

    A. 独占锁:指该锁一次只能被一个线程占用;

    B. 共享锁:指该锁可以被多个线程所持有。

  3. 互斥锁/读写锁

    A. 互斥锁和读写锁中写锁是独占锁的具体实现;

    B. 读写锁中读锁是共享锁的具体实现;

    C. 互斥锁加锁失败后,线程会释放CPU给其他线程

  4. 可重入锁/不可重入锁

    A. 可重入锁:指同一个线程在外层方法获取锁的时候,在进入内层方法中会自动获取锁;

  5. 乐观锁/悲观锁

    A. 乐观锁:指很乐观,每次去拿数据的时候都会认为别人不会修改,所以不会上锁;

    B. 悲观锁:指总是假设最坏情况,每次去拿数据的时候都会认为别人会修改,所以数据上锁。

 

二、锁优化

  1. 锁消除(Lock Elision):指虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除;

  2. 锁粗化(Lock Coarsening):指一序列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体内的,将会把加锁同步的范围扩展到整个操作系列的外部;

  3. 自旋锁:是指当一个线程尝试获取某个锁时,如果该锁已经被其他线程占用,就一直循环检测锁是否被释放,而不是进入线程挂起或休眠状态,即自旋锁加锁失败后,线程会忙等待,直到拿到锁

  4. 分段锁:是将数据分段上锁,把锁进一步细粒度化,有助于提升并发效率;

  5. 偏向锁/轻量级锁/重量级锁

    A. 偏向锁:是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径,因为轻量级锁的获取及释放依赖多次CAS原子指令,而偏向锁只需要在置换线程ID的时候依赖一次CAS原子指令,它是在只有一个线程执行同步块时来进一步提高性能;

    B. 轻量级锁(自旋锁):是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能;

    C. 重量级锁:是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁升级为重量级锁;

    D. 偏向锁、轻量级锁的状态转化及对象Mark Word的关系如下图

 

   E. 为什么有自旋锁还需要重量级锁:自旋是消耗CPU资源的,如果锁的时间长,或者自旋线程多,CPU会被大量消耗,而重量级锁有等待队列,所有拿不到锁的进入等待队列,不需要消耗CPU资源;

   F. 偏向锁是否一定比自旋锁效率高:不一定,在明确知道会有多线程竞争的情况下,偏向锁肯定会涉及锁撤销,这时候直接使用自旋锁,如JVM启动过程就是不打开偏向锁,因为多线程不停偏向及撤销消耗资源,效率低;

 

三、 公平锁/非公平锁

  1. 特点

    A. 公平锁在并发环境下,每个线程在获取锁的时候都要先看锁的等待队列是否为空,若为空就可以获取锁,否则等待;

    B. 非公平锁很随机,线程直接尝试获取锁,获取失败就采用类似公平锁的方式,非公平锁的优点在于吞吐量比公平锁大

  2. 举例

    A. ReentrantLock默认是非公平锁,可以通过构造函数改为公平锁,原因在于ReentrantLock是通过AQS来实现线程调度;

     B. Synchronized是非公平锁,它没法变成公平锁。

 

 四、独占锁(排他锁)/共享锁

  1. 特点

    A. 独占锁和共享锁都是通过AQS来实现的。

  2. 举例

    A. Synchronized和ReentrantLock都是是独占锁;

    B. ReadWriteLock中读锁是共享锁,写锁是独占锁,读锁可保证并发读是非常高效的,读写、写写、写读过程是互斥的。

 

五、 互斥锁/读写锁

  1. 举例

    A. Synchronized和ReentrantLock是互斥锁;

    B. ReadWriteLock是读写锁。

  2. 读写锁ReadWriteLock

    A. ReadWriteLock的实现类为ReentrantReadWriteLock,通过锁的分离,实现更加精确的控制,使得并发性提高,多线程下尽量加锁,应用在读写分离的业务中,将只读的业务加读锁,提供效率;

    B. ReetrantReadWriteLock特性

      获取锁顺序:默认非公平锁(无序),可以指定为公平锁(有序);

      可重入:拥有可重入特性;

      锁降级:锁降级是指从写锁变成读锁,具体是指当前拥有写锁,再获取读锁,随后释放写锁的过程,锁升级是指从读锁变成写锁。

    C. ReetrantReadWriteLock对比

      读写锁的效率高于Synchronized关键字;

      读锁(readLock方法)是共享锁,写锁(writeLock方法)是独占锁,二者都需要配对使用;

      读写锁互斥:当有读锁时,写锁就不能获得;而当有写锁时,除了获得写锁的这个线程外,其他线程不能获得读锁;

        ReetrantReadWriteLock代码演示

 

 

六、可重入锁(递归锁)

  1. 特点

    A. 可重入锁最大的好处就是避免死锁

  2. 举例

    A. ReentrantLock和Synchronized都是可重入锁,前者更灵活方便;

    B. Synchronized代码演示

     C. ReentrantLock代码演示

   3. 方法解释

    A. 锁加了多少次,释放锁需要对应次数,且释放锁必须放在finally中,避免异常没有释放;

 

七、乐观锁/悲观锁

  注意:该锁是人为定义的概念,主要站在并发同步角度区分的

  1. 特点

    A. 乐观锁是在更新的时候会判断在此期间有没有人去更新这个数据,适合读操作;

    B. 悲观锁是在别人去拿数据就会阻塞,直到他拿到锁,适合写操作。

  2. 实现机制

    A. 乐观锁可采用数据版本机制,其实现体现在数据库加上版本号或时间戳,或采用CAS操作;

    B. 悲观锁可采用独占锁机制。

  3. 举例

    A. 乐观锁就是无锁编程,悲观锁就是使用各种锁;

    B. Synchronized就是悲观锁的体现。

 

八、偏向锁

  1. 偏向锁的获取

    A. 先访问目标对象的MarkWord中偏向锁的标识是否设置成1,锁标志位是否为01,如果是就代表处于可偏向状态;

    B. 如果为可偏向状态,则检测MarkWord中存储的线程ID是否等于当前的线程ID,如果是就代表本线程已经获取到偏向锁,直接执行步骤E,否则就代表该对象目前偏向于其他线程,接着执行步骤C;

    C. 如果线程ID并未指向当前线程,则通过CAS操作竞争锁,如果竞争成功,则将MarkWord中线程ID设置为当前线程ID,直接执行步骤E,如果竞争失败,接着执行步骤D;

    D. 如果CAS获取偏向锁失败,则说明有另一个线程抢先获取到了偏向锁,那么当到达全局安全点(safepoint)时获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码;

    E. 执行同步代码,注意线程在执行完同步代码块以后,并不会尝试将MarkWor 中的线程ID赋回原值,目的是如果该线程需要再次对这个对象加锁,而这个对象之前一直没有被其他线程尝试获取过锁,依旧停留在可偏向的状态下, 即可在不修改对象头的情况下,直接认为偏向成功。

  2. 偏向锁的撤销

    A. 偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,否则线程不会主动去释放偏向锁;

    B. 偏向锁的撤销,需要等待全局安全点(在这个时间点上所有线程都停止了字节码的执行),它会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态,撤销偏向锁后恢复到未锁定(标志位为“01”)或轻量级锁(标志位为“00”)的状态;

  3. 其他

    A. 优点:加锁和解锁不需要额外的消耗,和执行非同步方法比仅存在纳秒级的差距;

    B. 缺点:如果线程间存在锁竞争,会带来额外的锁撤销的消耗;

    C. 使用场景:适用于只有一个线程访问同步块场景;

    D. 偏向锁参数

      关闭:-XX:-UseBiasedLocking;

      开启:-XX:+UseBiasedLocking,这是JVM的默认值。

 

九、轻量级锁

  1. 轻量级锁的加锁

    A. 先根据标志位判断出对象状态处于不可偏向的无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”),然后在当前线程的栈帧中创建用于存储锁记录(Lock Record)的空间,并将对象头中的Mark Word拷贝到锁记录中;

    B. 拷贝对象头中的Mark Word复制到锁记录中,拷贝成功后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock record里的owner指针指向object mark word;

    C. 如果更新成功,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态;

    D. 如果更新失败,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行,否则说明多个线程竞争锁,轻量级锁就要升级为重量级锁,锁标志的状态值变为“10”,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态, 而当前线程便尝试使用自旋来获取锁,自旋就是为了不让线程阻塞,而采用循环去获取锁的过程。

  2. 轻量级锁的解锁

    A. 通过CAS操作尝试把线程中拷贝的Displaced Mark Word对象替换当前的Mark Word;

    B. 如果替换成功,整个同步过程就完成了;

    C. 如果替换失败,说明有其他线程尝试过获取该锁,那就要在释放锁的同时,唤醒被挂起的线程。

  3. 其他

    A. 优点:在没有多线程竞争的前提下,减少传统的重量级锁使用产生的性能消耗,竞争的线程不会阻塞,提高了程序的响应速度;

    B. 缺点:如果始终得不到锁竞争的线程,使用自旋会消耗CPU;

    C. 使用场景:线程交替执行同步块的情况。

 

十、其他

  1. 环形缓冲区RingBuffer

    A. 实现原理:环形缓冲区设置了一个读指针head和一个写指针tail,读指针指向环形缓冲区中下一次读的位置,写指针指向环形缓冲区中下一次写的位置;

          通过对head和tail指针的移动,可以实现数据在数组中的环形存取,当head==tail时,说明buffer为空,当head==(tail+1)%bufferSize则说明buffer满了;

          在读操作时,先读取tail值并赋值给copyTail,赋值是原子操作的,当读到copyTail之后,由于head到copyTail之间一定是有数据可以读的,就不会出现数据没有写入就进行读操作的情况,读操作完成后才修改head的值;

          在写操作时,先读取head的值判断是否有空间可以用来写数据,这时tail到head - 1之间一定是有空间可以写数据的,故不会出现一个位置的数据还没有读出就被写操作覆盖的情况,当内容写入到buffer之后才修改tail的值。

    B. 特点:采用数组存储,访问速度比链表快,由于数组内存地址是连续的,对于CPU缓存而言很友好,数组元素会被预加载的;

        可以为数组预先分配内存,使对象一直存在,避免程序花大量时间用于垃圾回收。

  2. 高性能无锁并发框架Disruptor

 

可参考:Java中21种锁

    JUC锁:ReentrantLock详解

    JUC锁:ReentrantReadWriteLock详解

posted @ 2020-02-11 09:45  如幻行云  阅读(191)  评论(0编辑  收藏  举报