文章目录
一、常见的锁策略
锁策略,即当发生"锁冲突"时,该怎么办?(不只是阻塞
(一)乐观锁和悲观锁
乐观锁与悲观锁: 预测接下来发生锁冲突的概率(冲突概率角度)
- 乐观锁:预测接下来锁冲突的概率不高,因此接下来对共享资源的操作就不会加锁,当提交数据更新时,才去检查是否发生冲突,发生了就执行相应的重试逻辑
- 悲观锁:预测接下来锁冲突的概率较高,因此接下来对共享资源的操作会提前加锁
悲观的心态就是总是在以防万一,因此其总是会做很多事,避免意料之外的事。
synchronized既是乐观锁也是悲观锁(自适应),详细内容见后文。
以悲观锁的姿态工作,付出的代价更大,开销更大,更加稳健;
以乐观锁的姿态工作,付出的代价更小,开销更小,可能引入额外的风险;
(二)重量级锁和轻量级锁
重量级锁与轻量级锁: (开销角度)
- 重量级锁:加锁的开销比较大 =》悲观锁
- 轻量级锁:加锁的开销比较小 =》乐观锁
大部分情况下,重量级锁就是悲观锁,轻量级锁就是乐观锁;
synchronized既是重量级锁也是轻量级锁
(三)挂起等待锁和自旋锁
挂起等待锁与自旋锁:
- 挂起等待锁:重量级锁的典型实现方式,出现锁冲突时,会将线程进入到阻塞状态(消耗的时间多,但会让出CPU资源);因为是阻塞,因此一个线程A释放锁,与其冲突的线程B无法第一时间感知到锁(B在做自己的事);
- 自旋锁:轻量级锁的典型实现方式,出现锁冲突时,通过忙等的方式,等待锁被释放(消耗的时间更少,但会消耗更多CPU资源);因为是忙等,因此如果一个线程A释放锁,与其冲突的线程B能第一时间拿到锁(B一直在等)
synchronized是自适应,在冲突概率低的时候为自旋锁,冲突概率高的时候为挂起等待锁;
(四)公平锁和非公平锁
公平锁与非公平锁:
Java中,先来后到算作公平;
synchronized是非公平锁(概率均等),这是因为操作系统线程调度是随机的;
公平锁需要额外创建队列,维护多个线程加锁的先后顺序;
非公平锁,也能解决大部分情况,因此synchronized设计为非公平锁;
ReentrantLock提供了两个版本的锁,公平锁和非公平锁;
(五)可重入锁和不可重入锁
可重入锁和不可重入锁: 多个线程对能否对同一线程多次加锁
(六)互斥锁和读写锁
普通互斥锁和读写锁:
synchronized是普通互斥锁,就只有普通的加锁,解锁;
读写锁就是将读写操作进行分离,其针对的是多线程同时写/一个读一个写的情况(存在线程安全问题);
读写锁的核心操作有三种: 1.加读锁 2.加写锁 3.解锁 (写锁和写锁,读锁和写锁都要竞争,读锁和读锁不会竞争)
由于日常开发中,"一写多读"的情况很常见,读的频率远高于写,因此其实用性很强;
二、synchronized
1.synchronized的特性
- synchronized是一把可重入锁,可以对该锁进行多次加锁
- synchronized是一把不公平锁,意味着不会像公平锁那样引入额外的开销
- synchronized是一把互斥锁,只进行普通的加锁和解锁
- synchronized是一把自适应锁,会根据可能的锁竞争情况来对锁进行升级
2.synchronized加锁原理

偏向锁:并不是真的加锁,只是做一个标记;
- 线程使用synchronized时,进入{}并不会直接加锁,而是先设置标记(标记过程比较轻量,快速);
- 当程序运行在{}中时,如果没有其他线程来竞争锁,则其会一直保持偏向锁的状态,直到"}"结束解除标记(解除标记也快);
- 当有其他线程来竞争锁时,其会抢先一步将偏向锁升级为轻量级锁,使得另一个线程只能阻塞/忙等;
- 自旋锁会统计发生冲突的频率,如果冲突频率到达一定程度,则会从轻量级锁升级到重量级锁;
因为自旋锁发生冲突是采用的是忙等 ,会一直占用CPU资源,如果自旋锁线程多了,大家都忙等,显然消耗的资源就非常多,因此将其升级为重量级锁,重量级锁冲突时,会阻塞等待让出CPU资源,让其他线程先做,更加节省资源;
对于当前的JVM版本来说,锁升级是单向的,只能升不能降;
3.优化策略
除了如上的优化策略,synchronized还存在着其他的优化策略。
锁消除 :是编译器优化的一种。代码中加了锁,但编译器会自行判断这个"加锁"是否存在意义,如果没有意义,就会将这个锁消除;
编译器只会在非常确定无意义加锁的情况下才进行锁消除; 即便如此,即使有些锁无意义,但编译器也不一定能够消除,因此仍需要程序员自己判断;
锁粒度: 锁的粒度指加锁和解锁间包含的代码数 (实际代码数,包含了调用的代码数)。代码越多,锁粒度越粗,代码越少,锁粒度越细 ;
锁粗化,是将细粒度的锁合并成更加粗粒度的锁;
java
//多个细粒度,每次加锁都可能出现阻塞!!!
synchronized(locker){
x++;
}
synchronized(locker){
x++;
}
synchronized(locker){
x++;
}
//合并成粗粒度,阻塞的概率更低!!!
synchronized(locker){
x++;
x++;
x++;
}
三.ReentrantLock
ReentrantLock是一把可重入锁 。其为传统的锁,即通过 lock和unlock进行加锁和解锁;
java
private static int count=0;
public static void main(String[] args) throws InterruptedException {
ReentrantLock locker=new ReentrantLock();
Thread t=new Thread(()->{
for(int i=0;i<=100000;i++){
try{
locker.lock();
count++;
//使用finally来避免忘记释放锁的情况
}finally {
locker.unlock();
}
}
});
t.start();
t.join();
System.out.println(count);
}
synchronized和ReentrantLock的区别:
- synchronized可用来修饰方法和代码块,ReentrantLock只能用在代码块上
- synchronized使用代码块来加/解锁,ReentrantLock使用lock/unlock来加/解锁
- synchronized发生锁竞争时,对应线程阻塞;ReentrantLock提供了tryLock方法可以设置超时时间(甚至可以没有超时时间),可以转而去处理其他事情;
- ReentratLock还提供了公平锁的实现,可以通过构造方法(true公平,false非公平)切换;
- synchronized是Java的关键字,其底层是JVM通过C++实现的;ReentrantLock是Java标准库的类,核心逻辑是在Java层面实现的(底层调用C++的逻辑,调用操作系统api完成系统层面的加锁);
- synchronized的等待通知是基于Object类的wait和notify方法;ReentrantLock则是搭配Condition类来实现的;