目录
一、synchronized 关键字简介
二、synchronized 的特点 -- 互斥
三、synchronized 的特点 -- 可重入
四、synchronized 的使用示例
4.1 修饰代码块 - 锁任意实例
4.2 修饰代码块 - 锁当前实例
4.3 修饰普通方法 - 锁方法所在实例
4.4 修饰代码块 - 锁指定类对象
4.5 修饰静态方法 - 锁方法所在类对象
五、锁竞争和死锁
5.1 出现死锁的三种典型场景
5.1.1 “重复锁”
5.1.2 “互相锁”
5.1.3 “复杂锁”
5.2 死锁产生的必要条件
5.3 解决死锁的方案
一、synchronized 关键字简介
概述: | Java中加锁的方式有很多种,其中使用 synchronized 关键字进行加锁是最常用的。synchronized 是一种监视器锁(monitor lock)。 |
加锁的目的: | 是为了将多个操作“打包”为一个有“原子性”的操作。 |
加锁的核心规则: | 进行加锁的时候必须先准备好“锁对象”,锁对象可以是任何类型的实例。 |
synchronized 的底层实现: | synchronized 的底层是使用操作系统的 mutex lock 实现的,本质上依然是调用系统的 API ,依靠 CPU 的特定指令完成加锁功能的。 |
二、synchronized 的特点 -- 互斥
1)什么是互斥? |
某个对象使用了 synchronized 进行修饰,当一个线程访问这个对象时,就会加锁,其他线程想要访问这个对象,就会先阻塞等待,直到这个对象解锁。这就是使用 synchronized 关键字时产生的互斥效果。 |
2)什么是加锁、解锁? |
当程序进入由 synchronized 修饰的代码块、对象或方法时,即相当于加锁。 当程序退出由 synchronized 修饰的代码块、对象或方法时,即相当于加锁。 |
3)由互斥到冲突,什么是锁冲突/锁竞争? |
由于 synchronized 具有互斥的特点,因此当多个线程同时竞争同一个锁时,线程间的冲突就不可避免。当有一个线程获得了锁,那么此时其他还想获得该锁的线程就只能阻塞等待,直到锁被释放后,才能再次竞争这个锁。这就是锁冲突或者说锁竞争。 |
三、synchronized 的特点 -- 可重入
1)什么是不可重入锁? |
同一个线程在还没释放锁的情况下,访问同一个锁。 从 synchronized 的互斥特点可以了解到,当锁未被释放,访问该锁的线程会阻塞等待。 由于锁还没有释放,第二次加锁时,线程进入阻塞等待。 线程进入阻塞等待,则第一次的锁无法释放。 这样程序就进入了僵持状态。 这种状态被称为“死锁”。 而这样的锁,被称为“不可重入锁”。 |
2)什么是可重入锁? |
可重入锁与不可重入锁不同,不会出现自己把自己锁死的情况。synchronized 就是可重入锁。 |
3)可重入锁是怎么实现可重入的? | |
可重入锁,锁内部会有两个属性,分别是“线程持有者”和“计数器”。 | |
线程持有者 | 记录了当前锁是被哪一个线程持有的。 当发生重复加锁时,会判断是否是同一线程加锁。 如果是则跳过加锁步骤,只是在另一个属性“计数器”上自增1。 如果不是,则阻塞等待。 |
计数器 | 用于记录当前锁的加锁次数。 每次加锁,“计数器”计数会自增1(比如重复加锁10次,那么计数器的值就会等于10)。 每次解锁,“计数器”计数会自减1,当计数器的值归零时,才是真正的释放锁,此时该锁才能被其他线程获取。 |
四、synchronized 的使用示例
4.1 修饰代码块 - 锁任意实例
public class Test{//创建任意类型实例作为锁对象;Object locker = new Object();public void lockTest(){//使用synchronized,指定locker作为锁对象,在需要加锁的代码块上加锁;synchronized (locker) {//需要加锁的代码;}}
}
4.2 修饰代码块 - 锁当前实例
public class Test{public void lockTest(){//使用synchronized,指定this(当前实例)作为锁对象,在需要加锁的代码块上加锁;synchronized (this) {//需要加锁的代码;}}
}
4.3 修饰普通方法 - 锁方法所在实例
public class Test{//在普通方法上,使用synchronized,指定当前实例作为锁对象,将方法加锁;public synchronized void lockTest(){//需要加锁的代码;}
}
4.4 修饰代码块 - 锁指定类对象
//任意类;
public class Locker{}public class Test{public void lockTest(){//使用synchronized,指定class(类对象)作为锁对象,在需要加锁的代码块上加锁;synchronized (Locker.class) {//需要加锁的代码;}}
}
4.5 修饰静态方法 - 锁方法所在类对象
public class Test{//在静态方法上,使用synchronized,指定当前类对象作为锁对象,将方法加锁;public synchronized static void lockTest(){//需要加锁的代码;}
}
五、锁竞争和死锁
1)由锁竞争到死锁,什么是死锁? |
上文在“synchronized 的特点 -- 互斥”中,介绍了什么是锁竞争。 人可以卷,但卷到一定程度就可以卷死自己或卷死别人。 那么,锁,也是可以卷的,比如锁竞争。 加锁可以解决线程安全问题,但是如果加锁方式不当,就可能产生死锁。 |
2)死锁对程序来说意味着什么? |
死锁是程序中最严重的一类BUG。 程序可能因此停摆、崩溃。 当然,人也可能因此“停摆、崩溃”。 |
5.1 出现死锁的三种典型场景
死锁有以下三种典型场景。 | |
<1> | “重复锁”:如,一个线程,一把锁,自己把自己拷上了。 |
<2> | “互相锁”:如,两个线程,两把锁,互相把对方拷上了。 |
<3> | “复杂锁”:如,上述两种锁重复或复合发生的情况,多个线程多把锁,超级加倍。 |
以上三个锁的名字,是笔者归纳总结后,为方便记忆而概括出的锁名,不是公认的专业名词。 |
5.1.1 “重复锁”
“重复锁”是指什么情况? |
锁在被释放前,同一个线程再次要求获得同一个锁。 锁没被释放,线程无法获得锁,进入阻塞。 但线程阻塞,代码就不会继续执行,锁也就一直得不到释放。 由此实现了自己卡死自己的“壮举”。 |
代码演示死锁:
public static void main(String[] args) {//创建任意类型实例作为锁对象;Object locker = new Object();Thread t = new Thread(()->{//指定locker作为锁对象;synchronized (locker) {//再次指定locker作为锁对象;synchronized (locker){//需要加锁的代码;}}});}
synchronized 是“可重入锁”。 |
“可重入锁”和“不可重入锁”的定义和区别,在上文“synchronized 的特点 -- 可重入”中说明了。 Java 提供的 synchronized 关键字,实现的是一个“可重入锁”。所以不用担心会发生这种死锁。 |
5.1.2 “互相锁”
1)“互相锁”是指什么情况? |
两个线程,都获取了一个不同的锁。 但是在各自的锁释放前,又分别去获取了对方的锁。 但此时两把锁都还没有被释放,那么两个线程都进入阻塞等待的状态,都在等对方把锁释放。 |
代码演示死锁:
public static void main(String[] args) {//创建两个任意类型实例作为锁对象;Object locker1 = new Object();Object locker2 = new Object();Thread t1 = new Thread(()->{//指定locker1作为锁对象;synchronized (locker1) {System.out.println("t1获取locker1");//休眠1秒,保证线程t2可以获取到锁locker2。try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}//指定locker2作为锁对象;synchronized (locker2) {//需要加锁的代码;System.out.println("t1获取locker2");}}});Thread t2 = new Thread(()->{//指定locker2作为锁对象;synchronized (locker2) {System.out.println("t2获取locker2");//休眠1秒,保证线程t1可以获取到锁locker1。try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}//指定locker1作为锁对象;synchronized (locker1) {//需要加锁的代码;System.out.println("t2获取locker1");}}});t1.start();t2.start();}//运行结果:
t2获取locker2
t1获取locker1
...程序没有正常执行完毕,因为出现了死锁。
5.1.3 “复杂锁”
“复杂锁”是指什么情况? |
“复杂锁”指前两种情况重复发生,或复合发生时,锁与锁之间相互叠加、“犬牙交错”的局面。 |
图示演示死锁:
5.2 死锁产生的必要条件
产生死锁有以下四个必要条件: | |
<1> | 互斥,获取锁的过程需要是互斥的,当锁被一个线程获取,另一个线程想获取这把锁就必须阻塞等待。这是锁的基本特性之一。 |
<2> | 不可劫取。锁被一个线程获取后,另一个线程不能强行把锁抢走,除非锁被持有线程释放。这也是锁的基本特性之一。 |
<3> | 请求保持。当一个线程申请锁而进入阻塞等待时,对自己已经持有的锁保持持有状态。这个条件与代码结构相关。 |
<4> | 循环等待/环路等待。线程申请锁,而锁在等待线程释放,形成环路。这个条件与代码结构相关。 |
5.3 解决死锁的方案
解决死锁的方案有以下几种方法: | |
<1> | 超时放弃。线程进入阻塞等待,当等待时间超过预设时间,则获取锁失败,将持有的锁释放。 |
<2> | 依序加锁。指定加锁的顺序规则,所有线程都需要按照规则规定的加锁顺序进行加锁。 |
图示演示依序加锁: