系列文章目录
第五章 synchronized 总结
文章目录
前言
上一章,我们讲到了线程安全问题,想要解决线程安全问题,核心思路就是加锁,synchronized 关键字就可以进行加锁。
一、synchronized 的特性
互斥
- synchronized 在使用的时候是要搭配代码块 { } 来使用的,进入 synchronized 修饰的代码块时就相当于进行了加锁,退出synchronized 修饰的代码块时就相当于解锁了。
- 在已经加锁的状态下,当另一个线程尝试同样加这个锁时,就会产生“锁冲突/锁竞争”。那么,后一个线程就会阻塞等待,一直等到前一个已经加锁了的线程解锁为止 。
public class Demo2 {
private static int count = 0;
Object locker = new Object();
public static void main(String[] args) throws InterruptedException {
Object locker = new Object();
Thread t1 = new Thread(() ->{
for (int i = 0; i < 5000; i++) {
synchronized (locker){
count++;
}
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
synchronized (locker){
count++;
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
}
- 那么,在上述两条规则的限定下,大家也可以很轻松地理解在加了锁以后,两个线程其实已经不是同时执行还是变成了具有先后关系地去执行。由并发执行变成了串行执行。
二、synchronized 的使用
1、synchronized 修饰一个实例方法
class Counter{
public int count;
synchronized public void increase1(){
count++;
}
public void increase2(){
synchronized (this){
count++;
}
}
}
public class Demo1 {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Thread t1 = new Thread(() ->{
for (int i = 0; i < 5000; i++) {
counter.increase1();
}
});
Thread t2 = new Thread(() ->{
for (int i = 0; i < 5000; i++) {
counter.increase1();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter.count);
}
}
- 上面就是 synchronized 修饰了一个实例方法
- increase1 和 increase2 是等价的,increase1 的写法是 increase2 的简化版本。
2、synchronized 修饰一个静态方法
- increase3 和 increase4 是等价的,increase3 的写法是 increase4 的简化版本。
三、synchronized 的锁机制
可重入锁(重要)
- 所谓的可重入锁,指的是:一个线程针对一把锁加锁两次,不会出现死锁。满足这个要求,就是“可重入”,不满足,就是“不可重入”。
那么,什么又是死锁呢?
//线程t
synchronized(locker){
synchronized(locker){
......
}
}
-
观察上面线程 t,假设第一次加锁成功了。此时 locker 就属于是“被锁定”状态,但是紧接着又要进行第二次加锁操作,发现同样加的也是 locker 锁对象,但是之前的锁还没有解锁,原则上来说就要进行阻塞等待。但是,如果这个不进行加锁,代码也就不会往下执行,那么第一次加锁操作也不会解锁,那么,就出现了死锁的情况,也就是说,线程卡死了。
-
于是把 synchronized 设计成“可重入锁”,就可以有效解决上述死锁问题。
也就是说,让锁记录一下,是哪个线程给它锁住的,后续再加锁的时候,如果加锁线程就是持有锁的线程就直接加锁成功。 -
关于出现死锁可能情况:
1、一个线程,针对 一把锁,连续加锁两次(如果是不可重入锁,就死锁了)
2、两个线程,两把锁
t1 线程获取了锁A,t2 线程获取了锁B,但是现在 t1线程尝试获取B,t2 线程尝试获取A。
public class Demo3 {
private static Object locker1 = new Object();
private static Object locker2 = new Object();
public static void main(String[] args) {
//此时的 sleep 很重要,保证 t1和t2 分别都已经各自拿到了一把锁
Thread t1 = new Thread(() ->{
synchronized (locker1){
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
synchronized (locker2){
System.out.println("t1加锁成功!");
}
});
Thread t2 = new Thread(() ->{
synchronized (locker2){
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
synchronized (locker1){
System.out.println("t2加锁成功!");
}
});
t1.start();
t2.start();
}
}
从这里的执行结果可以看出,出现了死锁情况,当线程 t1 获取第二把锁B时,此时锁B已经被线程 t2 获取了,于是拿不到了。
3、N个线程,M把锁
- 死锁的成因(四个必要条件)
- 互斥使用(锁的基本特性)
当一个线程持有一把锁之后,另一个线程也想获取到锁,就要阻塞等待 - 不可抢占(锁的基本特性)
当锁已经被线程1拿到之后,线程2只能等待线程1主动释放,不能强行抢过来 - 请求保持(代码结构)(避免编写“锁嵌套”逻辑)
一个线程尝试获取多把锁,先拿到锁1之后,再尝试获取锁2,获取锁2的时候,锁1不会释放 - 循环等待(等待的依赖关系,形成环了)
想要解决死锁问题,破坏上述一个必要条件即可。
1和2破坏不了(这是synchronized 自带的特性,无法人为进行干预)。对于3来说,调整代码结构,避免编写“锁嵌套”逻辑。对于4来说,可以约定加锁的顺序,就可以避免循环等待了。
- 谈谈对死锁的理解
- 死锁其实就是线程出现了卡死的状况
- 有三种出现死锁的可能情况
- 造成死锁的四个必要条件
- 如何解决死锁问题
调整代码结构,避免编写“锁嵌套”逻辑。
约定加锁的顺序,就可以避免循环等待了。
总结
这章我们总结了 synchronized 的特性,学习了如何使用 synchronized ,还知道了 synchronized 是一个可重入锁,还知道了什么是死锁,出现死锁的可能情况以及如何解决死锁问题。