什么是Java的锁机制
参考Java锁机制
可重入锁
- 可重入锁又称为递归锁
- 可重入锁的特征是,同一个线程可以多次获得当前持有的锁对象,但是会记录重复持有锁的次数,当释放锁时,也需要释放同样的次数,否则其它线程将无法获取该锁而陷入死锁
- 当记录锁的获取次数为0,即计数器的值为0时,表示该锁已经被释放了
synchronized 关键字
- synchronized 自动加的锁,也是可重入锁
synchronized 的实现
-
所有对象都有对象头,即所有对象都可以作为一把锁
-
同步方法或同步代码块的范围就是被控制的临界区,每次只有一个线程能持有特定的锁对象并进入临界区,其他线程进入临界区都需要先等待持有临界区的锁对象的线程释放锁,并抢到锁以后才能进入临界区
-
同步方法和同步代码块不同,进入同步方法需要持有的锁对象是方法所在类的实例或class对象本身(静态同步方法),同步代码块根据传入的锁对象不同而要求进入临界区的线程持有不同的锁对象
关于 Monitor(对象监视器)
锁升级
- 为什么需要锁升级
- 锁升级:使用同步关键字持有的锁对象加的锁可能是偏向锁、轻量级锁、重量级锁中的任意一种
- 当线程进入临界区,尝试拿到锁对象时,会先比较偏向锁中的线程ID,不同则升级为轻量级锁,会尝试去做同步操作,如果做成了就不用加很重的锁,如果做不成升级成重量级锁(JDK 早期版本实现
synchronized
是直接去向操作系统申请重量级锁)
- 当线程进入临界区,尝试拿到锁对象时,会先比较偏向锁中的线程ID,不同则升级为轻量级锁,会尝试去做同步操作,如果做成了就不用加很重的锁,如果做不成升级成重量级锁(JDK 早期版本实现
偏向锁
- 查看偏向锁的开启情况
java -XX:+PrintFlagsInitial grep BiasedLock*
- 编向锁在JDK1.6之后是默认开启的,但是启动时间有延迟
- 所以需要添加启动参数
-XX:BiasedLockingStartupDelay=0
,让其在程序启动时立刻启动 - 开启编向锁:
-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
- 关闭编向锁:
-XX:-UseBiasedLocking
,关闭之后程序默认会直接进入轻量级锁状态 - JDK15及之后偏向锁不再是默认开启
- 全局安全点:STW
轻量级锁
- 轻量级锁的近乎交替执行,是指发生上图中的两种情况时,要么是第一种情况,能在有限次数的CAS中得到锁,要么是第二种情况,前一个线程正好执行完,所以就是先到先得(重新偏向)
- 之所以说交替,是因为线程数多的情况下必然会发生超出限定的CAS次数,所以交替的发生一定是线程数较少的情况
- 同时每次获得轻量级锁之前,都会面临重新偏向,说明轻量级锁的获取总是会先发生前一个线程释放锁
- 轻量级锁的自适应自旋(CAS)
重量级锁
- 前面有提到轻量级锁的自适应CAS,所以什么时候升级成为重量级锁是由JVM决定的
hashCode() 对 Synchronized 的影响
- 在执行
synchronized
同步代码之前,执行hashCode()
,跳过使用偏向锁,直接使用轻量级锁, hashcode 值随着锁对象的 Mark Word 一同被复制到争抢锁的线程的 Displaced Mark Word 里面 - 在执行
synchronized
同步代码之中,执行hashCode()
,如上图所示,直接升级为重量级锁,hashcode 值存储在 Monitor 中
总结
锁消除与粗化
- 一个同步代码块使用的锁对象每次都不同,这种情况下等同于没有加锁,解释器、JIT都会无视加锁的语句
- 如果多个使用同一个锁对象的同步代码(块 or 方法)被连续的调用(中间没有任何其它代码),那么解释器、JIT会将这些连着调用执行的同步代码合并,锁的范围就被放大了(锁粗化)
ReentrantLock 类
ReentrantLock
在作用上可以用于替代synchronized
,在需要加锁的代码开始前的位置通过reEntrantLock.lock()
获得锁对象,在需要加锁的代码执行结束后释放锁reEntrantLock.unclock()
- 使用
ReentrantLock
需要注意的是,必须要手动释放锁reEntrantLock.unclock()
,因为使用synchronized
锁定的话,遇到异常 jvm 会自动释放锁,但是使用ReentrantLock
则不会,因此必须在finally
中进行锁的释放 - 需要注意的是,要让所生效,多个线程想要获得的锁对象必须是同一个,释放锁、加锁也必须是同一个锁对象
tryLock
- 使用
ReentrantLock
可以进行 “尝试锁定”reEntrantLock.tryLock(Long time)
的方式获取锁 - 在 指定时间内 尝试获取锁,根据
tryLock()
的返回值来判定是否成功(true
-当前线程已获得锁,false
-当前线程未获得锁) - 如果 指定时间内 无法获取锁,代码中可以决定线程是否继续等待,因为使用
tryLock()
进行尝试获取锁,不管成功与否 方法都将继续执行 - 由于
reEntrantLock.tryLock(Long time)
可能会抛出异常且不会自动释放锁,所以必须在finally
中进行reEntrantLock.unclock()
的处理
lockInterruptibly
- 使用
ReentrantLock
还可以通过reEntrantLock.lockInterruptibly()
获取锁,如果未获取到锁,进入阻塞状态。此时该线程的阻塞状态 可以因为响应 其它线程中 对自身的interrupt()
调用 而被打断,而无需类似等待寻常其他线程调用sleep()
、wait()
、join()
后才能被打断 - 如果是因为使用
reEntrantLock.lock()
的线程进入阻塞状态,则必须 等到 锁对象被释放才能继续往下执行,且不能 对自身的interrupt()
做出响应而被打断
公平锁与非公平锁
ReentrantLock
还可以指定为公平锁,new ReentrantLock(true)
表示为公平锁,默认为false
非公平锁- 公平锁的意思是:先到先得,多个线程按照进入等待队列的先后顺序,依次得到锁
- 公平锁的实现原理:每一个竞争相同锁对象的线程,会先去判断 想要获取的锁对象是否为公平锁,如果是公平锁,则会检查等待队列(AQS),队列为空则直接获取锁,否则进入队列等待
- 与非公平锁的区别
- 如果发现想要获取的锁对象为非公平锁,则会直接开始尝试获取锁,获取不到则进入阻塞等待 blocked
- 非公平锁的效率(吞吐量)比公平锁高,因为不用检查等待队列
- 需要注意的是公平锁不是说,等待队列中的每个线程都有均等的执行机会,因为没有线程能够一直占有 CPU 时间 ,所以队列中的线程都只是会按照先后顺序有机会获得锁并执行
- 非公平锁,同样因为没有线程能够一直占有 CPU 时间,所以也有概率出现其他等待中的线程获得锁并执行,只是这个获得锁的机会是随机的,谁抢到归谁
- 锁饥饿:出于上述原因,可能导致一些线程一直没有办法获得锁,也因此没有执行的机会
读写锁
ReentrantLock
本身是互斥锁、排它锁ReentrantReadWriteLock
是对ReentrantLock
的增强实现,都有相同的功能ReadWriteLock rwLock = new ReentrantReadWriteLock()
rwLock .readLock()
:获得读锁,读锁为共享锁,表示该锁可以被多个线程同时获得,意味着锁范围内的代码可以使并发执行的rwLock .writeLock()
:获得写锁,写锁为排它锁,意味着锁范围内的代码只有得到锁的线程才能执行- 读写锁互斥,因为是从同一个
rwLock
中分别获取读锁和写锁,所以当别的线程获取到了写锁,其它的所有线程都不能得到读锁,同样的,当其它不定数量的线程获得读锁时,另外一个想要获取写锁的线程就会被阻塞
写锁饥饿
- 当从同一个锁对象获取锁的线程大多都是获取读锁时,因为读写锁互斥的原因,导致想从同一个锁对象拿到写锁的线程一直无法成功,就造成了写锁饥饿
- 可以通过设置读写锁为公平锁解决写锁饥饿,但是却是以降低吞吐量为代价的
锁降级
- 在写锁的代码块中,加入读锁的获取和释放,当线程获取写锁之后,执行获取读锁的部分时,当前线程获得的锁会降级成为读锁(注意顺序,一定是获取写锁之后,再获取读锁)
- 锁降级是为了让当前线程感知到数据的变化,目的是保证数据可见性
- 其实就是为了保证当前线程更新资源后,能过够马上用到的是自己刚刚更新的资源,但同时又不影响其它线程读取
邮戳锁 StampedLock
StampedLock
是对ReentrantReadWriteLock
的优化,主要是为了解决写锁饥饿问题,读的时候允许写
- 三种锁模式
- 注意事项
Lock 的原理实现 AQS
-
Lock 接口主要有三个实现类,分别是
ReentrantLock
、ReentrantReadWriteLock().readLock()
、ReentrantReadWriteLock().writeLock()
,它们的原理都是通过 AQS 实现的
-
AbstractQueuedSynchronizer 即为 AQS,抽象队列同步器
-
AQS 是用来实现锁或者其它同步器组件的公共基础部分的抽象实现,是重量级基础框架及整个JUC体系的基石,主要用于解决锁分配给"谁"的问题
-
整体就是一个抽象的FIFO队列来完成想要获取锁资源的线程排队工作,并通过一个原子
int
变量表示锁是否被持有的状态(0-当前锁没有被其他线程持有,≥1-当前锁被其他线程持有)
内部类 Node
- Node 的成员属性
Lock实现类 对 AQS 的使用
Sync
为ReentrantLock
的静态内部类- 不管是加锁、还是释放锁都是对
Sync
实例的操作
- 尝试加锁,其实就是将尝试将
state
改为1 - 公平锁和非公平锁的区别在于,公平锁尝试改为1的时候直接调用
tryAcquire(1)
,非公平锁则先是通过CAS尝试将 state 从0改为1,不行则进入nonFairTryAcquire(1)
,acquire 方法返回 true 则表示当前线程获取锁成功了 - 公平锁进入
tryAcquire(1)
后,当state
为0,会先去检查 队列中是否有线程在排队,若队列中只有当前线程或为空,然后CAS尝试将 state 从0改为1,成功返回true
,失败进入队列 - 非公平锁进入
nonFairTryAcquire(1)
后,当state
为0,CAS尝试将 state 从0改为1,成功返回true
,失败进入队列 - 锁的重入判断是在进入acquire 方法以后,如果
state
不为0,再判断持有锁的的线程是否为当前线程,如果是则直接计数器 +1(state),acquire 方法返回true
(这里公平、非公平逻辑都一样),如果不是则获取锁失败,进入队列 - 进入队列的逻辑如下
- 在进入队列后,还会再次 acquire 一下,如果不行就直接放进队列,并
LockSupport.park()
进入阻塞状态,等待持有锁的线程释放锁后,当前线程被unpark()
唤醒,如果是非公平锁会再次重新来一次上述流程,公平锁则按序从队列中被唤醒然后 acquire 一下
AQS 总结
并发相关的性质
原子性
- 即原子操作,注意跟事务 ACID 里原子性的区别与联系。对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行
可见性
- 对于可见性,Java 提供了 volatile 关键字来保证可见性
- 当一个共享变量被 volatile 修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值
- 另外,通过 synchronized 和 Lock 也能够保证可见性
- synchronized 和 Lock 能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中
- 区别在于 volatile 并不能保证原子性,synchronized 和 Lock可以保证原子性
有序性
- Java 允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性
- 可以通过 volatile 关键字来保证一定的“有序性(synchronized 和 Lock也可以)
内存屏障的分类
-
具体详情参考 C++ 对这部分 java 的源码
-
volatile
通过内存栅栏实现有序性,如下图所示,对于x、y先后两次的赋值因为中间volatile 类型的 flag 的赋值导致它们多线程的执行顺序一定是代码编写的顺序,否则编译器会改变顺序为先x两次赋值、y两次赋值、flag赋值
happens-before 原则(先行发生原则,不会发生重排序)
- 程序次序规则:一个线程内,按照代码先后顺序
- 锁定规则:一个 unLock 操作先行发生于后面对同一个锁的 lock 操作
- Volatile 变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作
- 传递规则:如果操作 A 先行发生于操作 B,而操作 B 又先行发生于操作 C,则可以得出 A 先于 C
- 线程启动规则:Thread 对象的 start() 方法先行发生于此线程的每个一个动作
- 线程中断规则:对线程 interrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件的发生
- 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过 Thread.join() 方法结束、Thread.isAlive() 的返回值等手段检测到线程已经终止执行
- 对象终结规则:一个对象的初始化完成先行发生于他的 finalize() 方法的开始
CAS机制与Unsafe类
CAS
自旋
- 所有原子类都是JUC包提供,通过CAS机制实现,而CAS又是通过Unsafe类实现
- 对Java来说,CPU原语支持的CAS,内部逻辑是不能被打断的即与原值比较后再更新,失败再重试的这个过程不能被别的线程干扰
- CAS又被称为自旋锁、乐观锁在很多领域有用到
Unsafe类与CAS的实现
自定义原子类(原子引用)
CAS 问题
循环问题
- CAS 的核心就是先比较再复制,比较的过程必然涉及到 条件判断 和 多次循环,最坏的情况就是同一资源的争抢线程过多,导致部分线程一直处于循环中,无法实现更新,增大CPU的负担
ABA问题
- CAS会有ABA问题:即一个线程把一个变量值改了两次从A->B,再从B->A;此时对于另外一个使用CAS机制更新变量值的线程来说,这个变量没有被改变能够正常被更新,但这仅针对与该变量是基础数据类型的情况,如果涉及到引用数据类型,引用所指向的对象对于后来者线程可能看着还是那个对象,但是实际上可能进入对象后执行的逻辑与预期千差万别
- 一般解决ABA问题是通过添加版本号,每更新一次数据,版本号都要加1,也就是在更新值的同时CAS还可以附加上检查版本号
- 原子类通过类似版本号的
AtomicStamedReference
方式解决
原子类(CAS 的应用)
基本类型原子类
数组类型原子类
引用类型原子类
对象的属性修改原子类
- 以一种线程安全的方式操作(增删改)非线程安全的对象中的某些属性
- 属性必须是
public volatile
修饰 - 因为 对象的属性修改原子类 都是抽象类,每次使用都需要通过静态方法
newUpater()
创建一个更新器,并且设置想要更新的属性和其对应的类
原子操作增强类
- 原子增强类是对原子类的 CAS 自旋会产生的循环问题的进一步优化和解决,核心思想是:先分后合
- 缺点是合并计算结果后,这个结果是前一个阶段的累加结果,合并结果的过程以及之后的新加的数,没有合并到当前的结果中,所以累加结果不是实时准确的(仅保证最终一致性)
LongAdder 为什么这么快
LockSupport(CAS的应用)
park()
与unpark(T)
无先后顺序要求,且不要求必须在synchronized
orLock
范围之内park()
与unpark(T)
是一对一的关系,许可证不允许多发放- 对于挂起线程、唤醒线程提供了更加方便的方式
阻塞队列
- 线程安全的队列,适用于生产者、消费者模型中
ArrayBlockingQueue
- 创建时指定数组大小
LinkedBlockingQueue
- 由链表结构组成的有界(但大小默认值为
Integer.MAX_VALUE
)阻塞队列
DelayQueue
PriorityBlockingQueue
SynchronousQueue
- LinkedTransferQueue