Java内存模型——底层原理
1、死锁是什么?有什么危害?
一定是发生在并发中;
互不相让:当两个(或更多)线程(或进程)相互持有对方所要的资源,又不主动释放,导致所有人都无法继续前进,导致程序陷入无尽的阻塞,这就是死锁。
1.1、死锁的影响
死锁的影响在不同的系统中是不一样的,这取决于系统对死锁的处理能力;数据库中:可以检测并放弃事务;JVM中:无法自动处理。
死锁发生几率不高但危害大,不一定发生,但是遵守墨菲定律;一旦发生,多是高并发场景,影响用户多;整个系统崩溃,子系统崩溃、性能降低;压力测试无法找出潜在的死锁。
1.2、发生死锁的例子(*)
public class MustDeadLock implements Runnable {
int flag = 1;
static Object o1 = new Object();
static Object o2 = new Object();
public static void main(String[] args) {
MustDeadLock r1 = new MustDeadLock();
MustDeadLock r2 = new MustDeadLock();
r1.flag = 1;
r2.flag = 0;
Thread t1 = new Thread(r1);
Thread t2 = new Thread(r2);
t1.start();
t2.start();
}
@Override
public void run() {
System.out.println("flag = " + flag);
if (flag == 1) {
synchronized (o1) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o2) {
System.out.println("线程1成功拿到两把锁");
}
}
}
if (flag == 0) {
synchronized (o2) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o1) {
System.out.println("线程2成功拿到两把锁");
}
}
}
}
}
必定发生死锁的情况:
a、当类的对象flag=1时(r1),先锁定o1,睡眠500ms,然后锁定o2;
b、而r1在睡眠的时候另一个flag = 0的对象(r2)线程启动,先锁定o2,睡眠500ms,等待r1释放O1;
c、r1、r2相互等待,都需要对方锁定的资源才能继续执行,从而死锁。
1.3、实际生产中的例子-转账
public class TransferMoney implements Runnable {
int flag = 1;
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) {
transferMoney(a, b, 200);
}
if (flag == 0) {
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 = to.balance + amount;
System.out.println("成功转账" + amount + "元");
}
}
}
static class Account {
public Account(int balance) {
this.balance = balance;
}
int balance;
}
}
1.4、死锁发生的4个必要条件
- 互斥条件:一个资源每次只能被一个进程使用,即在一段时间内某 资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待
- 请求与保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源 已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。
- 不剥夺条件:进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能 由获得该资源的进程自己来释放(只能是主动释放)。
- 循环等待条件:若干进程间形成首尾相接循环等待资源的关系。
四个缺一不可。
1.5、如何定位死锁
jstack命令行,首先使用命令行查看到java的pid:jps;jstack pid。
2、修复死锁的策略
2.1、线上发生死锁应该怎么办?
- 首先保存案发现场,然后立刻重启服务器(使用java相应的命令把整个堆栈信息保存下来),不能进一步影响用户体验;
- 暂时保证线上服务的安全,然后再利用刚才保存的信息,排查死锁,修改代码,重新发版。
2.2、常见修复策略
- 避免策略:哲学家就餐的换手方案、转账换序方案,思路:避免相反的获取锁的顺序;
public class TransferMoneyFix implements Runnable {
int flag = 1;
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 {
TransferMoneyFix r1 = new TransferMoneyFix();
TransferMoneyFix r2 = new TransferMoneyFix();
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) {
transferMoney(a, b, 200);
}
if (flag == 0) {
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 = to.balance + amount;
System.out.println("成功转账" + amount + "元");
}
}
//利用hash排序————hash值只要对象不变,hash值就不会变
int fromHash = System.identityHashCode(from);
int toHash = System.identityHashCode(to);
if (fromHash < toHash) {
synchronized (from) {
synchronized (to) {
new Helper().transfer();
}
}
} else if (fromHash > toHash) {
synchronized (to) {
synchronized (from) {
new Helper().transfer();
}
}
} else { //考虑hash碰撞 两个hash值相等(极少出现)
synchronized (lock) {
synchronized (to) {
synchronized (from) {
new Helper().transfer();
}
}
}
}
}
static class Account {
public Account(int balance) {
this.balance = balance;
}
int balance;
}
}
- 检测与恢复策略:一段时间检测是否有死锁,如果有就剥夺某一个资源,来打开死锁
- 鸵鸟策略:如果我们发生死锁的概率极其低,那么我们就直接忽略它,直到死锁发生的时候,在人工修复。
2.2.1、哲学家就餐的换手方案
死锁
public class DiningPhilosophers {
public static class Philosopher implements Runnable {
private Object leftChopstick;
private Object rightChopstick;
public Philosopher(Object leftChopstick, Object rightChopstick) {
this.leftChopstick = leftChopstick;
this.rightChopstick = rightChopstick;
}
@Override
public void run() {
while (true) {
//思考
try {
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) {
Philosopher[] philosophers = new Philosopher[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 Philosopher(leftChopstick, rightChopstick);
new Thread(philosophers[i], "哲学家" + (i + 1) + "号").start();
}
}
}
解决方案:
- 服务员检查(避免策略);
- 改变一个哲学家拿叉子的顺序(避免策略)
- 餐票(避免策略);
- 领导调节(检测与恢复策略)。
FIX
public class DiningPhilosophersFix {
public static class Philosopher implements Runnable {
private Object leftChopstick;
private Object rightChopstick;
public Philosopher(Object leftChopstick, Object rightChopstick) {
this.leftChopstick = leftChopstick;
this.rightChopstick = rightChopstick;
}
@Override
public void run() {
while (true) {
//思考
try {
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) {
Philosopher[] philosophers = new Philosopher[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 Philosopher(rightChopstick, leftChopstick);
} else {
philosophers[i] = new Philosopher(leftChopstick, rightChopstick);
}
new Thread(philosophers[i], "哲学家" + (i + 1) + "号").start();
}
}
}
2.2.2、死锁的检测与恢复策略
- 检测算法:锁的调度链路图
- 恢复策略
- 恢复方法1:进程终止,逐个终止线程,直到死锁消除,终止顺序:a、优先级(是前台交互还是后台处理);b、已占用资源、还需要的资源;c、已经运行时间
- 恢复方法2:资源抢占,把已经分发出去的锁给收回来;让线程回退几步,这样就不用结束整个线程,成本比较低;缺点:可能同一个线程一直被抢占,那就造成饥饿。
2.2.3、实际工程中如何有效避免死锁
我上面说了产生死锁的四个必要条件,为了避免死锁,我们只要破坏产生死锁的四个条件中的其中一个就可以了。现在我们来挨个分析一下:
-
破坏互斥条件 :这个条件我们没有办法破坏,因为我们用锁本来就是想让他们互斥的(临界资源需要互斥访问)。
-
破坏请求与保持条件 :一次性申请所有的资源。
-
破坏不剥夺条件 :占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
-
破坏循环等待条件 :靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。
-
设置超时时间:
- Lock的tryLock(long timeout, TimeUnit unit)
- synchronized不具备尝试锁的能力
- 造成超时的可能性很多:发生死锁、线程陷入死循环、线程执行很慢。
-
多使用并发类而不是自己设计锁:
- ConcurrentHashMap、ConcurrentLinkedQueue、AtomicBoolean等;
- 实际应用中 java.util.concurrent.atomic 十分有用,简单方便且效率比使用Lock更高;
- 多用并发集合少用同步集合,并发集合比同步集合的可扩展性更好;
- 并发场景需要用到map。首先想到用ConcurrentHashMap。
-
尽量降低锁的使用粒度:用不同的锁而不是一个锁;
-
如果能使用同步代码块,就不使用同步方法:自己指定锁对象;
-
给你的线程起个有意义的名字:debug和排查时事半功倍,框架和JDK都遵守这个最佳实践;
-
避免锁的嵌套:MustDeadLock类
-
分配资源前先看能不能收回来:银行家算法
-
尽量不要几个功能使用同一把锁:专锁专用
3、其他活性故障
死锁是最常见的活跃性问题,不过除了刚才的死锁之外,还有一些类似的问题,会导致程序无法顺利执行,统称为活跃性问题。
3.1、活锁(LiveLock)
什么是活锁:线程之间不停的在运行,但是程序无法进一步的推进。
例如:在完全相同的时刻进入餐厅,并同时拿起左边的餐叉,那么这些哲学家就会等待五分钟,同时放下手中的餐叉,在等五分钟,又同时拿起这些餐叉。
特点:
- 虽然线程并没有阻塞,也始终在运行(所以叫做“活锁”,线程是“活”的),但是程序却得不到进展,因为线程始终重复做同样的事。
- 如果这里死锁,那么就是这里两个人都始终一动不动,直到对方先抬头,他们之间不在说话了,只是等待
- 如果发生活锁,那么这里的情况就是,双方都不听的对对方说“你先起来吧,你先起来吧”,双方一直在说话在运行,在消耗资源。
- 死锁和活锁的结果都是一样的,就是谁都不能先抬头。
public class LiveLock {
static class Spoon {
private Diner owner; //就餐者
public synchronized void use() {
System.out.printf("%s has eaten!", owner.name);
}
public Spoon(Diner owner) {
this.owner = owner;
}
public Diner getOwner() {
return owner;
}
public void setOwner(Diner owner) {
this.owner = owner;
}
}
static class Diner {
private String name;
private boolean isHungry;
public Diner(String name) {
this.name = name;
isHungry = true;
}
public void eatWith(Spoon spoon, Diner spouse) {
while (isHungry) { //只有饿的情况下才能进来
if (spoon.owner != this) { //没有勺子判断
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
continue;
}
//问题在此处:一直再谦让
if (spouse.isHungry) {
System.out.println(name + ": 亲爱的" + spouse.name + "你先吃吧");
spoon.setOwner(spouse);
continue;
}
spoon.use();
isHungry = false;
System.out.println(name + ": 我吃好了");
spoon.setOwner(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.eatWith(spoon, wife);
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
wife.eatWith(spoon, husband);
}
}).start();
}
}
原因:重试机制不变,消息队列始终重试,吃饭始终谦让.
解决:以太网的指数退避算法
加入随机因素
public class LiveLockFix {
static class Spoon {
private Diner owner; //就餐者
public synchronized void use() {
System.out.printf("%s has eaten!", owner.name);
}
public Spoon(Diner owner) {
this.owner = owner;
}
public Diner getOwner() {
return owner;
}
public void setOwner(Diner owner) {
this.owner = owner;
}
}
static class Diner {
private String name;
private boolean isHungry;
public Diner(String name) {
this.name = name;
isHungry = true;
}
public void eatWith(Spoon spoon, Diner spouse) {
while (isHungry) { //只有饿的情况下才能进来
if (spoon.owner != 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.setOwner(spouse);
continue;
}
spoon.use();
isHungry = false;
System.out.println(name + ": 我吃好了");
spoon.setOwner(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.eatWith(spoon, wife);
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
wife.eatWith(spoon, husband);
}
}).start();
}
}
3.2.1、工程中的活锁实例:消息队列
消息如果处理失败,就放在队列开头重试;
由于依赖服务出了问题,处理该消息一直失败;
没阻塞,但程序无法继续。
3.2.2、如何解决活锁问题
3.2、饥饿
- 当线程需要某些资源(例如CPU),但是却始终得不到;
- 线程的优先级设置的过于低,或者有某些线程持有锁同时又无限循环从而不释放锁,或者某程序始终占用某文件的写锁;
- 饥饿可能会导致响应性差:比如,我们的浏览器有一个线程负责处理前台响应(打开收藏夹等动作),另外的后台线程负责下载图片和文件,计算渲染等。在这种情况下,如果后台线程把CPU资源都占用了,那么前台线程将无法得到很好的执行,这会导致用户的体验很差。
解决:为了避免饥饿的发生一方面是逻辑上不应该有对于锁的使用完不释放的逻辑错误;不应该在程序中设置优先级。
4、常见问题
- 写一个必然死锁的例子?
MustDeadLock.java - 生产中什么场景下会发生死锁?
在一个方法中获取多个锁 - 发生死锁必须满足哪些条件?
(死锁发生的4个必要条件) - 如何用工具定位死锁?
(如何定位死锁) - 有哪些解决死锁问题的策略?
(修复死锁的策略) - 避免策略和检测与恢复策略的主要思路是什么?
(常见修复策略) - 讲一讲经典的哲学家就餐问题?怎么解决?
(哲学家就餐的换手方案),(讲一讲经典的哲学家就餐问题) - 实际开发中如何避免死锁?
(实际工程中如何有效避免死锁) - 什么是活跃性问题?活锁、饥饿和死锁有什么区别?
死锁与活锁的区别,死锁与饥饿的区别?
下一篇:第十三章 线程池