Java 锁与多线程协作

一般谈到多线程都会谈到这个概念,通过限制并发控制来保证操作互斥的要求。

:在执行多线程时用于强行限制资源访问的同步机制,即用于在并发控制中保证对互斥要求的满足(wikipedia)。

Synchronized

Synchronized是Java中提供的最基础的悲观锁。

悲观锁:它可以阻止一个事务以影响其他用户的方式来修改数据(wikipedia)。

Synchronized是通过Monitor来实现的,通过javap命令生成的字节码大体结构如下,

1
2
3
4
5
6
7
method ...
...
monitorenter // 加锁
...
aload 0
moniterexit // 解锁
...

注意上面的monitorentermonitorexit对应着加锁和解锁的操作。

Monitor:一种线程同步机制,每个Java对象都持有一个Monitor锁(再下面会仔细介绍)。
Key Point 1: 每个线程都有一个Monitor Record 列表
Key Point 2: 每个被锁住的对象都会与当前线程的Monitor Record 列表关联;
Key Point 3: Monitor的Owner字段存放持有锁的线程的唯一标识;

对象头:每一个对象都包含了一个Mark Word字段,默认存储对象的HashCode分代年龄锁标志位信息。

每当一个线程执行到Synchronized包裹的代码块时,都会先查询当前线程是否已经持有或可以持有当前对象的Monitor锁。

问题:线程切换耗时可能比用户代码执行的之间还要长,从而频繁线程切换会浪费大量的时间。

在JDK 1.6开始,HotSpot虚拟机开发团队开始对Java中的锁进行优化,加入了适应性自旋锁消除锁粗化轻量级锁偏向锁等针对Synchronized的优化。

推荐看一下这篇文章对锁优化的总结:不可不说的Java“锁”事

synchronized & volatile

可见性&原子性volatile仅能保证变量修改的可见性,不能保证原子性;synchronized即可以保证变量的修改可见性,又能保证变量修改的原子性。

线程阻塞volatile不会造成线程阻塞,synchronized会造成线程阻塞。

用法volatile仅能用在变量,synchronized可以使用在变量、方法和类。

锁的膨胀

偏向锁轻量级锁是乐观锁,基于CAS来实现的,而重量级锁是悲观锁,基于底层的操作系统的Mutex Lock(互斥锁)来实现的。

偏向锁:仅一个线程进入临界区,当前对象会持有偏向锁

轻量级锁:当两个线程交替进入临界区,会发生锁的升级,更新到轻量级锁

重量级锁:当多个线程同时进入临界区或者线程自旋的次数太大的时候,会发生锁的升级,更新到重量级锁

Object wait/notify

Object提供了waitnotify来实现多线程协作,而其底层实现也是基于Monitor来实现的。

对于每一个对象都存在一个ObjectMonitor结构,

Owner:当前持有锁的线程ID;
WaitSet:存放处于wait状态的线程队列;
EntryList:候选队列,存放处于等待锁block状态的线程队列;
ContentionList:竞争队列,所有请求锁的线程首先被放在这个竞争队列中。

步骤:

  1. 当多个线程同时访问一段同步代码时,会进入ContentionList队列中尝试获取锁;
  2. JVM会将一部分线程移动到EntryList中作为候选竞争线程;
  3. 当一个线程获取到锁的时候会更新对象的Owner字段中的线程ID为当前线程ID。
  4. 当线程调用wait方法时,释放当前对象的Monitor,Owner字段恢复为null,当前线程加入到WaitSet中等待被其他线程唤醒。
  5. 当线程调用notify方法时,释放当前对象得Monitor,Owner字段恢复为null,并唤醒处于WaitSet中的线程。

AQS

AQS是一个用来构建锁和同步器的框架,例如ReentrantLockReentrantReadWriteLock等皆是基于AQS来实现的。

核心思想

  1. 如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。
  2. 如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制(CLH队列锁)。

CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。

AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现锁的分配。

AQS定义两种资源共享方式:独占共享

独占是只有一个线程能执行(如如ReentrantLock等),而共享是允许多个线程同时执行(如Semaphore等)。

独占模式下又可分为公平锁非公平锁

公平锁:按照线程在队列中的排队顺序,先到者先拿到锁;
非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的。

重入

SynchronizedJUC lock都是可重入的。

可重入是同一线程可以重复获得当前锁,也是为了避免死锁问题

参考

https://www.jianshu.com/p/f4454164c017
https://blog.csdn.net/kobejayandy/article/details/39975339
https://tech.meituan.com/2018/11/15/java-lock.html
https://www.hollischuang.com/archives/2030