死锁发生在并发中,并且互不相让。
描述:当两个或者多个线程(或者进程)互相持有对方所占有的资源,又不主动释放自己的资源,导致线程陷入阻塞,即为死锁。多个线程如果存在环形的锁依赖关系,可能导致死锁。
例子:有两个人见面分别向对方鞠躬,然而出于绅士风度,两人都不想早于对方起身。
数据库事务发生死锁时,数据库会强行终止事务。但JVM无法自动处理死锁。陷入死锁的线程可能非常重要,不能放弃锁,因此由程序员处理。
死锁发生的几率不高,但是很大。一般发生在高并发场景,影响用户多;根据死锁在系统中位置不同,可能导致系统崩溃、性能降低等。
压力测试无法找出所有潜在的死锁。例如,线程等待锁被调起是随机的;短时间持有锁,是为了降低锁的竞争程度,但却增加了测试中找出潜在死锁的难度。
public class MustDeadLock implements Runnable {
private int flag;
public MustDeadLock(int flag) {
this.flag = flag;
}
private static final Object resourceA = new Object();
private static final Object resourceB = new Object();
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(new MustDeadLock(0));
Thread thread2 = new Thread(new MustDeadLock(1));
thread1.start();
thread2.start();
}
@Override
public void run() {
if (flag == 0) {
synchronized (resourceA) {
System.out.println(Thread.currentThread().getName() + "获取A锁");
try {
TimeUnit.MILLISECONDS.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (resourceB) {
System.out.println(Thread.currentThread().getName() + "获取B锁");
}
}
} else {
synchronized (resourceB) {
System.out.println(Thread.currentThread().getName() + "获取B锁");
try {
TimeUnit.MILLISECONDS.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (resourceA) {
System.out.println(Thread.currentThread().getName() + "获取A锁");
}
}
}
}
}
Thread-1获取B锁
Thread-0获取A锁
Process finished with exit code -1 / 130
控制台正常终止结束信号是0。
转账需要两把锁:转出账户和转入账户,两者都不能同时转入转出。获取两把锁成功,并且余额大于0,则扣除转出账户,增加转入账户余额,是原子操作。互相转钱时可能导致死锁。
四个条件是必要条件,必须同时满足才会发生死锁。
使用jps命令定位线程PID
> jps
25604 MustDeadLock
使用jstack打印死锁信息
> jstack -l 25604
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HSjqYKvc-1583594286763)(931BFB9548734896AD717D643BD3713E)]
jstack无法应对复杂死锁,但是可以用来分析栈信息。
// 使用ThreadMXBean检测死锁
Thread.sleep(1000);
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
long[] deadLockThreads = threadMXBean.findDeadlockedThreads();
if (deadLockThreads != null && deadLockThreads.length != 0) {
for (long deadLockThread : deadLockThreads) {
ThreadInfo threadInfo = threadMXBean.getThreadInfo(deadLockThread);
System.out.println("死锁线程:" + threadInfo.getThreadName());
}
}
平时开发需要提前预防死锁,如果线上出现死锁,很难做到无损失的解决。因此要保存好堆栈信息,然后立刻重启服务器,避免影响用户体验。
死锁的修复策略有以下几种:
思想:避免相反获取锁的顺序。
public class TransferMoneyNotDeadLock implements Runnable {
private int flag;
public TransferMoneyNotDeadLock(int flag) {
this.flag = flag;
}
/**
* 两个账户锁
*/
private static Account account1 = new Account(500);
private static Account account2 = new Account(500);
/**
* 额外的锁,用于hashcode相同时规范获取锁顺序
*/
private static final Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
Thread thread0 = new Thread(new TransferMoneyNotDeadLock(0));
Thread thread1 = new Thread(new TransferMoneyNotDeadLock(1));
thread0.start();
thread1.start();
thread0.join();
thread1.join();
System.out.println("账户1余额:" + account1.blance);
System.out.println("账户2余额:" + account2.blance);
}
@Override
public void run() {
if (flag == 0) {
transMoney(account1, account2, 200);
} else {
transMoney(account2, account1, 200);
}
}
private void transMoney(Account from, Account to, int account) {
// 获取两个账户的hashcode,通过比较大小规范获取锁的顺序
int fromHashCode = System.identityHashCode(from);
int toHashCode = System.identityHashCode(to);
if (fromHashCode < toHashCode) {
synchronized (from) {
synchronized (to) {
transfer(from, to, account);
}
}
} else if (fromHashCode > toHashCode) {
synchronized (to) {
synchronized (from) {
transfer(from, to, account);
}
}
} else { // 此时hashcode值相同
synchronized (lock) {
synchronized (from) {
synchronized (to) {
transfer(from, to, account);
}
}
}
}
}
private void transfer(Account from, Account to, int account) {
if (from.blance < account) {
throw new RuntimeException("账户余额不足");
}
from.blance -= account;
to.blance += account;
System.out.println("成功转账" + account + "元");
}
static class Account {
int blance;
public Account(int blance) {
this.blance = blance;
}
}
}
案例中,使用锁的hashcode值决定锁的顺序,在hash冲突时采用加时赛(使用额外的锁)。如果对象数据有能够标识顺序的字段(例如自增主键),则不需要额外的锁。
实际场景不会直接加锁,可能用到事务保证转账的一致性和原子性,使用消息队列异步处理,即使加锁也会用分布式加锁。
哲学家就餐问题:假设五位哲学家围成一桌,两人中间放置一根筷子。哲学家除了思考就是吃饭,哲学家吃饭时先拿左手边筷子,再拿右手边筷子,此时下一位哲学家吃饭只能等待左手边筷子。如果五位哲学家同时吃饭,同时拿起了左手边筷子,就会导致死锁和资源耗尽。
哲学家就餐问题的解决方案:
描述:以银行借贷分配策略为基础,判断并保证系统处于安全状态
角色
思想:银行的剩余资源满足某个申请的未来需要,并回收资源。以此迭代,不断满足未来需要、回收资源,构成一个安全序列。不满足未来需要,表示不安全。
思想:一段时间检查是否有死锁,如果有,剥夺资源,打破死锁。
【代码演示】使用显式锁Lock中的定时tryLock功能代替内置锁机制。
public class TryLockDeadLock implements Runnable {
private int flag;
public TryLockDeadLock(int flag) {
this.flag = flag;
}
private static final Lock lock1 = new ReentrantLock();
private static final Lock lock2 = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(new TryLockDeadLock(0));
Thread thread2 = new Thread(new TryLockDeadLock(1));
thread1.start();
thread2.start();
}
@Override
public void run() {
for (int i = 0; i < 3; i++) {
if (flag == 0) {
try {
if (lock1.tryLock(1, TimeUnit.SECONDS)) {
System.out.println(Thread.currentThread().getName() + "获取lock1");
TimeUnit.MILLISECONDS.sleep(new Random().nextInt(1000));
if (lock2.tryLock(1, TimeUnit.SECONDS)) {
System.out.println(Thread.currentThread().getName() + "获取lock2,此时拥有两把锁");
lock2.unlock();
lock1.unlock();
break;
} else {
System.out.println(Thread.currentThread().getName() + "获取lock2失败,重试");
lock1.unlock();
TimeUnit.MILLISECONDS.sleep(new Random().nextInt(1000));
}
} else {
System.out.println(Thread.currentThread().getName() + "获取lock1失败,重试");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
try {
if (lock2.tryLock(1, TimeUnit.SECONDS)) {
System.out.println(Thread.currentThread().getName() + "获取lock2");
TimeUnit.MILLISECONDS.sleep(new Random().nextInt(1000));
if (lock1.tryLock(1, TimeUnit.SECONDS)) {
System.out.println(Thread.currentThread().getName() + "获取lock1,此时拥有两把锁");
lock1.unlock();
lock2.unlock();
break;
} else {
System.out.println(Thread.currentThread().getName() + "获取lock1失败,重试");
lock2.unlock();
TimeUnit.MILLISECONDS.sleep(new Random().nextInt(1000));
}
} else {
System.out.println(Thread.currentThread().getName() + "获取lock2失败,重试");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
检测算法:锁的调用链路
恢复方法
进程终止
资源抢占
造成超时的可能性大概有:发生死锁、线程陷入死循环、线程执行慢。
使用并发类发生死锁的概率低。
活锁:线程没有阻塞,始终在运行,但是程序得不到进展,重复做同样的事情。
活锁与死锁效果一样,程序无法正常运行。发生死锁,线程陷入阻塞;活锁不会阻塞,会更加耗费CPU资源。
举例1:类似于死锁例子,有两个人见面分别向对方鞠躬,然而处于绅士风度,两人都不想早于对方起身。因此两个人分别向对方说“你先请”,陷入循环,最终二人仍未起身。
举例2:例如哲学家就餐问题,五位哲学家同时拿起左手边筷子会造成死锁。如果给锁加5秒超时,5秒超时后获取锁失败,哲学家再次同时拿起筷子,循环往复,便是活锁。
public class LiveLock {
public static void main(String[] args) {
People gentleman1 = new People("绅士1");
People gentleman2 = new People("绅士2");
Action getUpAction = new Action(gentleman1);
new Thread(() -> {
try {
gentleman1.doAction(getUpAction, gentleman2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
new Thread(() -> {
try {
gentleman2.doAction(getUpAction, gentleman1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
class Action {
private People executor;
public Action(People executor) {
this.executor = executor;
}
public synchronized void execute() {
System.out.println(executor.getName() + " 执行起身动作");
}
public People getexecutor() {
return executor;
}
public void setexecutor(People executor) {
this.executor = executor;
}
}
class People {
private String name; // 姓名
private boolean isBendOver; // 是否弯腰
public People(String name) {
this.name = name;
this.isBendOver = true;
}
public void doAction(Action action, People other) throws InterruptedException {
while (isBendOver) {
// 如果对方正在起身,等对方先起身
if (action.getexecutor() != this) {
TimeUnit.MILLISECONDS.sleep(1);
continue;
}
// 如果对方仍在弯腰,请对方起身
if (other.isBendOver) {
// 使用随机元素打破活锁
// Random rand = new Random();
// if (other.isBendOver && rand.nextInt(10) < 9) {
System.out.println(name + "对" + other.getName() + "说,您先起身。");
action.setexecutor(other);
continue;
}
action.execute();
isBendOver = false;
System.out.println(name + ":我起身了");
action.setexecutor(other);
}
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
活锁解决方法:加入随机因素,减小获取锁碰撞的概率。
在消息队列中,如果依赖服务出现问题,例如宕机,消息处理失败,由于消息在消息队列头部,会导致消息队列一直重试发送消息。
解决方法:(1)放到消息队列末尾;(2)限制重试上限,超出上限的数据可以持久化到数据库中,触发报警机制,等待调度再次调起。
出现线程饥饿可能的原因:
线程饥饿时,无法得到很好地执行,导致系统响应性差。
结合系统优先级描述。