1.什么是死锁
死锁发生于并发中,当两个或更多线程互相持有对方的资源,但又不主动释放,令两个线程都无法前进,从而陷入无尽的等待之中的情况就是死锁。
2.死锁的影响
死锁在不同系统中的影响是不同的,这取决于系统对死锁的处理能力
在数据库中可以对事物进行检测和放弃,如果发生抢占的情况可以指定某个事务放弃,这样可以解决死锁。但是在JVM中不具备自动处理的能力。
死锁发生的概率比较低,但是产生的危害比较大,在多线程并发情况下,影响的用户比较多。
死锁会导致系统整体崩溃,子系统崩溃,性能降低。并且压力测试无法发现所有潜在的死锁
3.发生死锁的例子
3.1 看程序停止的信号
/**
* 必定发生死锁的情况
*/
public class MustDeadLock implements Runnable {
int flag = 1;
static Object o1 = new Object();
static Object o2 = new Object();
@Override
public void run() {
System.out.println("flag = " + flag);
if (flag == 1){
synchronized (o1){
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("thread1持有o1");
synchronized (o2){
System.out.println("thread1持有o2");
}
}
}
if (flag == 0){
synchronized (o2){
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("thread2持有o2");
synchronized (o1){
System.out.println("thread2持有o1");
}
}
}
}
public static void main(String[] args) {
MustDeadLock r1 = new MustDeadLock();
MustDeadLock r2 = new MustDeadLock();
r1.flag = 1;
r2.flag = 0;
Thread thread1 = new Thread(r1);
Thread thread2 = new Thread(r2);
thread1.start();
thread2.start();
}
}
复制代码
3.2 银行转账发生死锁
前提条件:需要两把锁(将转账和被转账的线程锁住,保证中间不被干扰),在成功获取两把锁的情况下,且余额大于0,则扣除转账人,增加收款人的余额,是原子操作。 顺序相反导致死锁。
/**
* 转账的时候遇到了死锁,一旦打开注释,便会发生死锁
*/
public class TransferMoney implements Runnable {
int flag = 1;
//a和b没人都有500
static Account a = new Account(500);
static Account b = new Account(500);
public static void main(String[] args) throws InterruptedException {
TransferMoney r1 = new TransferMoney();
TransferMoney r2 = new TransferMoney();
r1.flag = 1;
r2.flag = 0;
Thread t1 = new Thread(r1);
Thread t2 = new Thread(r2);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("a的余额为:" + a.balance);
System.out.println("b的余额为:" + b.balance);
}
@Override
public void run() {
if (flag == 1){
//a向b转200
transferMoney(a, b, 200);
}
if (flag == 0){
//b向a转200
transferMoney(b, a, 200);
}
}
public static void transferMoney(Account from, Account to, int amount){
synchronized (from){
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (to){
if (from.balance - amount < 0){
System.out.println("转账失败,余额不足");
}
from.balance -= amount;
to.balance += amount;
System.out.println("成功转账:" + amount + "元");
}
}
}
//账户
static class Account{
int balance;
public Account(int balance) {
this.balance = balance;
}
}
}
复制代码
如果注释掉sleep()
就不会发生死锁,但是添加了sleep()
在这500ms中就会发生死锁。
3.3 模拟多人转账
/**
* 多人转账情况下发生死锁
*/
public class MultiTransferMoney {
//50个账户
private static final int NUM_ACCOUNTS = 50;
//每个账户有1000元
private static final int NUM_MONEYS = 1000;
//转账次数
private static final int NUM_ITERATIONS = 1000000;
//操作账户的人数
private static final int NUM_THREADS = 20;
public static void main(String[] args) {
Random random = new Random();
TransferMoney.Account[] accounts = new TransferMoney.Account[NUM_ACCOUNTS];
for (int i = 0; i < accounts.length; i++) {
accounts[i] = new TransferMoney.Account(NUM_MONEYS);
}
class TransferThread extends Thread {
@Override
public void run() {
for (int i = 0; i < NUM_ITERATIONS; i++) {
int fromAccount = random.nextInt(NUM_ACCOUNTS);
int toAccount = random.nextInt(NUM_ACCOUNTS);
int amount = random.nextInt(NUM_MONEYS);
TransferMoney.transferMoney(accounts[fromAccount], accounts[toAccount], amount);
}
}
}
for (int i = 0; i < NUM_THREADS; i++) {
new TransferThread().start();
}
}
}
复制代码
死锁发生的概率随着账户数量的减少而增加
4.发生死锁的4个必要条件(缺一不可)
- 互斥:当
thread1
拿到lockA
后,其他线程就无法获取到lockA
。 - 请求与保持:当
thread1
拿到lockA
后,还一定要获取到lockB
。 - 不可剥夺:在数据库中可以避免发生死锁是因为数据库自身可以剥夺某个事务,这样就会避免死锁,但是在Java中不可剥夺。
- 循环等待:在两个线程中两个线程相互等待,在多个线程中每个线程首尾相接形成环路,也就是发生循环等待。
5.如何定位死锁
5.1 ThreadMXBean代码演示
/**
* 用ThreadMXBean检测死锁
*/
public class ThreadMXBeanDetection implements Runnable {
int flag = 1;
static Object o1 = new Object();
static Object o2 = new Object();
@Override
public void run() {
if (flag == 1){
synchronized (o1){
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o2){
System.out.println("t1持有o2");
}
}
}
if (flag == 0){
synchronized (o2){
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o1){
System.out.println("t2持有o1");
}
}
}
}
public static void main(String[] args) throws InterruptedException {
ThreadMXBeanDetection d1 = new ThreadMXBeanDetection();
ThreadMXBeanDetection d2 = new ThreadMXBeanDetection();
d1.flag = 1;
d2.flag = 0;
Thread t1 = new Thread(d1);
Thread t2 = new Thread(d2);
t1.start();
t2.start();
Thread.sleep(1000);
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
long[] deadlockedThreads = threadMXBean.findDeadlockedThreads();
if (deadlockedThreads != null && deadlockedThreads.length > 0){ //发现了死锁
for (int i = 0; i < deadlockedThreads.length; i++) {
//通过死锁的线程ID获取线程信息
ThreadInfo threadInfo = threadMXBean.getThreadInfo(deadlockedThreads[i]);
System.out.println("发现死锁" + threadInfo.getThreadName());
}
}
}
}
复制代码
发现死锁后可以通过报警或者日志的方式对死锁进行修复。
6.如何修复死锁
6.1 线上发生死锁怎么办
对于线上问题一定要防患于未然,因为在线上想要没有任何损失的修复死锁几乎是不可能的了。所以需要
- 先将“案发现场”保存下来然后立刻重启服务器。
- 暂时保证线上服务的安全,然后利用刚才留下的信息立刻定位死锁,进行修复,然后重新发版。
6.2 修复死锁的策略
- 避免策略:哲学家就餐的换手方案、转账换序方案 思路:避免相反的获取锁的顺序
- 检测与恢复策略:一段时间内检查是否发生死锁,如果发生死锁,对资源进行剥夺,从而修复死锁。
- 鸵鸟策略:鸵鸟这种动物在遇到危险时会把头埋在地上,这样就看不到危险了。这也就是说在发生可能性低的时候可以暂时忽略掉死锁,等到发生后进行人工修复。
6.2.1 避免策略的使用
/**
* 转账的时候遇到了死锁,一旦打开注释,便会发生死锁
*/
public class TransferMoney implements Runnable {
int flag = 1;
//a和b没人都有500
static Account a = new Account(500);
static Account b = new Account(500);
static Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
TransferMoney r1 = new TransferMoney();
TransferMoney r2 = new TransferMoney();
r1.flag = 1;
r2.flag = 0;
Thread t1 = new Thread(r1);
Thread t2 = new Thread(r2);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("a的余额为:" + a.balance);
System.out.println("b的余额为:" + b.balance);
}
@Override
public void run() {
if (flag == 1){
//a向b转200
transferMoney(a, b, 200);
}
if (flag == 0){
//b向a转200
transferMoney(b, a, 200);
}
}
public static void transferMoney(Account from, Account to, int amount){
class Helper{
public void transfer(){
if (from.balance - amount < 0){
System.out.println("转账失败,余额不足");
}
from.balance -= amount;
to.balance += amount;
System.out.println("成功转账:" + amount + "元");
}
}
//获取转入和转出的hash值
int fromHash = System.identityHashCode(from);
int toHash = System.identityHashCode(to);
if (fromHash < toHash){ //通过hash值保证了获取锁的顺序
synchronized (from) {
synchronized (to) {
new Helper().transfer();
}
}
} else if (fromHash > toHash){
synchronized (to) {
synchronized (from) {
new Helper().transfer();
}
}
} else { //fromHash == toHash
synchronized (lock){ //谁先拿到lock谁就先执行
synchronized (to) {
synchronized (from) {
new Helper().transfer();
}
}
}
}
}
//账户
static class Account{
int balance;
public Account(int balance) {
this.balance = balance;
}
}
}
复制代码
通过修改transfer方法计算转出和转入的hash值,通过hash值作比较来设定锁的获取顺序,这样可以避免死锁的发生。
7.哲学家就餐问题
7.1 什么是哲学家就餐问题
每个哲学家吃饭需要先拿起左手(或右手的叉子)再拿起右手(或左手)的叉子才可以吃饭,等待自己用完了放回原处,叉子再供另外的人使用(暂时不考虑卫生问题:))。
死锁:如果每个人都同时拿起了左边的叉子,这样就无法拿到右手边的叉子,这样就造成了等待的问题
/**
* 描述: 演示哲学家就餐问题导致的死锁
*/
public class DiningPhilosophers {
public static class Philosophers implements Runnable {
private Object leftChopstick;
private Object rightChopstick;
public Philosophers(Object leftChopstick, Object rightChopstick) {
this.leftChopstick = leftChopstick;
this.rightChopstick = rightChopstick;
}
@Override
public void run() {
try {
while (true) {
doAction("Thinking");
synchronized (leftChopstick) {
doAction("Picked up left chopstick");
synchronized (rightChopstick) {
doAction("Picked up right chopstick -eating");
doAction("Put down right chopstick");
}
doAction("Put down left chopstick");
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private void doAction(String action) throws InterruptedException {
System.out.println(Thread.currentThread().getName() + " " + action);
Thread.sleep((long) (Math.random() * 10));
}
}
public static void main(String[] args) {
//设置哲学家的人数
Philosophers[] philosophers = new Philosophers[5];
//设置筷子的数量,数量与哲学家人数相同
Object[] chopsticks = new Object[philosophers.length];
for (int i = 0; i < chopsticks.length; i++) {
chopsticks[i] = new Object();
}
for (int i = 0; i < philosophers.length; i++) {
Object leftChopstick = chopsticks[i];
Object rightChopstick = chopsticks[(i+1)%chopsticks.length];
philosophers[i] = new Philosophers(leftChopstick, rightChopstick);
new Thread(philosophers[i], "哲学家"+(i+1)+"号").start();
}
}
}
复制代码
导致死锁的一大特征就是:每个哲学家的左手都拿着筷子,右手无法获取筷子。
7.2 解决哲学家死锁问题的4种办法
- 服务员检查(避免策略)
- 改变哲学家拿叉子的顺序(避免策略)
- 餐票(将餐票数量设置为人数-1)(避免策略)
- 领导调节(检测与恢复策略)
7.2.1 实现换手策略
public static void main(String[] args) {
//设置哲学家的人数
Philosophers[] philosophers = new Philosophers[5];
//设置筷子的数量,数量与哲学家人数相同
Object[] chopsticks = new Object[philosophers.length];
for (int i = 0; i < chopsticks.length; i++) {
chopsticks[i] = new Object();
}
for (int i = 0; i < philosophers.length; i++) {
Object leftChopstick = chopsticks[i];
Object rightChopstick = chopsticks[(i + 1) % chopsticks.length];
if (i == philosophers.length - 1) { //让这个哲学家换手,避免形成环路
philosophers[i] = new Philosophers(rightChopstick, leftChopstick);
}else{
philosophers[i] = new Philosophers(leftChopstick, rightChopstick);
}
new Thread(philosophers[i], "哲学家" + (i + 1) + "号").start();
}
}
复制代码
7.2.2 死锁检测与恢复策略
检测算法:锁的调用链路图
- 允许发生死锁
- 每次调用锁都记录
- 定期检查锁的调用链路图是否形成环路
- 一旦发生死锁,就用死锁恢复机制进行恢复
恢复方法1:进程终止
逐个终止线程,直到死锁消除
终止顺序:
- 1.优先级(是前台交互还是后台处理)
- 2.已占用资源,还需要的资源
- 3.已经运行的时间
恢复方法2:资源抢占
把每个分发出去的锁收回来
让线程回退几步,这样就不用结束整个线程,成本比较低,但是这样可能会造成资源一直被抢占,造成饥饿
8.避免死锁的有效手段
8.1 设置超时时间(退一步海阔天空)
Lock的tryLock(long timeout, TimeUnit unit)
synchronized不具备尝试锁的能力
/**
* 描述: 用tryLock来避免死锁
*/
public class TryLockDeadLock implements Runnable {
int flag;
static Lock lock1 = new ReentrantLock();
static Lock lock2 = new ReentrantLock();
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (flag == 1) {
try {
if (lock1.tryLock(800, TimeUnit.MILLISECONDS)) {
System.out.println("线程1获取到了锁1");
Thread.sleep(new Random().nextInt(1000));
if (lock2.tryLock(800, TimeUnit.MILLISECONDS)) {
System.out.println("线程1获取到了锁2");
System.out.println("线程1成功获取到两把锁");
lock2.unlock();
lock1.unlock();
break;
} else {
System.out.println("线程1尝试获取锁2失败,已重试");
lock1.unlock();
Thread.sleep(new Random().nextInt(1000));
}
} else {
System.out.println("线程1获取锁1已失败,已重试");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if (flag == 0) {
try {
if (lock2.tryLock(3000, TimeUnit.MILLISECONDS)) {
System.out.println("线程2获取到了锁2");
Thread.sleep(new Random().nextInt(1000));
if (lock1.tryLock(3000, TimeUnit.MILLISECONDS)) {
System.out.println("线程2获取到了锁1");
System.out.println("线程2成功获取到两把锁");
lock1.unlock();
lock2.unlock();
break;
} else {
System.out.println("线程2尝试获取锁1失败,已重试");
lock2.unlock();
Thread.sleep(new Random().nextInt(1000));
}
} else {
System.out.println("线程2获取锁2已失败,已重试");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
TryLockDeadLock r1 = new TryLockDeadLock();
TryLockDeadLock r2 = new TryLockDeadLock();
r1.flag = 1;
r2.flag = 0;
new Thread(r1).start();
new Thread(r2).start();
}
}
复制代码
8.2 多使用并发类而不是自己设计锁
ConcurrentHashMap、ConcurrentLinkedQueue、AtomicBoolean等
实际使用时java.util.concurrent.atomic十分有用,简单方便且效率比使用Lock高
多用并发集合少用同步集合,并发集合比同步集合扩展性更好
并发场景需要用到map,首先想到用ConcurrentHashMap
8.3 降低使用锁的粒度,避免使用同一个锁
保护的范围大,效率低,容易发生死锁
8.4 尽量使用同步代码块而非同步方法
使用同步代码块相对于同步方法,缩小了保护的范围,增加了对对象的控制权,降低发生死锁的风险。
8.5 给线程起个有意义的名字
8.6 避免锁的嵌套
例如上面的MustDeadLock类
8.7 分配资源前先看能不能收回来
例如:银行家算法
8.8 专锁专用
尽量不要多个功能使用同一把锁
9.其他活性问题
死锁是最常见的活跃性问题,不过除了刚才的死锁之外,还有一些类似的问题,会导致程序无法顺利执行,统称为活跃性问题。
9.1 活锁
9.1.1 什么是活锁
再回到前面的哲学家就餐问题,发生死锁是因为每个哲学家都是先拿到左手的餐具,永远在等待右手边的餐具(或者相反),这样就会发生死锁。
活锁相对与死锁更加智能一点,这些哲学家同时进入餐厅,同时拿起左边的餐具然后会等待5分钟,然后放下餐具,再等5分钟,又同时拿起餐具,这样也会导致每个哲学家无法吃饭。
换成程序中的话就是说:程序一直在运行,但是属于无用功,白白浪费资源。
9.1.2 活锁的出现
/**
* 描述: 演示活锁问题
*/
public class LiveLock {
static class Spoon{
private Diner onwer;
public Spoon(Diner onwer) {
this.onwer = onwer;
}
public Diner getOnwer() {
return onwer;
}
public void setOnwer(Diner onwer) {
this.onwer = onwer;
}
public synchronized void use(){
System.out.printf("%s has eaten!", onwer.name);
}
}
static class Diner{
private String name;
private boolean isHungry;
public Diner(String name) {
this.name = name;
this.isHungry = true;
}
public void earWith(Spoon spoon, Diner spouse){
while (isHungry){
//自己没有拿到勺子
if (spoon.onwer != this){
//等待伴侣吃饭
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
continue;
}
//如果伴侣是饥饿的
if (spouse.isHungry){
System.out.println(name + ": 亲爱的" + spouse.name +"你先吃吧");
//将勺子给伴侣
spoon.setOnwer(spouse);
continue;
}
//我可以吃饭了
spoon.use();
//吃完了改变hungry的状态
isHungry = false;
System.out.println(name + ": 我吃完了");
//将勺子给伴侣
spoon.setOnwer(spouse);
}
}
}
public static void main(String[] args) {
Diner husband = new Diner("牛郎");
Diner wife = new Diner("织女");
Spoon spoon = new Spoon(husband);
new Thread(new Runnable() {
@Override
public void run() {
husband.earWith(spoon, wife);
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
wife.earWith(spoon, husband);
}
}).start();
}
}
复制代码
9.1.3 如何解决活锁问题
出现活锁的原因:重试机制不变,消息队列始终重试,吃饭始终谦让
解决: 加入随机因素
9.1.4 代码演示
/**
* 描述: 演示活锁问题
*/
public class LiveLock {
static class Spoon{
private Diner onwer;
public Spoon(Diner onwer) {
this.onwer = onwer;
}
public Diner getOnwer() {
return onwer;
}
public void setOnwer(Diner onwer) {
this.onwer = onwer;
}
public synchronized void use(){
System.out.printf("%s has eaten!", onwer.name);
}
}
static class Diner{
private String name;
private boolean isHungry;
public Diner(String name) {
this.name = name;
this.isHungry = true;
}
public void earWith(Spoon spoon, Diner spouse){
while (isHungry){
//自己没有拿到勺子
if (spoon.onwer != this){
//等待伴侣吃饭
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
continue;
}
Random random = new Random();
//如果伴侣是饥饿的
if (spouse.isHungry && random.nextInt(10) < 9){ //降低给勺子的几率
System.out.println(name + ": 亲爱的" + spouse.name +"你先吃吧");
//将勺子给伴侣
spoon.setOnwer(spouse);
continue;
}
//我可以吃饭了
spoon.use();
//吃完了改变hungry的状态
isHungry = false;
System.out.println(name + ": 我吃完了");
//将勺子给伴侣
spoon.setOnwer(spouse);
}
}
}
public static void main(String[] args) {
Diner husband = new Diner("牛郎");
Diner wife = new Diner("织女");
Spoon spoon = new Spoon(husband);
new Thread(new Runnable() {
@Override
public void run() {
husband.earWith(spoon, wife);
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
wife.earWith(spoon, husband);
}
}).start();
}
}
复制代码
9.2 饥饿
当线程需要某些资源(例如CPU),却始终得不到
饥饿的原因
- 当某个线程的执行优先级过低,始终得不到CPU资源
- 某个线程一直持有锁,却从不释放锁
- 某程序始终占用某文件的写锁。
饥饿的危害
饥饿可能会导致响应性变差:例如一个线程负责前台的响应,另一条线程负责后台的数据处理,但是由于前台线程优先级比较低始终得不到执行,这样会导致用户体验变差。