锁策略
一、锁策略的引入
锁策略的使用表示当锁遇到加锁/解锁/锁冲突/的时候所采取的方法。
二、锁策略的分类
(1)乐观锁和悲观锁
加锁的时候,预测当前锁冲突的概率大小。
预测冲突概率大,那么后续要做的工作就会增加,加锁开销所花费的资源开销就更大。(悲观锁)
预测冲突概率小,那么后续要做的工作就会减少,加锁开销所花费的资源开销就更小。(乐观锁)
synchronized既是乐观锁,也是悲观锁。它支持自适应,能够自动统计当前锁冲突的次数,进行判定当前锁冲突的概率大小。当冲突概率低,就采取乐观锁执行,当冲突概率高,则会自动上升为悲观锁执行。
在悲观锁中,往往需要内核态和用户态共同完成一些操作,要做的工作就比较多;而在乐观锁中,采取的是纯用户态进行执行,要做的工作就比较少。
(2)重量级锁和轻量级锁
这两种锁,顾名思义,根据所需要进行的工作,所要花费的资源来区分锁是否重量。 一般来说,悲观锁就是重量级锁,乐观锁就是轻量级锁。
(3) 自旋锁和挂起等待锁
自旋锁是轻量级锁的一种典型实现方式。
所谓自旋,即cpu处于空转、忙等状态,消耗了很多cpu资源。但是一旦锁被释放,那么自旋锁就能第一时间拿到锁。即通过消耗cpu资源的方式,提高了获取锁的速度。
挂起等待锁是重量级锁的一种典型实现方式。
在该类锁中,当尝试加锁时,锁被占用的状态下,就会让尝试加锁的该线程进入挂起(阻塞)状态,此时该线程不会参与调度。直到该锁被释放之后,系统会重新唤醒该线程,重新参与锁竞争获取锁。
synchronized轻量级锁部分,是基于自旋锁实现;重量级锁部分,是基于挂起等待锁实现。
(4)可重入锁和不可重入锁
synchronized属于可重入锁,即在一个线程中,连续加同一把锁两次不会发生死锁。
相反,不可重入锁即一个线程中针对同一把锁连续加锁两次,会发生死锁。
(5)公平锁和非公平锁
公平锁:严格按照先来后到的顺序获取锁,即哪个线程等待的时间长,哪个就先拿到锁。
非公平锁:若干个线程进行锁竞争。
synchronized属于非公平锁。
(6)互斥锁和读写锁
互斥锁:synchronized 属于普通的互斥锁,只有普通的加锁和解锁。
读写锁:在该锁中,读操作和写操作之间是互斥的,有利于降低锁冲突的概率,提高并发能力。其中有以下三个特点
- 读锁和读锁之间,不会产生互斥。
- 读锁和写锁之间,会产生互斥。
- 写锁和写锁之间,会产生互斥。
synchronized 实现原理
synchronized既是悲观锁,也是乐观锁;既是轻量级锁,也是重量级锁;轻量级锁是自旋锁实现,重量级锁是挂起等待锁实现。
锁的自适应
synchronized拥有自适应的功能,会根据不同的情况进行升级。这个升级过程分为四步。
- 未加锁的状态
- 当调用synchronzied的时候,就会尝试加锁,这时叫做偏向锁,即“似锁非锁“,处于不锁和锁之间的中间态。当不需要锁的时候,就不加锁;当遇到锁冲突的时候,马上加锁升级为轻量级锁。
- 轻量级锁是最初步的锁,所需要的工作较少。
- 当冲突进一步提升,锁升级为重量级锁。
- 在上述的锁升级过程中,锁不能降级只能升级。
三、锁消除
锁消除是编译器的优化策略。在进行加锁操作后,编译器会对当前代码进行判定,判断这个地方到底需不需要锁,如果不需要就优化掉加锁操作。
四、锁粗化
锁粗化也属于一种优化策略。在部分逻辑中,需要频繁的加锁解锁。编译器会自动将多个细粒度的锁合并成一次粗粒度的锁。
伪代码如下图所示
五、CAS指令
CAS(compare and swap) 属于一条cpu原子指令,负责完成比较和交换操作。
在JVM中对CAS指令进行封装,以便Java代码中可以使用CAS操作。承载CAS指令的载体之一就是**原子类(Atomic) **。
以子类AtomicInteger进行解释,对int进行封装,此时无论是++还是–等一系列操作都是原子的。这也保证了线程安全的操作。
下面为代码案例
public static void main(String[] args) {
//原子类
AtomicInteger count = new AtomicInteger(0);
//count++
count.getAndIncrement()