Java-并发-关于锁的一切

版权声明:欢迎转载,请注明作者和出处 https://blog.csdn.net/baichoufei90/article/details/85004307

Java-并发-关于锁的一切

摘要

本文简要说下Java中的各种锁和类锁机制,还有一些相关的如sleep/yield join等,分析其实现原理,做简单比较。

请点击右侧目录,挑选感兴趣的章节观看。

注意:最近发现本文所讲偏向锁和轻量级锁的代码分析章节有误,请大家移驾参阅死磕Synchronized底层实现–概论系列文章,查看源码分析。待后续有时间我会改正本文内容。

0x01 Thread相关方法

Thread 相关方法,是锁和类锁代码中大量使用的一些基本方法。第一张简单提一下。

1.1 sleep

1.1.1 基本概念

  • sleep方法如其名,就是让线程休息下,直到指定时间耗尽。
  • 最大的特点就是阻塞过程中,不释放线程拥有的对象锁(ObjectMonitor)。
  • sleep过程,会让出CPU时间片给其他线程执行。
  • 底层使用linux系统的pthread_cond_timedwait方法实现。
  • sleep方法可被中断

1.1.2 实现原理

请点击这里

1.1.3 Sleep对比Wait

  • wait会释放ObjectMonitor控制权;sleep不会
  • wait逻辑复杂,需要首先调用synchronized获取ObjectMonitor控制权,才能调用wait,且wait后还有放入WaitSet逻辑,唤醒时还有一系列复杂操作;而sleep实现简单,不需要别的线程唤醒
  • wait与sleep都能被中断(除了sleep(0),当然对他中断没有意义)

1.2 yield

1.2.1 基本概念

  • 该方法是给调度器一个提示,当前线程愿意放弃占有的CPU使用权。但注意,调度器可以忽略该提示。
  • yield只是一个探索式的尝试,期望改善多线程场景下某些线程过度使用CPU的情况。该方法的使用应经过长期性能测试,以确保它实际上具有所需的效果。
  • 用户编码中很少能正确使用该方法。因为可能在调试或测试的时候能达到预期,但在生产环境高并发环境下有可能导致bug!
  • 该方法在jdk的如JUC并发包内被用设计来做并发控制

1.2.2 实现原理

请点击这里

1.3 join

1.3.1 基本概念

join方法主要用来等待其他线程运行结束,再继续运行自己的线程代码。

1.3.2 实现原理

请点击这里

0x02 LockSupport

2.1 基本概念

  • LockSupport是用来创建锁和其他同步类的基本线程阻塞原语。
  • LockSupport中有一个许可的概念
    • 当调用park()方法时,如果拥有许可就立刻返回;否则也许会阻塞
    • 调用unpark()会使得本来不可用的许可变为可用状态,解除线程阻塞
    • parkNanos可指定超时时长、parkUntil可指定截止时间戳
  • java.util.concurrent.Semaphore中许可的概念不同,LockSupport的许可每个线程最多能拥有1个
  • LockSupport的线程park可因中断、timeout或unpark甚至是毫无理由的返回,所以一般是通过循环检查附加条件是否满足
  • LockSupport的park行为可被中断,但不会抛出InterruptedException。此时可通过interrupted(会清除中断标记位)或isInterrupted方法判断是否发生中断
  • LockSupport是通过调用Unsafe函数中的UNSAFE.parkUNSAFE.unpark实现阻塞和解除阻塞的。

2.2 实现原理

请查阅源码解读:Java-并发-锁-LockSupport

2.3 LockSupport和wait/notify区别

  • LockSupport中的阻塞和唤醒操作是直接作用于Thread对象的,更符合我们队线程阻塞这个语义的理解,使用起来也更方便;
  • 而wait/notify的调用是面向Object的,线程的阻塞/唤醒对Thread本身来说是被动的。而且notify是随机唤醒的,无法精确地控制唤醒的线程以及唤醒的时机。代码上来说也很麻烦,稍不注意就会写错。

2.4 例子

2.4.1 普通使用例子

import java.util.concurrent.locks.LockSupport;

public class LockParkDemo1 {
    private static Thread mainThread;
    public static void main(String[] args) {
        InnerThread it =  new LockParkDemo1().new InnerThread();
        Thread td = new Thread(it);
        mainThread = Thread.currentThread();
        System.out.println(Thread.currentThread().getName() + " start it");
        td.start();
        System.out.println(Thread.currentThread().getName() + " block");
//        LockSupport.park(Thread.currentThread());
        LockSupport.park();
        System.out.println(Thread.currentThread().getName() + " continue");

    }
    class InnerThread implements Runnable{
        @Override
        public void run() {
            int count = 5;
            while(count>0){
                System.out.println("count=" + count);
                count--;
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName()+" wakup others");
            LockSupport.unpark(mainThread);
        }
    }
}

程序输出结果如下:

main start it
main block
Thread-0 wakup others
main continue

2.4.2 Blocker及调试例子

  1. 代码很简单,如下:
import java.util.concurrent.locks.LockSupport;

/**
 * Created by chengc on 2018/12/15.
 */
public class BlockerTest
{
    public static void main(String[] args)
    {
        Thread.currentThread().setName("Messi");
        LockSupport.park("YangGuang");
    }
}
  1. jps查看该进程pid:
$ jps
73900 BlockerTest
  1. jstack:
$ jstack -l 73900
"Messi" #1 prio=5 os_prio=31 tid=0x00007fe34c822000 nid=0x1b03 waiting on condition [0x0000700006470000]
   java.lang.Thread.State: WAITING (parking)
	at sun.misc.Unsafe.park(Native Method)
	- parking to wait for  <0x000000076ac8fcc0> (a java.lang.String)
	at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
	at demos.concurrent.lock.park.BlockerTest.main(BlockerTest.java:13)

   Locked ownable synchronizers:
	- None

可以看到我们的主线程Messi处于WAITING状态,而且原因是parkingBlocker对象时个java.lang.String

2.5 应用

AQS(AbstractQueuedSynchronizer)就是利用了LockSupport的相关方法来控制线程阻塞或者唤醒。

public final void awaitUninterruptibly() {
            Node node = addConditionWaiter();
            int savedState = fullyRelease(node);
            boolean interrupted = false;
            while (!isOnSyncQueue(node)) {
                LockSupport.park(this);
                if (Thread.interrupted())
                    interrupted = true;
            }
            if (acquireQueued(node, savedState) || interrupted)
                selfInterrupt();
        }

0x03 synchronized

3.1 基本概念

  • 每次只能有一个线程进入临界区
  • 保证临界区内共享变量的可见性和有序性
  • 成功进入synchronized区域的线程可以拿到对象的Object-Monitor。具体有3种用法,作用域不同,在后面例子中介绍。
  • 对于拿到锁的线程来说,同一个对象的synchronized具有可重入性
  • 不要将可变对象作为synchronized
  • 如果相互等待对方的synchronized 对象,可能出现死锁
  • synchronized锁是非公平的

3.2 实现原理

关于synchronized的实现原理可以查看这篇文章: Java-并发-锁-synchronized

3.3 ReentrantLock对比synchronized

ReentrantLock和synchronized对比如下:

可重入 等待可中断 公平性 绑定对象数 性能优化
synchronized 支持 不支持 非公平 只能1个 较多
ReentrantLock 支持 支持 非公平/公平 可以多个 -

0x04 锁优化

4.1 基本概念

4.1.1 什么是锁优化

JDK 1.6开始HotSpot虚拟机团队花了很多精力实现各种锁的优化技术,主要目的很明显就是为了更少的阻塞、更少的竞争、更高效的获取锁和释放锁,说白了就是提高多线程需要访问共享区间的执行效率。

4.1.2 锁与对象

JavaHeap中的对象主要包括三部分:

4.1.2.1 实例数据。

这部分是对象真正存储的有效信息,各种类型字段内容,还包括父类继承过来的信息。

4.1.2.2 字节对齐填充

只是占位符,因为HotSpot内存管理要求对象起始地址必须是8字节整数倍,即对象长度必是8字节整数倍。而对象头一般来说已经是整数倍,所以字节填充主要是为实例数据填充。

4.1.2.3 对象头
  1. Mark Word,即对象运行时数据。他的内部字节长度分布与含义非固定,节约空间。存有如hashCode、分代年龄、锁标志信息等。
  2. Klass Pointer,即类型指针(用于确定对象属于的类),指向方法区中的该对象Class类型对象。
  3. 如果对象是数组,还有个数组长度。

下面是一个32位的HotSpot虚拟机中 MarkWord示意图:
对象头
Mark Word中的最后2bit就是锁状态的标志位,用来记录当前对象的锁状态:

状态 标志位 存储内容
未锁定 01 对象哈希码、对象分代年龄
轻量级锁定 00 指向锁记录的指针
膨胀(重量级锁) 10 执行重量级锁定的指针
GC标记 11 空,不需要记录信息
可偏向 01 偏向线程ID、偏向时间戳、对象分代年龄

注意上图中的后方1bit还会在无锁和偏向锁时不同以区分两种锁状态,因为他们的最后2bit锁标志位都是01。

jdk8/hotspot/src/share/vm/oops/markOop.hpp描述了对象头部信息,有兴趣的读者可以看看。

4.2 自旋锁-SpinLock

4.2.1 思想

使用互斥锁的时候,往往阻塞时间其实很短,但线程阻塞和唤醒操作由用户态转为内核态,性能开销大。

这个时候自旋锁产生了,他的思想很朴素,前提是有多于1个CPU:

  1. 线程一请求并获取锁
  2. 线程二请求锁,发现线程一持有锁
  3. 线程二并不放弃CPU进入等待,而是进入空循环即自旋,看是否线程一很快就释放锁

4.2.2 小结

  • 优点
    在总是能较短时间获取锁、线程竞争不激烈时,可仅自旋而不是线程阻塞和唤醒,对性能提升大。
  • 缺点
    自旋锁的问题显而易见,就是等待锁的时候占有CPU资源空跑。可以使用-XX:PreBlockSpin修改自旋次数,默认为10次。

4.3 自适应自旋

这个自适应自旋锁思想也很朴素,相当于基于HBO(历史)的优化:

  • 如果前一次获取锁很快,那本次就允许自旋次数多一些如100,因为JVM认为这一次也能成功获取锁
  • 如果锁很难获取,自旋锁很少成功,那甚至可以直接不自旋,直接阻塞线程进行等待

4.4 锁消除

锁消除,顾名思义,就是JVM在编译器运行时会扫描代码,当检查到那些不可能存在共享区竞争但却有互斥同步的代码,直接将这样的多此一举的锁消除。

除了那些经验不足的编程人员会写无意义的同步代码,还有很多是JVM帮程序加上的,比如以下代码:

public String connectStrs(String str1, String str2, String str3){
    return str1 + str2 + str3;
}

会因为String是不可变类,反复产生新对象,所以被JVM自动优化成以下形式(JDK1.5之前版本,1.5之后是StringBuilder了):

public String connectStrs(String str1, String str2, String str3){
    StringBuffer sb = new StringBuffer;
    sb.append(str1);
    sb.append(str2);
    sb.append(str3);
}

此时,StringBuffer是带锁的了。

锁消除的主要依据是逃逸分析,详见。这里简单说下,就是指代码中的位于JavaHeap的所有数据都不会逃逸导致被其他线程访问,那就将可将他们作为栈内数据,作为线程私有。这样一来同步锁就没有意义了,可以消除。

4.5 锁粗化

这个名字有点诡异,其实说白了就是扩大锁的范围。

什么?不是说好了要尽量减小同步锁的适用范围,缩短占有锁的时间吗?!

其实,JVM是会在反复在段代码中对同一对象加锁的情况进行锁粗化优化的。

比如

public String optimizedConnectStrs(String str1, String str2, String str3){
    StringBuffer sb = new StringBuffer;
    sb.append(str1);
    sb.append(str2);
    sb.append(str3);
}

这种情况每个append都会执行如下代码:

public synchronized StringBuffer append(String str) {
    toStringCache = null;
    super.append(str);
    return this;
}

也就是说会反复对sb这个对象监视器加synchronized同步锁。

此时,JVM就会进行优化,将锁包住多次append操作的起始,只需加锁一次。这就是所谓锁粗化。

4.6 锁升级

4.6.1 基本概念

  • 前面已经提到过Java中4种锁状态,随着锁竞争开始,这几种锁之间有锁升级的关系:
    无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁
  • 锁只能升级不能降级。这么做的原因是缩短锁的获取释放周期,提升效率。

4.6.2 重量级锁

4.6.2.1 基本概念

重量级锁就是前面提到过的传统的基于ObjectMonitor的锁synchronized,底层使用MutexLock。使用这类互斥锁的时候,往往阻塞时间其实很短,但线程阻塞和唤醒操作会有用户态和内核态转换,性能开销大。

4.6.2.2 MutexLock对比SpinLock

上述的monitorLock底层采用MutexLock实现,他和自旋锁SpinLock对比如下:

MutexLock SpinLock
原理 尝试获取锁,若可得到就占有;若不能,就阻塞等待 尝试获取锁,若可得到就占有。若不能,空转并持续尝试直到获取
使用场景 当线程进入阻塞没有很大问题,或需要等待一段足够长的时间才能获取锁 当线程不应该进入睡眠如中断处理等或只需等待非常短的时间就能获取锁
缺点 引起线程切换和线程调度开销大 线程空跑CPU等待,浪费资源

4.6.3 轻量级锁

4.6.3.1 思想

JDK1.6后引入

该轻量级锁的名字是相对于传统的那些锁来说,认为传统同步锁(重量级锁)开销极大,大部分锁其实在同步期间并没有竞争,没必要使用重量级锁导致不必要开销。

轻量级锁加锁过程图:
轻量级锁

  1. 代码进入同步块时,如果该同步对象未被锁定(01)且偏向锁标志位为0,JVM就会在当前线程的栈中建立一个Lock Record空间,他是一个锁对象头的Mark Word的内容拷贝,名为Displaced Mark Word
  2. JVM以CAS(锁对象, MarkWord, DisplacedMarkWord) 即把MarkWord更新为指向复制的Lock Record的指针
  3. 如果第2步成功,就认为该线程拥有这个对象锁。此时将MarkWord最后两bit标记为 00,表示轻量级锁状态。
  4. 如果第2步失败,JVM就检查锁对象的MarkWord是否指向当前线程的栈帧。如果是,就说明当前线程拥有了该对象锁,这是锁重入,可以开始执行同步块内代码;否则说明被其他线程拥有锁就膨胀为重量级锁,此时会标记为10。在膨胀过程中,其他线程全部阻塞等待,而当前线程会使用4.1章节中提到的自旋锁等待膨胀完成,避免阻塞。待膨胀为重量级锁完成后重新竞争同步锁。

轻量级锁膨胀为重量级锁的过程可以在jdk8/hotspot/src/share/vm/runtime/synchronizer.cppObjectMonitor * ATTR ObjectSynchronizer::inflate (Thread * Self, oop object)方法代码中看到,这里不再展开。

解锁过程如下:

  1. CAS(锁对象, DisplacedMarkWord, MarkWord)
  2. 如果第1步成功,同步结束
  3. 第1步失败,说明其他线程尝试过获取该锁。此时不仅要释放锁,同时需要唤醒被挂起等待的线程,锁也要膨胀为重量级锁。
4.6.3.2 小结

轻量级锁的依据是大部分锁在同步期间没有竞争,从而用CAS方式避免了使用互斥量开销。

但如果线程竞争锁激烈的场景,就会额外加上CAS的开销。此时反而效率低于所谓的重量级锁了。

4.6.4 偏向锁

4.6.4.1 思想

JDK1.6后引入

相对于轻量级锁是消除无竞争时用CAS消除同步原语,偏向锁是直接在无竞争时消除所有同步。

当开启了偏向锁配置(-XX:+UserBiasedLocking)时,偏向锁加锁过程如下:

  1. 当锁对象第一次被某个线程获取,JVM就将对象头中的锁标志位设为01,即可偏向模式,同时CAS方式更新偏向锁内容,如将线程ID指向当前线程等信息。
  2. 如果上一步CAS成功,那么持有该偏向锁的线程以后每次获取该锁进入同步块时,检查Mark Word中线程ID是否是当前线程ID。如果是,那么可以直接执行同步块代码,JVM可以不用再进行加解锁、更新偏向信息等同步操作,效率提高很多
  3. 当有别的线程开始获取该锁时,可偏向模式结束,进入安全点(SafePoint)。此时需撤销偏向锁,会导致stop the word暂停拥有偏向锁线程,判断是否处于被锁定状态:
    • 如果此时已锁定,就重设为轻量级锁(00)。
    • 如果无锁,就设为未锁定状态(01)。

偏向锁的释放:
偏向锁释放锁的动作是被动的,如加锁过程中第三步即在其他线程尝试获取竞争偏向锁时才会触发偏向锁释放过程。上面说的安全点指在该时间点上没有代码运行。

4.6.4.2 小结
  • 优点
    在线程竞争少时,偏向锁使得线程仅需在获取锁进入同步块时有JVM一些相关同步操作,后面每次该线程进入同步块都不再需要额外操作(比轻量级锁更轻,不需每次做CAS了),对线程性能提高十分有利。
  • 缺点
    但对于竞争激烈的场景中,偏向锁反而低效,此时可以考虑禁用偏向锁。

4.6.5 锁升级小结

锁升级
初始分配对象时分为开启/不开启偏向锁模式。注意,初始时,偏向锁模式开启,但是拥有锁线程ID为0,代表未锁定。

4.7 锁优化小结

在学习了前面几种类别的锁后,再把synchronized加锁过程串起来讲一下,前提已经打开偏向锁:

  1. 第一次进入的线程获取偏向锁,将ownerId设为自己
  2. 后序进入的该线程都会检查该锁对象ownerID,如果是自己就直接利用偏向锁执行同步块
  3. 后续进入的其他线程检查到锁对象ownerID不是自己,偏向锁模式结束,升级为轻量级锁。复制一份Mark WordDisplaced Mark Word,且CAS(锁对象, MarkWord, DisplacedMarkWord)
  4. 以后每次进入时,CAS前先检查

0x05 wait notify

wait notify 还有个notifyAll都是线程通信的常用手段。

有一个先导概念就是对象锁和类锁,他们其实都是对象监视器Object Monitor,只不过类锁是类对象的监视器,可以看另一篇文章:
Java-并发-锁-synchronized之对象锁和类锁

5.1 基本概念

5.1.1 wait

  • 作用
    顾名思义,wait其实就是线程用来做阻塞等待的。
  • 超时参数
    在JDK的Object中,wait方法分为带参数和无参数版本,这里说的参数就是等待超时的参数。
  • 中断
    其他线程在当前线程执行wait之前或正在wait时,对当前线程调用中断interrupted方法,会抛出InterruptedException,且中断标记会被自动清理。

5.1.2 notify

  • 该方法用来任意唤醒一个在对象锁的等待集的线程(其实看了源码会发现不是任意的,而是一个WaitQueue,FIFO)。
  • 但要注意,被唤醒的线程不会马上开始运行,因为对象锁还被调用notify的线程拥有,直到退出synchronized块。
  • 唤醒后的线程跟其他线程一起竞争该同步对象锁。
  • 注意,该方法和wait方法一样也必须是拥有该对象同步对象锁的线程才能调用,否则抛出IllegalMonitorStateException

5.1.3 notifyAll

  • 该方法用来唤醒所有在对象锁的等待集的线程。
  • 但要注意,被唤醒的线程不会马上开始运行,因为对象锁还被调用notifyAll的线程拥有。
  • 唤醒后的线程跟其他线程一起竞争该同步对象锁。
  • 注意,该方法和wait方法一样也必须是拥有该对象同步对象锁的线程才能调用,否则抛出IllegalMonitorStateException

5.2 实现原理

请参考文档Java-多线程-wait/notify

5.3 wait与sleep比较

经常面试会问这个问题,往往我们都是网上查资料死记硬背。现在我们都看完了源码(sleep源码点这里),可以得出以下结论

  1. wait会释放ObjectMonitor控制权;sleep不会
  2. wait逻辑复杂,需要首先调用synchronized获取ObjectMonitor控制权,才能调用wait,且wait后还有放入WaitSet逻辑,唤醒时还有一系列复杂操作;而sleep实现简单,不需要别的线程唤醒
  3. wait与sleep都能被中断(除了sleep(0),当然对他中断没有意义)

5.4 Condition.await/signal对比wait/notify

5.4.1 Condition和Object关系

等待 唤醒 唤醒全部
Object wait notify notifyAll
Condition await signal signalAll

5.4.2 wait和await对比

中断 超时精确 Deadline
wait 可中断 可为纳秒 不支持
await 支持可中断/不可中断 可为纳秒 支持

5.4.3 notify和signal对比

全部唤醒 唤醒顺序 执行前提 逻辑
notify 支持,notifyAll 随机(jdk写的,其实cpp源码是一个wait_queue,FIFO) 拥有锁 从wait_list取出,放入entry_list,重新竞争锁
signal 支持,signalAll 顺序唤醒 拥有锁 从condition_queue取出,放入wait_queue,重新竞争锁

5.4.4 底层原理对比

  • Object的阻塞和唤醒,前基于synchronized的。底层实现是在cpp级别,调用synchronized的线程对象会放入entry_list,竞争到锁的线程处于active状态。调用wait方法后,线程对象被放入wait_queue。而notify会按FIFO方法从wait_queue中取得一个对象并放回entry_list,这样该线程可以重新竞争synchronized同步锁了。
  • Condition的阻塞唤醒,是基于lock的。lock维护了一个wait_queue,用于存放等待锁的线程。而Condition也维护了一个condition_queue。当拥有锁的线程调用await方法,就会被放入condition_queue;当调用signal方法,会从condition_queue选头一个满足要求的节点移除然后放入wait_queue,重新竞争lock。

5.4.5 应用场景对比

  • Object使用比较单一,只能针对一个条件。
  • 一个ReentrantLock可以有多个Condition,对应不同条件。比如在生产者消费者可以这样实现:
private static ReentrantLock lock = new ReentrantLock();
	
private static Condition notEmpty = lock.newCondition();
private static Condition notFull = lock.newCondition();

// 生产者
public void produce(E item) {
	lock.lock();
	try {
		while(isFull()) {
		// 数据满了,生产者就阻塞,等待消费者消费完后唤醒
			notFull.await();
		}
		
		// ...生产数据代码
		
		// 唤醒消费者线程,告知有数据了,可以消费
		notEmpty.signal();
	} catch (InterruptedException e) {
		e.printStackTrace();
	} finally {
		lock.unlock();
	}
}

// 消费者
public E consume() {
	lock.lock();
	try {
		while(isEmpty()) {
		// 数据空了,消费者就阻塞,等待生产者生产数据后唤醒
			notEmpty.await();
		}
		
		// ...消费数据代码
		
		// 唤醒生产者者线程,告知有数据了,可以消费
		notFull.signal();
		return item;
	} catch (InterruptedException e) {
		e.printStackTrace();
	} finally {
		lock.unlock();
	}
	return null;
}

这样好处就很明显了。如果使用Object,那么唤醒的时候也许就唤醒了同类的角色线程。而使用condition可以在只有一个锁的情况下,实现我们想要的只唤醒对方角色线程的功能。

0x06 CAS

6.1 基本概念

JDK中大量代码使用了CAS,底层是调用的sun.misc.Unsafe,如Unsafe.compareAndSwapInt方法:

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

该方法第一个参数为对象,第二个参数为指定field在对象中的偏移量,第三个为期望值,最后一个是要更新的目标值。

CAS的基本思想就是原子性的执行以下两个操作:

  1. 比较对象中的field当前值是否为期望值
  2. 如果是就更新为指定值,否则不更新

那么,java是怎么实现这个操作的原子性的呢?我们接着往下看

6.2 实现原理

透过前面的代码,可以看到compareAndSwapInt是一个JNI调用。

jdk8/hotspot/src/share/vm/prims/unsafe.cpp中可以找到以下内容:

{CC"compareAndSwapInt",  CC"("OBJ"J""I""I"")Z",      FN_PTR(Unsafe_CompareAndSwapInt)},

UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
  UnsafeWrapper("Unsafe_CompareAndSwapInt");
  oop p = JNIHandles::resolve(obj);
  // 获取该filed内存地址
  jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
  // 调用Atomic.cmpxchg方法
  return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END

鉴于本人能力有限,就不再继续向下了。有兴趣的读者可以研究下jdk8/hotspot/src/share/vm/runtime/atomic.cpp

也可参考文章:

0x07 ReentrantLock

7.1 基本概念

ReentrantLock是使用最广的、最出名的AQS(AbstractQueuedSynchronizer)系列的可重入锁。

它属于是高层API。和synchronized对比如下:

可重入性 等待可中断 公平性 绑定对象数 性能优化
synchronized 支持 不支持 非公平 只能1个 较多
ReentrantLock 支持 支持 非公平/公平 可以多个 -
  • 等待可中断
    获取锁时可以指定一个超时时间,如果超过这个时间还没有拿到锁就放弃等待
  • 公平性
    公平锁就是按线程申请锁时候FIFO的方式获取锁;而非公平锁没有这个规则,所有线程共同竞争,没有先来后到一说
  • 绑定对象
    一个synchronized绑定一个Object用来wait, notify等操作;而ReentrantLock可以newCondition多次等到多个Condition实例,执行await, signal等方法。

7.2 实现原理

限于篇幅,这里可以大概说下其原理。

7.2.1 AQS

AQS全称AbstractQueuedSynchronizer,他是ReentrantLock内部类NonfairSyncFairSync的父类Sync的父类,其核心组件如下:

  1. state,int 类型,用来存储许可数
  2. Node双向链表,存储等待锁的线程

Node就是AQS的内部类,这里可以简单看看Node定义:

static final class Node {
    // 表明等待的节点处于共享锁模式,如Semaphore:addWaiter(Node.SHARED)
    static final Node SHARED = new Node();
    // 表明等待的节点处于排他锁模式,如ReentranLock:addWaiter(Node.EXCLUSIVE)
    static final Node EXCLUSIVE = null;

    // 线程已撤销状态
    static final int CANCELLED =  1;
    // 后继节点需要unpark
    static final int SIGNAL    = -1;
    // 线程wait在condition上
    static final int CONDITION = -2;

    // 使用在共享模式头Node有可能处于这种状态, 表示锁的下一次获取可以无条件传播 
    static final int PROPAGATE = -3;
    
    // 这个waitStatus就是存放以上int状态的变量,默认为0
    // 用volatile修饰保证多线程时的可见性和顺序性
    volatile int waitStatus;
    
    // 指向前一个Node的指针
    volatile Node prev;
    
    // 指向后一个Node的指针
    volatile Node next;
 
    // 指向等待的线程
    volatile Thread thread;

    // condition_queue中使用,指向下一个conditionNode的指针
    Node nextWaiter;

    // 判断是否共享锁模式
    final boolean isShared() {
        return nextWaiter == SHARED;
    }

    // 返回前驱结点,当前驱结点为null时抛出NullPointerException
    final Node predecessor() throws NullPointerException {
        Node p = prev;
        if (p == null)
            throw new NullPointerException();
        else
            return p;
    }

    // 用来初始化wait队列的构造方法;也被用来做共享锁模式
    Node() {
    }

    // 在addWaiter方法时,将指定Thread以指定模式放置
    Node(Thread thread, Node mode) {
        this.nextWaiter = mode;
        this.thread = thread;
    }

    // Condition使用的构造方法
    Node(Thread thread, int waitStatus) { 
        this.waitStatus = waitStatus;
        this.thread = thread;
    }
}    

AQS的Node等待队列双向链表如下图:
LockNodes

7.2.2 非公平锁的实现

默认采用非公平的实现NonFairSync

7.2.2.1 lock()

lock()方法流程如下图:

NonFairSync

可以看到,lock()方法最核心的部分就是可重入获取许可(state),以及拿不到许可时放入一个AQS实现的双向链表中,调用LockSupport.park(this)将自己阻塞。就算阻塞过程被中断唤醒,还是需要去拿锁,直到拿到为止,注意,此时在拿到锁之后还会调用selfInterrupt()方法对自己发起中断请求。

7.2.2.2 unlock()

unlock

7.2.3 公平锁的实现

他的实现和非公平锁有少许区别:

 static final class FairSync extends Sync {
    private static final long serialVersionUID = -3000897897090466540L;

    final void lock() {
    // 这里不再有非公平锁的
    // if (compareAndSetState(0, 1))setExclusiveOwnerThread(Thread.currentThread());
    // 也就是说,公平锁中,必须按规矩办事,不能抢占
        acquire(1);
    }

    /**
     * Fair version of tryAcquire.  Don't grant access unless
     * recursive call or no waiters or is first.
     */
    protected final boolean tryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
        // 这里多了一个 !hasQueuedPredecessors(),也就是不需要考虑wait链表
        // 否则就老实按流程走acquireQueued方法
            if (!hasQueuedPredecessors() &&
                compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        else if (current == getExclusiveOwnerThread()) {
            int nextc = c + acquires;
            if (nextc < 0)
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        return false;
    }
}

下面看看的hasQueuedPredecessors实现

public final boolean hasQueuedPredecessors() {
    // The correctness of this depends on head being initialized
    // before tail and on head.next being accurate if the current
    // thread is first in queue.
    Node t = tail; // Read fields in reverse initialization order
    Node h = head;
    Node s;
    // h != t 代表wait链表不为空状态
    // (s = h.next) == null代表wait链表已经初始化
    // s.thread != Thread.currentThread()代表当前线程不是第一个在wait链表排队的线程
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}

7.2.4 小结

  • 非公平锁比起公平锁来说,唯一区别就是非公平锁可以快速用compareAndSetState(0, acquires)进行抢占,而公平锁必须老老实实FIFO形式排队;但unlock唤醒的时候是没有区别的。
  • state采用volatile,保证有序性和可见性
  • 大量使用如unsafe.compareAndSwapInt(this, stateOffset, expect, update);此类的CAS操作,保证原子性,同时在竞争小的时候效率胜过synchronized
  • 所谓的加锁就是AQS.state++。且该锁是可重入的,每次就state加1,unlock一次减一。两个操作必须一一对应,否则其他等待锁的线程永远等待。
  • 所谓的等待锁阻塞,就是放在一个链表里,然后用LockSupport.park(this)阻塞
  • 就算用中断唤醒已经等待锁而阻塞的线程,依然必须直到获取锁才能执行。且在其后如果执行可中断操作,会发生中断!

7.3 ReentrantLock对比synchronized

ReentrantLock和synchronized对比如下:

可重入 等待可中断 公平性 绑定对象数 性能优化
synchronized 支持 不支持 非公平 只能1个 较多
ReentrantLock 支持 支持 非公平/公平 可以多个 -

0x08 Condition

8.1 基本概念

Condition类其实是位于java.util.concurrent.locks的一个接口类。他的一个常用实现类是AQS的非静态内部类ConditionObject

public class ConditionObject implements Condition, java.io.Serializable

虽说ConditionObject是public修饰,但不能直接使用,因为他是非静态内部类,必须先实例化AQS的实例。而AQS定义如下:

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable

很明显,他是一个抽象类,不能直接实例化。也就是说必须使用继承他的子类才能实例化,从而使用ConditionObject

我们最常使用的是配套ReentrantLock和Condition使用:

ReentrantLock lock = new ReentrantLock(true);
Condition condition = lock.newCondition();
condition.await();
condition.signal();

下面简单分析下后面3步代码实现:

8.2 实现原理

具体请查阅
Java-并发-Condition

这里只给出流程总结:

  1. await方法,将当前线程加入AQS.condition_queue,且会顺便清理其中不为CONDITION状态的结点
  2. await方法,让当前线程释放所有的锁许可(state归0)
  3. await方法,将当前线程阻塞,直到该线程被放入AQS.wait_queue
  4. signal方法,将从AQS.condition_queue队列的头结点开始往后遍历,从AQS.condition_queue中将该线程结点移除,并放回AQS.wait_queue,并根据前驱结点是否已经撤销或异常按需唤醒当前结点。注意,此过程只要成功移动一个节点,遍历就结束了,也就是说每次signal方法最多只能从AQS.condition_queue中移动一个结点到AQS.wait_queue
  5. signal方法,在上述遍历移动节点过程中会顺便清理掉AQS.condition_queue中那些状态不为CONDITION的结点
  6. await方法,阻塞的线程因为被signal方法重新放入AQS.wait_queue而被其他前驱结点唤醒,此时有几种情况:
    1. 意外情况。LockSupport有可能会因为意外导致线程唤醒,该情况和情况2处理相同,需要再次判断节点是否已经放入AQS.wait_queue
    2. wait_queue中该结点的前驱结点执行unlock方法时唤醒。处理同情况1
    3. 其他线程调用signal方法前调用中断方法唤醒,需要重设interruptMode
    4. 其他线程调用signal方法后调用中断方法唤醒,需要重设interruptMode
  7. await方法,该节点调用acquireQueued走申请锁许可流程。注意,如果此时申请不到锁,线程又会被LockSupport.park阻塞。
  8. await方法,会又一次顺便清理其中不为CONDITION状态的结点
  9. await方法,按阻塞前后收到中断请求的情况按需发起中断
  10. await方法返回,可继续执行用户代码

8.3 await/signal对比wait/notify

上面wait/notify章节已经比较过了 ,请点击这里查看

8.4 小结

Condition特点如下:

  • Condition必须搭配AQS.sync使用
  • await过程可中断
  • Condition的阻塞唤醒,是基于lock的。lock维护了一个wait_queue,用于存放等待锁的线程。而Condition也维护了一个condition_queue。当拥有锁的线程调用await方法,就会被放入condition_queue;当调用signal方法,会从condition_queue选头一个满足要求的节点移除然后放入wait_queue,重新竞争lock。
  • 一个lock可以对应多个Condition,在多条件情况十分方便

0x09 ReadWriteLock

9.1 基本概念

现在大家开发程序,大多是在多线程场景,就会用到各种锁。但其实往往读和读之间是不冲突的,是无状态无修改的,不应该互相互斥。我们往往只需在读写或者写与写之间互斥即可。在JDK中就直接提供了一个ReadWriteLock,互斥关系如下表:

不互斥 互斥
互斥 互斥

ReadWriteLock的其他重要知识点如下:

  • 读写锁分为读锁和写锁,分开使用
  • 读锁允许读可并发,但此时写锁申请会被阻塞
  • 写锁不允许任何并发,一旦有线程拥有写锁,其他线程的读写锁申请全被阻塞
  • ReadWriteLock适用于写少读多的场景
  • ReadWriteLock也被称为共享-独占锁
  • 读写锁都可重入,调用了几次lock就必须配套调用几次unlcok

使用时一般是这几个api:

// 获得读写锁实例
ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
// 读锁加锁,可与其他线程读锁共享,阻塞其他写锁申请,可重入
lock.readLock().lock()
// 读锁解锁,调用次数必须和lock相同
lock.readLock().unlock();

// 写锁加锁,阻塞其他线程读写锁申请,可重入
lock.writeLock().lock();
// 写锁解锁,调用次数必须和lock相同
lock.writeLock().unlock();  

9.2 实现原理

9.2.1 流程分析

9.2.2 源码分析

Java-并发-锁-ReadWriteLock

0x10 StampedLock

10.1 基本概念

10.2 实现原理

0x11 Unsafe.park

已经在第二章的park cpp分析一节中分析过了。

LockSupport.park底层就是用的Unsafe.park,而JDK中很多地方使用了Unsafe。这两个锁偏底层,建议用基于他们或AQS的高级锁如ReentrantLockCountDownLatchCyclicBarrier等。

0x12 性能对比

Java 8 StampedLock,ReadWriteLock以及synchronized的比较

0xFF 参考文档

Java 8 并发篇 - 冷静分析 Synchronized(下)

Synchronized的原理及自旋锁,偏向锁,轻量级锁,重量级锁的区别

浅谈Mutex (Lock)

jdk源码剖析二: 对象内存布局、synchronized终极原理

JVM源码分析之synchronized实现

Java线程源码解析之yield和sleep

java并发编程之LockSupport

猜你喜欢

转载自blog.csdn.net/baichoufei90/article/details/85004307
今日推荐