并发与活跃性危险问题
最近几天重新整理了下 Java 并发编程实践这本书的知识点,在此把并发活跃性相关问题列举出来,对于编程 1~2 年且有过多线程编程经验却没有相关的知识树的同学(统称)可以花点时间去读下这本经典之作。
作者当时写这本书的时候应该正是 JDK1.6 到 JDK1.7 过渡的阶段,所以关于 JDK1.8 并发的内容并未涉及,但作者谈了他的猜想(本书的作者正是 Java 并发包的开发人员之一)
综述
活跃性常见问题:
- 死锁:线程 A 占有资源 1,想访问资源 2,线程 B 占有资源 2,想访问资源 1;二者互不谦让
- 活锁:二者总是互相谦让
- 饥饿:线程想访问某个资源(比如 CPU 时钟周期作为资源),可惜优先级过低,或者该资源被其他资源占用(while 无限循环),导致饥饿
- 信号丢失:生产者与消费者背景下(Object 的 wait 与 notify 方法),wait 方法还未调用,notify 就已被调用
- 糟糕的响应性
主要关注于如何死锁的问题上
死锁的产生
典型错误案例 A
public class DeadSynchronizedTest {
private Object resourceA = new Object();
private Object resourceB = new Object();
// 路径A
public void routeA(){
synchronized(resourceA) {
synchronized(reourceB) {
// 某些操作
}
}
}
// 路径B
public void routeB() {
synchronized(resourceB) {
synchronized(resourceA) {
// 某些操作
}
}
}
}
当两个线程分别执行 A 路径与 B 路径时,便会产生死锁的可能
典型错误案例 B
public class DeadSynchronizedDemo {
public void deadSynchronized(Object resourceA, Object resourceB) {
synchronized(resourceA) {
synchronized(resourceB) {
// 某些操作
}
}
}
}
上述 deadSynchronized
方法并未直接体现隐含的死锁情况,可是当我们这样调用该方法时:
Object resourceA = new Object();
Object resourceB = new Object();
// 线程A
deadSynchronized(resourceA, resourceB);
// 线程B
deadSynchronized(resourceB, resourceA);
同样的死锁问题便产生(我们永远无法保证用户传递参数一定会符合我们 API 接口想要的顺序,所以要避免这种发生的可能性)
经典错误案例 C
在 ResourceA
和 ResourceB
中确实每个方法只锁住了自己的对象,但是依然可能造成死锁问题 —— 因为 ResourceA
对 ResourceB
进行了访问
class ResourceA {
private ResourceB resrouceB = new ResourceB();
public synchronized void deadSynchronizedA() {
// 某些操作
resourceB.deadSynchronizedB();
}
}
class ResourceB {
public synchronized void deadSynchronizedB() {
// 某些操作
}
}
改进点:缩小锁的范围,只锁自己的方法
class ResourceA {
private ResourceB resrouceB = new ResourceB();
public void deadSynchronizedA() {
synchronized(this) {
// 某些操作
}
resourceB.deadSynchronizedB();
}
}
class ResourceB {
public void deadSynchronizedB() {
synchronized (this) {
// 某些操作
}
}
}
总结
以上三个例子其实本质上都是双方互相握住了对方想要的资源,只是我们在编码的过程当中有些死锁的例子可能不是那么明显,所以需要多加小心。
同样,最后的改进方案并不是唯一的答案。
死锁的避免
将加锁的顺序统一起来 (避免单行道却出现两辆车相向而行的情况)
public class DeadSynchronizedTest {
private Object resourceA = new Object();
private Object resourceB = new Object();
// 路径A
public void routeA(){
synchronized(resourceA) {
synchronized(reourceB) {
// 某些操作
}
}
}
// 路径B
public void routeB() {
synchronized(resourceA) {
synchronized(resourceB) {
// 某些操作
}
}
}
}
然而,如果项目中的锁比较多,那么顺序性就很难满足,而且也很难设计了
定时的锁
1. 思路介绍
前提背景:有两个锁,分别设为代号 1 和 2.
故事主线:线程 A 在已经获取锁 1,且在等待获取锁 2。线程 B 已经获取锁 2,且在等待获取锁 1。
解决方案:
线程 A 获取锁 2 超出某个时间后,便主动释放当前掌握的锁 1,随后再获取锁 1,再尝试获取锁 2。
应用场景:只有两个锁的场景下可以使用。
2. 额外补充一点
synchronized
关键字代表的内置锁不支持中断、时间设定等功能。所以我们需要使用 JDK1.5 之后(引入了 Lock
接口 —— 显式锁)实现了 Lock
接口的类(比如 ReentrantLock
)
死锁诊断 —— 通过线程转储信息分析死锁
线程转储
初步看到这个名词是一脸懵逼的
- 线程的栈的调用信息
- 线程拥有哪些锁
- 线程在哪个栈帧获取这些锁
- 线程被阻塞时在等待哪一个锁
JVM 通过等待关系图(什么是等待关系图?)进行搜索循环找出死锁,并得到是哪个锁以及是哪个线程
个人对等待关系图的理解(并未看过具体实现,所以只是猜测)
等待关系图:
暂时不清楚实现原理
,目前个人理解是锁资源与线程可以构成一幅有向图,然后利用深度遍历去寻找这个图是否存在环,如果存在环,那么就有死锁,环上的节点(包括锁资源节点与线程节点)就是发生死锁的相关线程和具体锁
Lock 显式锁与 synchronized 内置锁
注意 JDK1.5 之前是不支持 Lock显式锁
的 以上信息。JDK6 增加了对 Lock显式锁
转储信息的支持,但无法精确到栈帧相关
其他活跃性危险
饥饿
可能造成原因
- 线程想得到资源,却得不到(资源可能被占用)
- 线程优先级过低,得不到 CPU 资源
但是 Java 线程优先级不能作为一个标准:优先级高则一定比低优先级占用 CPU 时间更长。(可能会将不同优先级映射到 OS 下的同一个优先级上)
活锁
不再讨论活锁的问题
信号丢失
最后再讨论一下信号丢失的例子
错误示范
在这里省略了其他代码,只看等待与唤醒两个操作
public class SignalMissDemo {
public synchroinzed void entryWaitState() {
wait();
// 等待被唤醒,然后做某些操作
}
public synchronized void notfiyObject() {
notifyAll();
}
}
信号的丢失:关键在于对象还未进入 wait
状态,notifyAll
方法就已经被调用,所以会错失这个信号
改进
public class SignalMissDemo {
/**
* 用signal来记录是否有信号量
*/
private volatile boolean signal = false;
public synchroinzed void entryWaitState() {
while(!signal){
wait();
}
// 等待被唤醒,然后做某些操作
}
public synchronized void notfiyObject() {
notifyAll();
signal= true;
}
}
最后送给作为学生党的自己
关于死锁方面,任何一本操作系统上经典书籍一定会讲
- 产生死锁的四个必要条件
- 死锁的检测与恢复
- 单类资源:有向图的环检测
- 多类资源:向量判断法
- 以及各种不靠谱恢复法
- 死锁的避免与银行家算法
- 资源轨迹图(其实就是访问顺序不一致问题)
- 单个银行家算法
- 死锁的预防(杜绝那个四个必要条件中的任何一个即可)
- 资源可以被多个进程共有
- 资源可被抢占(资源是分为抢占式资源——内存和不可抢占式资源——锁)
- 线程一次性获得所有资源,而不是一个一个获取
- 我们上述讲到的讲各个线程对资源的请求顺序一致
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于