并发与活跃性危险问题

本贴最后更新于 2818 天前,其中的信息可能已经水流花落

并发与活跃性危险问题

最近几天重新整理了下 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

ResourceAResourceB 中确实每个方法只锁住了自己的对象,但是依然可能造成死锁问题 —— 因为 ResourceAResourceB 进行了访问

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显式锁 转储信息的支持,但无法精确到栈帧相关

其他活跃性危险

饥饿

可能造成原因

  1. 线程想得到资源,却得不到(资源可能被占用)
  2. 线程优先级过低,得不到 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;
	}
}

最后送给作为学生党的自己

关于死锁方面,任何一本操作系统上经典书籍一定会讲

  • 产生死锁的四个必要条件
  • 死锁的检测与恢复
    • 单类资源:有向图的环检测
    • 多类资源:向量判断法
    • 以及各种不靠谱恢复法
  • 死锁的避免与银行家算法
    • 资源轨迹图(其实就是访问顺序不一致问题)
    • 单个银行家算法
  • 死锁的预防(杜绝那个四个必要条件中的任何一个即可)
    • 资源可以被多个进程共有
    • 资源可被抢占(资源是分为抢占式资源——内存和不可抢占式资源——锁)
    • 线程一次性获得所有资源,而不是一个一个获取
    • 我们上述讲到的讲各个线程对资源的请求顺序一致
  • Java

    Java 是一种可以撰写跨平台应用软件的面向对象的程序设计语言,是由 Sun Microsystems 公司于 1995 年 5 月推出的。Java 技术具有卓越的通用性、高效性、平台移植性和安全性。

    3187 引用 • 8213 回帖
  • 并发编程
    2 引用 • 6 回帖
  • resourceb
    1 引用

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...