### 说明
本篇是继上一篇并发编程未讨论完的内容的续篇。上一篇传送门:
[Java并发编程一万字总结(吐血整理)](https://editor.csdn.net/md/?articleId=104642587)
## 活跃性问题
在上一篇我们讨论并发编程带来的风险的时候,说到其中 一个风险就是活跃性问题。活跃性问题其实就是我们的程序在某些场景或条件下执行不下去了。在这个话题下我们会去了解什么是死锁、活锁以及饥饿,该如何避免这些情况的发生。
### 死锁
我们一般使用加锁来保证线程安全,但是过度地使用加锁,可能导致死锁发生。
**哲学家进餐问题**
“哲学家进餐”问题能很好地描述死锁的场景。5个哲学家去吃火锅,坐在一张圆桌上。它们有5根筷子(不是5双),这5根筷子放在每个人的中间。哲学家时而思考,时而进餐。每个人都要取到一双筷子才能吃到东西,并且在吃完后将筷子放回原处。
![在这里插入图片描述](https://img-blog.csdnimg.cn/20200322142424192.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzM3OTY1MDE4,size_16,color_FFFFFF,t_70)
可以考虑一下这种情况,如果每个人都立即抓住自己左边的筷子,然后等待自己右边的筷子空出来,但同时都不放手自己已经拿到的筷子。会出现什么情况。可以想到,每个人都吃不上火锅了,只等凉凉了。
**什么是死锁**
每个人都拥有其他人需要的资源,同时又等待其他人已经拥有的资源,并且每个人在获得所需资源之前都不会放弃已经拥有的资源。这就是一种死锁。
再使用线程的术语描述一下。在线程A持有锁L并想获得锁M的同时,线程B持有锁M并尝试获取锁L,那么这两个线程将永远地等待下去。
**简单死锁代码示例**
```java
public class LeftRightDeadLock {
private final Object left = new Object();
private final Object right = new Object();
public void leftRight(){
synchronized (left){
synchronized (right){
doSomething();
}
}
}
public void rightLeft(){
synchronized (right){
synchronized (left){
doSomething();
}
}
}
}
```
上面的代码中,如果一个线程执行leftRight()方法,另一个线程调用rightLeft()方法,则会发生死锁。
上面生产死锁的原因是,两个线程视图以不同的顺序来获得相同的锁。如果按照相同的顺序请求锁,那么就不会出现循环的加锁依赖性,因此就不会产生死锁。
**产生死锁的四个条件**
有个叫Coffman的牛人帮我们总结了产生死锁的四个条件:
1. 互斥,共享资源X和Y只能被一个线程占用
2. 占用且等待,线程T1已经获得了共享资源X,在等待共享资源Y的时候,不释放共享资源X;
3. 不可抢占,其他线程不能强行抢占线程T1占用的资源;
4. 循环等待,线程T1等待线程T2占有的资源,线程T2等待线程T1占有的资源,就是循环等待。
反过来说,我们只要破坏掉四个条件中的一个,就可以避免死锁的发生。
首先第一个互斥条件没法破坏,因为加锁就是互斥的语义。
1. 对于“占用且等待”的条件,我们可以一次性申请所有资源;
2. 对于“不可抢占”这个条件,占用部分资源的线程在申请其他资源时,如果申请不到,可以主动释放它占有的资源。
3. 对于“循环等待”这个条件,可以按照固定的顺序申请资源,所有线程都按照规定的顺序获得锁,这样就不存在循环等待了。
### 活锁
活锁是另一种形式的活跃性问题,该问题尽管不会阻塞线程,但也不能继续执行下去,因为线程将不断重复执行相同的操作,而且总会失败。
当多个相互协作的线程都对彼此进行响应从而修改各自的状态,并使得任何一个线程都无法继续执行时,就发生了活锁。就比如路上两个人相遇,出于礼貌,都给对方让路,结果每次都碰到一起。
要解决这种活锁问题,需要在重试机制中加入随机性。比如,在网络上,两台机器使用相同的载波来发送数据包,那么这些数据包就会发生冲突。这两台机器都检查到了冲突,并都在稍后再次重发。如果二者都选择了在1秒后重试,那么又会发生冲突,并且不断地冲突下去,因而即使有大量闲置的带宽,也无法将数据包发送出去。**为避免这种情况的发生,需要让他们分别等待一段随机的时间,这样就能避免活锁的发生了。**
### 饥饿
**“饥饿”就是当线程由于无法访问它所需要的资源而不能继续执行时的场景。**所谓“不患寡而患不均”。当某些线程一直获取不到CPU执行资源的时候,就发生了“饥饿”。
一些容易导致饥饿的场景:
1. 在应用中对Java线程优先级的使用不当。(因为JVM会将Thread API中的10个优先级映射到操作系统的调度优先级上,这就可能存在两个不同的优先级被映射到了操作系统层的同一个优先级,因此尽量不要改变线程优先级)
2. 持有锁的线程,如果执行的时间过程或者存在无限循环,也可能导致“饥饿”问题。
解决“饥饿”问题的一般方案就是使用公平锁(注意synchronized术语非公平锁)。
## JUC工具类库
Java并发包给我们提供了非常丰富的构建并发程序的基础模块,例如线程安全容器类、同步工具类,阻塞队列等。
**这些工具类都在java.util.concurrent包下面,所以简称J.U.C工具包。**
### 同步容器和并发容器
**同步容器类有哪些**
主要有Vector和Hashtable,这两个都是早期JDK的一部分,还有一些封装器类是由Collections.sychronizedXxx等工厂方法创建的。
这些类实现线程安全的方式都是:将他们的状态封装起来,并对每个公有方法都进行同步,也就是使用synchronized内置锁的方式,使得每次只有一个线程能访问容器的状态。
**同步容器类的问题**
同步容器类虽然是线程安全的类,但是在某些场景下可能需要额外的客户端加锁来保护复合操作的线程安全性。比如迭代(反复访问元素,直到遍历完容器中的所有元素)、条件运算(如果没有则添加)。下面给出一个示例说明下:
```java
public class GetLastElement implements Runnable{
private List