做网站编辑需要具备的素质,老李网站建设,怎么做竞拍网站,溧阳市建设局网站6目录
一、死锁
1.1 出现死锁的常见场景#xff1a;
1.2 产生死锁的后果#xff1a;
1.3 如何避免死锁#xff1a;
二、内存可见性
2.1 由内存可见性产生的经典案例#xff1a;
2.2 volatile 关键字#xff1a;
2.2.1 volatile 用法#xff1a;
2.2.2 volatile 不…目录
一、死锁
1.1 出现死锁的常见场景
1.2 产生死锁的后果
1.3 如何避免死锁
二、内存可见性
2.1 由内存可见性产生的经典案例
2.2 volatile 关键字
2.2.1 volatile 用法
2.2.2 volatile 不保证原子性
2.2.3 volatile 作用总结
三、wait 和 notify
3.1 wait 详解
3.2 notify 和 notifyAll
3.2.1 notify
3.2.2 notifyAll
3.3 面试题wait 和 sleep 的区别 在上一篇文章我们了解了什么是线程安全分析了产生线程不安全的原因。今天我们就要深度刨析一下线程不安全的经典案例死锁和内存可见性引起的线程不安全问题。
一、死锁
1.1 出现死锁的常见场景
• 场景一
锁是不可重入锁synchronized 是可重入锁并且一个线程针对一个锁对象连续加锁两次。
• 场景二
两个线程两把锁。先让两个线程分别拿到一把锁然后再去尝试获取对方的锁这时就出现了死锁的情况。
• 场景三
多个线程多把锁。随着线程和锁的数目的增加情况就会变得更加复杂死锁就更容易出现。下面就是一个经典的死锁场景哲学家就餐除非吃到面条否则不会放下筷子。 如果出现极端的情况同一时刻所有的哲学家都拿起左边的筷子这时就会出现死锁。
1.2 产生死锁的后果
死锁是非常严重的问题。一个进程中线程的个数是有限的死锁会使线程被卡住没法继续工作。更加严重的是死锁这种 bug 往往都是概率性出现未知才是最可怕的。测试的时候怎么测试都没事一旦发布就出现了问题。更加要命的是发布也没有问题等到夜深人静的时候大家都睡着的时候突然给你来点问题直接带走年终奖。
1.3 如何避免死锁
要想避免死锁我们就要从产生死锁的原因入手。
教科书上经典的产生死锁的四个必要条件下面给出的四个条件友友们一定要背下来面试的经典问题。 1. 锁具有互斥性 这时锁的基本特点一个线程拿到锁之后其他线程就得阻塞等待。 2. 锁具有不可抢占性不可剥夺性 一个线程拿到锁之后除非他自己主动释放锁否则谁也抢不走。 3. 请求和保持 一个线程拿到一把锁之后不释放这个锁的前提下再尝试获取其他锁。 4. 循环等待。 多个线程获取多个锁的过程中出现了循环等待A 等待 B B 又等待 A。 在任何一个死锁的场景都必须同时具备上述四点只要缺少一个都不会构成死锁。观察上面的四个条件不难发现条件 1 和条件 2 是锁的基本特性这个我们无法改变观察到条件 3 和条件 4 都是代码结构的问题所以我们就从条件 34 入手。
• 针对条件 3
不要让锁嵌套获取即可。如果有些场景必须要嵌套获取锁那么就破除循环等待条件 4 即使出现嵌套也不会出现死锁。
• 针对条件 4
当代码中确实需要用到多个线程获取多把锁一定要记得约定好加锁的顺序每个线程都必须要先获取 A 锁再获取 B 锁再.......就可以有效避免死锁了。
二、内存可见性
2.1 由内存可见性产生的经典案例
请友友们观察一下下面这段代码可以粘贴到自己的编译器上跑一下看看是否符合你的预期。
public class demo1 {static int count 0;public static void main(String[] args) {Thread t1 new Thread(() - {while(count 0){}System.out.println(t1.end);});Thread t2 new Thread(() - {Scanner in new Scanner(System.in);System.out.println(请输入一个数字:);count in.nextInt();});t1.start();t2.start();}
}因为输入数据存在 IO 操作很慢所以一定能保证在我们输入数据的时候t1 线程已经开始执行了。
正常来说我们输入一个非 0 的数字后t1 线程里面就会停止循环。但是产生的结果如下
循环并没有退出由于是前台线程所以程序不能够结束。 上述问题产生的原因就是因为内存可见性。
• 案例解析
上面的案例产生的问题是由于编译器优化 / JVM 优化产生的问题。不是说优化不好而是 JVM 在这种情况下的优化太激进了。为什么会产生这么激进的优化呢
我们站在指令的角度来理解有两个方面
1. 在while 循环体中每次条件判断的时候分为两个步骤1. load从内存读取数据到 cpu 寄存器。2. cmp比较条件成立就会继续执行。 当前循环的旋转速度很快短时间内出现大量的 load 和 cmp 反复执行的效果由于 load 执行消耗的时间比 cmp 消耗的时间多很多量级是几千倍上万倍。
2. JVM 发现每次 load 执行的结果是一样的在 t2 修改之前。
于是 JVM 就把上述的 load 操作优化掉了只有第一次是真正的进行 load 后续的 load 就直接读取刚才 load 在寄存器中的值也就是说不会去内存中去读取值了这时即使内存中的值已经修改但是还是 load 不到这就是我们的线程可见性问题。
其实在这里加上打印程序就符合我们的预期了。 这是为什么呢答因为此时 IO 操作才是程序运行时间的大头优化 load 就没有必要了因为程序的瓶颈不是 load 。此外IO 操作是不能被优化掉的被优化的前提是反复执行的结果是相同的IO 操作注定是反复执行的结果是不相同的。
• 小结
上述问题的本质还是编译器优化引起的优化掉 load 操作之后使 t2 线程的修改没有被 t1 线程感知到这就是 ”内存可见性“ 问题。
2.2 volatile 关键字
2.2.1 volatile 用法
编译器到底啥时候优化这也是个 ”玄学问题“。我们作为程序员显然不希望看到这样的代码出现因此 Java 就引入的 volatile 关键字就可以解决内存可见性引起的问题。volatile 修饰的变量能够保证 内存可见性。
代码在写入 volatile 修饰的变量的时候
• 改变线程工作内存中 volatile 变量副本的值。
• 将改变后的副本的值从工作内存刷新到主内存。
代码在读取 volatile 修饰的变量的时候
• 从主内存中读取 volatile 变量的最新值到线程的工作内存中。
• 从工作内存中读取 volatile 变量的副本。
前面我们讨论内存可见性时说了直接访问工作内存实际是 CPU 的寄存器或者 CPU 的缓存)速度非常快但是可能出现数据不一致的情况。加上 volatile 强制读写内存。速度是慢了但是数据变的更准确了。
使用案例演示
public class demo2 {public volatile static int count 0;public static void main(String[] args) {Thread t1 new Thread(() - {System.out.println(t start);while(count 0){}System.out.println(t end);});Thread t2 new Thread(() - {Scanner in new Scanner(System.in);System.out.println(请输入一个数字:);count in.nextInt();});t1.start();t2.start();}
}
案例效果 2.2.2 volatile 不保证原子性
volatile 和 synchronized 有着本质的区别。synchronized 能够保证原子性volatile 保证的是内存可见性。例如下面这个案例
public class demo3 {public volatile static int count 0;public static void main(String[] args) {Object locker new Object();Thread t1 new Thread(() - {for(int i 0;i 50000;i){
// synchronized(locker){count;
// }}});Thread t2 new Thread(() - {for(int i 0;i 50000;i){
// synchronized(locker){count;
// }}});t1.start();t2.start();try {t1.join();} catch (InterruptedException e) {throw new RuntimeException(e);}try {t2.join();} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println(count);}
}案例演示效果如下 可以看到最终的结果还是不符合我们的预期所以 volatile 不保证原子性。
2.2.3 volatile 作用总结
volatile 关键字的作用主要有如下两个
• 保证内存可见性
基于屏障指令实现即当一个线程修改一个共享变量时另外一个线程能读到这个修改的值。
• 保证有序性
禁止指令重排序。编译时 JVM 编译器遵循内存屏障的约束运行时靠屏障指令组织指令顺序。
注意volatile 不能保证原子性。
三、wait 和 notify
由于线程之间是抢占式执行的因此线程之间执行的先后顺序难以预知。但是实际开发中有时候我们希望合理的协调多个线程之间的执行先后顺序。就好像足球队一样线程 1 要先传球线程 2 才能射门。 针对随即调度我们程序员也是有手段干预的即通过 “等待” 的方式能够让线程一定程度的按照我们预期的顺序来执行。无法主动让某个线程被调度但是可以主动让某个线程等待给别的线程机会。
完成这个协调工作主要涉及到三个方法 注意: waitnotifynotifyAll 都是 Object 类的方法意味着所有类都可以。
3.1 wait 详解
wait 做的事 • 使当前执行代码的线程进行等待把线程放到等待队列中。 • 释放当前的锁wait 必须要放在锁的代码块里面使用。 • 满足一定条件时被唤醒重新尝试获取这个锁。 wait 结束等待的条件 • 其他线程调用该对象的 notify 方法。 • wait 等待时间超时wait 方法提供一个带有 timeout 参数的版本来指定等待时间。 • 其他线程调用该等待线程的 interrupted 方法导致 wait 抛出 InterruptedException 异常。 注意wait 和 sleep 一样会被线程的 interrupt 打断wait 也会自动清空标志位。
案例演示如下
public class demo1 {public static void main(String[] args) {Object locker new Object();Thread t1 new Thread(() - {System.out.println(开始);try {synchronized(locker){locker.wait();}} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println(结束);});t1.start();}
}
案例演示效果如下 可以发现线程成功被停止了。
注意
wait 要搭配 synchronized 来使用。脱离 synchronized 使用 wait 会直接抛出异常。例如我们将上面的那段代码进行修改将 locker 脱离锁产生的情况如下 这样在执行到 object.wait() 之后就一直等待下去那么程序肯定不能⼀直这么等待下去了。这个时候就需要使用到了另外⼀个方法唤醒的方法 notify()。
3.2 notify 和 notifyAll
3.2.1 notify
notify 方法是唤醒等待的线程。具体作用如下 • 方法 notify() 也要在同步方法或同步块中调用该方法是用来通知那些可能等待该对象的对象锁的其它线程对其发出通知 notify并使它们重新获取该对象的对象锁。 • 如果有多个线程等待则有线程调度器随机挑选出⼀个呈 wait 状态的线程。(并没有 先来后到) 。 • 在 notify() 方法后当前线程不会马上释放该对象锁要等到执行 notify() 方法的线程将程序执行完也就是退出同步代码块之后才会释放对象锁。 案例演示如下
public class demo2 {public static void main(String[] args) {Object locker new Object();Thread t1 new Thread(() - {System.out.println(开始等待);synchronized(locker){try {locker.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}}System.out.println(结束等待);});Thread t2 new Thread(() - {Scanner in new Scanner(System.in);System.out.print(输入内容开始通知:);in.next();synchronized(locker){locker.notify();System.out.println(通知结束);}});t1.start();t2.start();}
}
案例演示效果如下 3.2.2 notifyAll
notify 只能随机唤醒一个由 wait 导致的等待线程例如
import java.util.*;
public class demo3 {public static void main(String[] args) {Object locker new Object();Thread t1 new Thread(() - {System.out.println(t1:开始等待);synchronized(locker){try {locker.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}}System.out.println(t1:结束等待);});Thread t2 new Thread(() - {System.out.println(t2:开始等待);synchronized(locker){try {locker.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}}System.out.println(t2:结束等待);});Thread t3 new Thread(() - {Scanner in new Scanner(System.in);System.out.print(输入内容开始通知:);in.next();synchronized(locker){locker.notify();System.out.println(通知结束);}});t1.start();t2.start();t3.start();}
}
最终跑出的结果如下
可以清楚的看到只有一个线程被唤醒了。 因此 Java 引入 notifyAll 来一次性唤醒全部。我们就直接将上述代码稍加修改即可。
import java.util.*;
public class demo3 {public static void main(String[] args) {Object locker new Object();Thread t1 new Thread(() - {System.out.println(t1:开始等待);synchronized(locker){try {locker.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}}System.out.println(t1:结束等待);});Thread t2 new Thread(() - {System.out.println(t2:开始等待);synchronized(locker){try {locker.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}}System.out.println(t2:结束等待);});Thread t3 new Thread(() - {Scanner in new Scanner(System.in);System.out.print(输入内容开始通知:);in.next();synchronized(locker){locker.notifyAll();//修改处System.out.println(通知结束);}});t1.start();t2.start();t3.start();}
}案例的演示效果如下
可以看到我们所有等待的线程都被唤醒了。 注意虽然是同时唤醒 2 个线程但是这 2 个线程需要竞争锁所以并不是同时执行而仍然是有先有后的执行。
3.3 面试题wait 和 sleep 的区别
• wait用于线程之间的通信。
• sleep让线程阻塞一段时间。
相同点是都可以让线程放弃执行一段时间。
大体的区别分为如下 3 点 1wait 需要搭配 synchronized 使用而 sleep 不需要。 2wait 是 Object 的方法sleep 是 Thread 的静态方法。 3(从状态来) wait 被调用后当前线程进入 waiting 状态并释放锁并可以通过 notify 和 notifyAll 方法进行唤醒。sleep 被调用后当前线程进入 TIMED_WAITING 状态不涉及锁相关的操作。 结语
其实写博客不仅仅是为了教大家同时这也有利于我巩固知识点和做一个学习的总结由于作者水平有限对文章有任何问题还请指出非常感谢。如果大家有所收获的话还请不要吝啬你们的点赞收藏和关注这可以激励我写出更加优秀的文章。