synchronized 死锁 volatile
一、synchronized 特性
(1) 互斥
synchronized会起到互斥效果,某个线程执⾏到某个对象的synchronized中时,其他线程如果也执⾏ 到同⼀个对象synchronized就会阻塞等待.

public class Demo {
private static Object locke = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
synchronized (locke){
System.out.println("t1线程启动");
while (true){
}
}
});
Thread t2 = new Thread(() -> {
synchronized (locke){
System.out.println("t2线程启动");
}
});
t1.start();
t2.start();
}
}

t1进入synchronized拿到locke锁 永不释放 t2尝试进入 发现锁被占用 进入阻塞状态
要想解除阻塞,需要往下执行才可以,要想往下执行,就需要等到第一次的锁被释放
这样的问题,就称为“死锁”
1.第一次进行加锁操作,能够成功的(锁没有人使用)
2.第二次进行加锁,此时意味着,锁对象是已经被占用的状态.第二次加锁,就会触发阻塞等待
死锁是一个非常严重的bug.使代码执行到这一块之后,就卡住
为了解决上述的问题,Java的synchronized就引入了可重入的概念.
(2)可重入
同一个线程可以多次获取同一把锁
synchronized同步块对同⼀条线程来说是可重入的,不会出现自己把自己锁死的问题;
public class Demo {
private static Object locker = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
synchronized (locker){
System.out.println("第一次加锁");
synchronized (locker){
System.out.println("第二次加锁");
}
}
});
t1.start();
}
}

例子
不可重入
你追一个妹子第
一次牵手成功
第二次想牵手,
被她说:「你已经牵过了,等别人牵完你再来」
?????????可重入
你已经牵着她的手你想:
“再牵一次”.“搂一下”“抱一下”
她说:「是你啊,可以」
前提:是同一个人(线程)游戏
你已经在Boss战中
再放技能、开buff、补血
系统不会说:「你已经在战斗中,不能再战斗」
连招顺畅
可重入锁的实现原理,关键在于让锁对象,内部保存,当前是哪个线程持有的这把锁

站在JVM的视角,看到多个 } 需要执行JVM如何知道哪个 } 是真正解锁的那个??
先引入一个变量,计数器(0)
每次触发 { 的时候,把计数器++
每次触发 } 的时候,把计数器--
当计数器--为0的时候,就是真正需要解锁的时候
二、死锁
1.关于死锁
两个线程,两把锁,每个线程获取到一把锁之后,尝试获取对方的锁
死锁必须满足的4个条件
互斥
请求与保持
不可剥夺
循环等待
2.构造死锁
public class Demo {
private static Object locker1 = new Object();
private static Object locker2 = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
synchronized (locker1) {
System.out.println("t1拿到locker1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (locker2) {
System.out.println("t1拿到locker2");
}
}
});
Thread t2 = new Thread(() -> {
synchronized (locker2){
System.out.println("t2拿到locker2");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (locker1){
System.out.println("t2拿到locker1");
}
}
});
t1.start();
t2.start();
}
}

第一步:t1拿到锁locker1
第二步:t2拿到锁locker2
第三步:t1想拿锁locker2 locker2在t2手上 阻塞等待
t2想拿锁locker1 locker1在t1手上 阻塞等待
此时 t1持有locker1等待t2
t2持有locker2等待t1
永久在等待 这就是死锁


如果把代码中sleep去掉会发生什么变化
如果不加sleep,很可能t1一口气就把locker1和locker2都拿到了.这个时候t2还没开始呢自然无法构成死锁.
3.如何避免代码中出现死锁
(1)构成死锁的四个必要条件
1.锁是互斥的.一个线程拿到锁之后,另一个线程再尝试获取锁,必须要阻塞等待.
2.锁是不可抢占的.线程1拿到锁线程2也尝试获取这个锁,线程2必须阻塞等待(不可剥夺)而不是线程2直接把锁抢过来
3.请求和保持.一个线程拿到锁1之后,不释放锁1的前提下,获取锁2
4.循环等待.多个线程,多把锁之间的等待过程,构成了"循环"A等待B,B也等待A或者A等待B,B等待C,C等待A。
(2)如何避免
打破构成死锁条件的(3) (4)。
1.避免锁的嵌套

2.约定好加锁的顺序,就可以破除循环等待。
N个线程M把锁

1.思考人生(放下筷子,思考)
2.吃面条(拿起筷子)
5个哲学家,随机的触发吃面条和思考人生
5个哲学家,就相当于5个线程5根筷子,就相当于5把锁每个线程只需要拿到其中的两根筷子即可
大部分情况下,上述模型,可以很好的运转在一些极端情况下会造成死锁的。
同一时刻,大家都想吃面条同时拿起左手的筷子此时,任何一个线程都无法拿起右手的筷子任何一个哲学家都吃不成面条
约定,每个线程加锁的时候永远是先获取序号小的锁后获取序号大的锁

三、volatile
1.volatile能够保证内存可见性
什么是内存可见性?
一个线程修改了共享变量,
其他线程能不能立刻看到这个修改。能看到 → 有可见性
看不到 → 发生了可见性问题
public class Demo{
private static int flag = 0;
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
while (flag == 0) {
}
System.out.println("t1线程结束");
});
Thread t2 = new Thread(() -> {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入flag的值:");
flag = scanner.nextInt();
});
t1.start();
t2.start();
}
}

理论上:t2 把 flag 改为 1 1 应该跳出循环
但现实可能:t1 永远不会结束
这就是 内存不可见问题。
原因:
编译器优化
咱们写的代码 javac把java=>.class=>jvm
编译器,虽然声称优化操作,是能够保证逻辑不变,尤其是在多线程的程序中,编译器的判断可能出现失误
可能导致编译器的优化,使优化后的逻辑,和优化前的逻辑出现细节上的偏差


稍微调整代码

针对内存可见性问题,也不能指望通过sleep来解决
使用sleep大大影响到程序的效率.
不使用sleep也能解决上述的内存可见性问题呢?
2.volatile
在语法中,引入volatile关键字通过这个关键字来修饰某个变量,此时编译器这对这个变量的读取操作,就不会被优化成都寄存器
public class Demo {
private volatile static int flag = 0;
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
while (flag == 0) {
}
System.out.println("t1线程结束");
});
Thread t2 = new Thread(() -> {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入flag的值:");
flag = scanner.nextInt();
});
t1.start();
t2.start();
}
}

注:volatile解决内存可见性问题不是解决原子性问题。

四、synchronized和volatile的区别
1.volatile 解决“可见性” synchronized 解决“原子性 + 可见性”。
2.volatile 不能保证复合操作安全。synchronized 可以保证代码块线程安全。
3.volatile 只能保证可见性和有序性,不能保证原子性,不具备互斥性。
4.synchronized 是悲观锁,可以保证原子性、可见性和有序性,通过互斥访问实现线程安全。
更多推荐




所有评论(0)