死锁
两个或多个线程相互等待对方释放锁,则会出现死锁现象。java虚拟机没有检测,也没有采用措施来处理死锁情况,所以多线程编程是应该采取措施避免死锁的出现。一旦出现死锁,整个程序即不会发生任何异常,也不会给出任何提示,只是所有线程都处于堵塞状态。
代码如下,
public class Main {
static Object lockA = new Object();
static Object lockB = new Object();
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
synchronized (lockA) {
System.out.println("第一个线程调用A");
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lockB) {
System.out.println("第一个线程调用B");
}
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
synchronized (lockB) {
System.out.println("第二个线程调用B");
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lockA) {
System.out.println("第二个线程调用A");
}
}
}
}).start();
}
}
两个线程都无法释放当前获得到的锁,并且还需要等待获取其他资源,但是其他资源被其他线程锁住,相互持有,无法释放。
形成死锁的条件
- 互斥条件:线程使用的资源必须至少有一个是不能共享的(至少有锁);
- 请求与保持条件:至少有一个线程必须持有一个资源并且正在等待获取一个当前被其它线程持有的资源(至少两个线程持有不同锁,又在等待对方持有锁);
- 非剥夺条件:分配资源不能从相应的线程中被强制剥夺(不能强行获取被其他线程持有锁);
- 循环等待条件:第一个线程等待其它线程,后者又在等待第一个线程(线程A等线程B;线程B等线程C;...;线程N等线程A。如此形成环路)。
死锁预防
- 加锁顺序
- 加锁时限
- 死锁检测(jconsole等客户端有此功能,或者,每当一个线程获得了锁,会在线程和锁相关的数据结构中(map等等)将其记下,除此之外,每当有线程请求锁,也需要记录在这个数据结构中,最终对这个数据结构进行检测,)
死锁的破除
打破死锁形成的四个条件之一,就可以破除
活锁
任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试,失败,尝试,失败。活锁和死锁的区别在于,处于活锁的实体是在不断的改变状态,所谓的“活”, 而处于死锁的实体表现为等待;活锁有可能自行解开,死锁则不能
生活中的典型例子: 两个人在窄路相遇,同时向一个方向避让,然后又向另一个方向避让,如此反复。
通信中也有类似的例子,多个用户共享信道(最简单的例子是大家都用对讲机),同一时刻只能有一方发送信息。发送信号的用户会进行冲突检测, 如果发生冲突,就选择避让,然后再发送。 假设避让算法不合理,就导致每次发送,都冲突,避让后再发送,还是冲突。
计算机中的例子:两个线程发生了某些条件的碰撞后重新执行,那么如果再次尝试后依然发生了碰撞,长此下去就有可能发生活锁。
活锁打破方式
解决协同活锁的一种方案是调整重试机制。
比如引入一些随机性。例如如果检测到冲突,那么就暂停随机的一定时间进行重试。这回大大减少碰撞的可能性。 典型的例子是以太网的CSMA/CD检测机制。
饥饿
我们知道多线程执行中有线程优先级这个东西,优先级高的线程能够插队并优先执行 【高优先级,占用CPU时间片更多】,这样如果优先级高的线程一直抢占优先级低线程的资源,导致低优先级线程无法得到执行,这就是饥饿。
当然还有一种饥饿的情况,一个线程一直占着一个资源不放而导致其他线程得不到执行,与死锁不同的是饥饿在以后一段时间内还是能够得到执行的,如那个占用资源的线程结束了并释放了资源。
通过代码来模拟,优先级越高的线程,越容易被调度
public static void main(String[] args) throws InterruptedException {
Map<Integer,Integer> counterMap=new ConcurrentHashMap<Integer,Integer>();
Random rd=new Random(System.currentTimeMillis());
for(int i=0;i<100;i++) {
Thread tmp=new Thread(new Runnable() {
@Override
public void run() {
while(true) {
int priority=Thread.currentThread().getPriority();
Integer tm=counterMap.get(priority);
if( tm==null) tm=0;
counterMap.put(priority,tm+1);
}
}
});
tmp.setPriority(rd.nextInt(Thread.MAX_PRIORITY)+1);
tmp.start();
}
System.out.println(counterMap);
}
无尽的while
循环,统计优先级的执行次数,这样,伴随着程序运行过程中,优先级越高的,执行的次数越多,某个时间点的counterMap中的数据如下,{1=22327, 2=20698, 3=207514, 4=149923, 5=285397, 6=370211, 7=208801, 8=246323, 9=173757, 10=340233}
无锁
无锁,即没有对资源进行锁定,即所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功。无锁典型的特点就是一个修改操作在一个循环内进行,线程会不断的尝试修改共享资源,如果没有冲突就修改成功并退出否则就会继续下一次循环尝试。所以,如果有多个线程修改同一个值必定会有一个线程能修改成功,而其他修改失败的线程会不断重试直到修改成功。之前的文章我介绍过JDK的CAS 原理及应用即是无锁的实现