目录
常用锁策略
1.乐观锁 VS 悲观锁
2.轻量级锁 VS 重量级锁
3.自旋锁 VS 挂起等待锁
4.互斥锁 VS 读写锁
5.公平锁 VS 非公平锁
6.可重入锁 VS 可重入锁
CAS
ABA问题
Synchronized原理
1. 锁升级/锁膨胀
2.锁消除
3.锁粗化
常用锁策略
1.乐观锁 VS 悲观锁
站在锁冲突概率的预测角度.乐观锁预测冲突概率较小,悲观锁预测锁冲突概率较大
synchronized既是一个悲观锁,也是一个乐观锁.它默认是一个乐观锁,但当锁竞争比较激烈,就会变成悲观锁.
2.轻量级锁 VS 重量级锁
站在加锁操作的开销角度. 轻量级锁开销较小,重量级锁开销较大.
synchronized默认是一个轻量级锁,但发现锁竞争比较激烈的时候就会转转换成重量级锁.
3.自旋锁 VS 挂起等待锁
自旋锁是一种典型的轻量级锁,对于自旋锁,当锁被释放后,线程能第一时间感知到锁,从而有机会获取到锁
挂起等待锁是一种典型的重量级锁.当锁被释放后,继续等待,不知道什么时候能过获取到锁
synchronized这里的轻量级锁是基于自旋锁的方式实现的,而synchronized的重量级锁是针对挂起等待锁的方式实现的.
4.互斥锁 VS 读写锁
互斥锁提供加锁和解锁两种操作,如果一个线程加锁,另外一个线程也尝试加锁,就会产生阻塞等待.
读写锁提供了三种操作,分别是针对读操作加锁,针对写操作加锁和解锁操作.多线程针对同一个变量并发读,这个时候没有线程安全问题,也不需要加锁控制.
读锁和读锁之间没有互斥,写锁和写锁之间存在互斥,写锁和读锁之间存在互斥.
synchronized不是读写锁
5.公平锁 VS 非公平锁
所谓公平,就是指 " 先来后到 ",下面举个栗子
公平锁: 当女神分手后,由等待队列中最早来的舔狗上
非公平锁: 就是当女神分手后,三个滑稽老铁都有了追求女神的机会,而和之前追了多长时间没有关系.
在操作系统和 Java synchronized 中都是非公平锁,操作系统针对加锁的控制,本身依赖线程的调度顺序,这个调度顺序是随机的,不会考虑线程等待了多长时间.
6.可重入锁 VS 可重入锁
不可重入锁: 一个线程针对一把锁,连续加锁两次出现死锁
可重入锁: 一个线程针对一把锁,连续加锁多次都不会死锁.
synchronized是可重入锁.
关于可重入问题和synchronized的相关操作
CAS
CAS指的是 compare and swap指的是 比较并交换,一个CAS涉及到一下操作:
上述这个CAS过程并非是通过异端代码实现的,而是通过一条 CPU指令完成的.而CAS操作是原子的,在一定程度上就回避了线程安全问题,同时在解决线程安全问题除了加锁之外,又可以使用CAS方法了.
CAS可以实现原子类,在Java标准库中提供的类
接下来用原子类写一个两个线程并发自加的操作
import java.util.concurrent.atomic.AtomicInteger;
public static void main(String[] args) {
//这些原子类,就是基于 CAS 实现了 自增,自减等操作
//此时进行这类操作不用加锁也是线程安全的
AtomicInteger count = new AtomicInteger(0);
Thread t1 = new Thread(()-> {
for (int i = 0; i < 50000; i++) {
count.getAndIncrement(); //count++
// count.incrementAndGet(); //++count
// count.getAndDecrement(); //count--
// count.decrementAndGet(); //count--
}
});
Thread t2 = new Thread(()-> {
for (int i = 0; i < 50000; i++) {
count.getAndIncrement(); //count++
}
});
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(count.get());
}
上面操作用伪代码进行实现
原子类这里的实现,每次修改之前,在确认一下这个值是否符合要求.
CAS还可以实现自旋锁
接下来看看CAS实现的自旋锁的伪代码
注意: 在java中并没有提供CAS方法,此处CAS相当于是一个简化的方式
ABA问题
CAS在运行中的核心是检查value和oldValue是否一致,如果一致,就视为value中途没有被修改过,在进行下一步操作,但可能在检查value和oldValue是否一致之前,可能会出现value从A修改成B,又从B修改为A这样的操作,CAS无法判断这种操作是否发生,这样的问题就叫ABA问题
针对这样的问题,采取的方案就是给要修改的数据加版本号,想象初始版本号是1,每次修改版本号都+1,人后进行CAS的时候,不是一以金额为基准了,而是以版本号为基准,因为版本号是只会自加的,而不会减少.
Synchronized原理
前面提到过,synchronized关键字,两个线程针对同一个线程加锁,就会产生阻塞等待.但在synchronized内部还有一些优化机制,存在的目的就是为了让锁更高效.
1. 锁升级/锁膨胀
通过Synchronized关键字进行加锁的的过程中,Synchronized会经历,无锁,偏向锁,轻量级锁,和重量级锁四种状态.
第一个尝试加锁的线程,优先进入偏向锁的状态.
偏向锁不是真的 " 加锁 ", 只是给对象做一个 " 偏向锁的标记 ", 记录这个锁属于哪个线程 .如果后续没有其他线程来竞争该锁 , 那么就不用进行其他同步操作了 ( 避免了加锁解锁的开销 )如果后续有其他线程来竞争该锁 ( 刚才已经在锁对象中记录了当前锁属于哪个线程了 , 很容易识别 当前申请锁的线程是不是之前记录的线程), 那就取消原来的偏向锁状态 , 进入一般的轻量级锁状态 .偏向锁本质上相当于 " 延迟加锁 " . 能不加锁就不加锁 , 尽量来避免不必要的加锁开销 .但是该做的标记还是得做的 , 否则无法区分何时需要真正加锁 .
2.锁消除
编译器智能的判定,看当前的代码是否真的要加锁,如果这个场景不需要加锁,而我们加了,编译器聚会自动把锁干掉.
3.锁粗化
锁的粒度: synchronized 包含的代码越多,粒度就越粗,包含的代码越少,粒度就越细.
通常情况下,认为锁的粒度细一点比较好,因为加锁部分的代码,是不能并发执行的,锁的粒度越细,能并发执行的代码就越多;反之,就越少.
但也有特殊的情况,有时候可能没有现成来抢占这个锁,jvm就会自动把锁粗化,避免频繁申请释放锁.