知识要分享,好的东西更要分享出来!
昨天了解了多线程死锁相关知识,以前总听别人讲: 面试的时候可能会问你
1、死锁产生的条件是什么?
资源的竞争
2、能集体说下那么多线程中,产生死锁的几个必要条件是什么?
需要同时满足四个条件
a、互斥,共享资源小x和小y只能被一个线程占用
b、持有等待,线程X持有资源小x,去等待资源小y的时候不释放小x
c、不可强占,线程X持有资源小x,那么其他线程不可强占小x
d、循环等待, 线程X等待线程Y占有的资源,线程Y等待线程X占有的资源
3、那么死锁问题能被解决么?如何解决?能简单说说么?
可以被解决
其中:
a:没有办法破坏,因为在单线程中是不存在死锁的问题的,那么死锁
必然产生多线程当中,多线程那么必定存在共享资源的互斥,因此a无法被破
坏
b:这个竞争条件是可以破坏的,我们可以使竞争的资源被一个线程所持有
即可
c:这个竞争条件也是可以破坏的,可以采用ReenTrantLock
d:这个条件也是可以破坏的,我们可以采用顺序加锁的方式去弄
下面就说说具体的案例吧,我们就以转账为例:
账户类:
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @author xxx xshlxx@126.com
* @since 2020/12/21
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Account {
private String accountName;
private int price;
/**
* 当前账户转账之后剩余金额
*
* @param amount 转账金额
*/
public void remainPrice(int amount) {
this.price = this.price - amount;
}
/**
* 转账之后的金额
*
* @param amount 转账金额
*/
public void transferPrice(int amount) {
this.price = this.price + amount;
}
}
模拟死锁:
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @author xxx xshlxx@126.com
* @since 2020/12/21
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class TransferAccount implements Runnable {
private Account fromAccount;
private Account toAccount;
private int amount;
@Override
public void run() {
while (true) {
synchronized (fromAccount) {
synchronized (toAccount) {
fromAccount.remainPrice(amount);
toAccount.transferPrice(amount);
}
}
System.out.println(fromAccount.getAccountName() + "--->" + fromAccount.getPrice());
System.out.println(toAccount.getAccountName() + "--->" + toAccount.getPrice());
}
}
public static void main(String[] args) {
Account fromAccount = new Account("小李",100000);
Account toAccount = new Account("小白",300000);
TransferAccount from = new TransferAccount(fromAccount,toAccount,10);
TransferAccount to = new TransferAccount(toAccount,fromAccount,30);
Thread t1 = new Thread(from);
Thread t2 = new Thread(to);
t1.start();
t2.start();
}
}
运行结果如下:
说一下为什么会产生死锁?
原因在于如下:
a、存在多个线程(t1、t2)
b、从案例中我们可以看出,两个线程的操作为:
b1: 小李向小白转了10块钱,小白感觉很开心,觉得要
有所回馈就向小李转了30!
b2: 所操作的对象: 账户小李和账户小白,这也是我们线程要完成转账
操作所需要的共享资源,既然都说开了,那就再说一点,我们以此
案例进行剖析,剖析上面提及到死锁发生的必须同时满足的四个条
件
1、资源互斥: 在这里阐述的比较清楚了,假设这里面资源不互斥,那么
就是资源为单线程,那么就不存在死锁,换到这个案例中就是,单向
转账,或者说一个线程执行完之后另外一个线程才能够执行
2、持有等待,线程X持有资源小x时,去等待资源小y而不是释放小x的资源
换到我们这个案例对应的就是: fromAccount和toAccount两个账户被
不同的线程线程所持有造成的死锁,在这里要提及一句: 认真看下两个
线程的上锁,是存在顺序的
3、不可强占,线程X持有资源小x时,其他线程不可获取小x的资源,这个也
比较容易,我们反向去思考,如果说,已经持有资源的线程资源可以被其
它线程强占,那么就不会存在死锁问题了,因为不存在等待!
4、循环等待,循环等待, 线程X等待线程Y占有的资源,线程Y等待线程X占有
的资源,在我们这里就是因为一个完成操作需要fromAccount和
toAccount两个账户,有其中一个就会陷入无限等待的可能
上面说了这么多,那么就说下如何解决死锁的问题吧
···
1: 破坏持有等待不释放的问题,我们完全可以写一个"调控器",让它起到一
个中介的作用,共享资源由它去统一调控
资源调配类:
import java.util.ArrayList;
import java.util.List;
/**
* @author xxx xshlxx@126.com
* @since 2020/12/21
*/
public class Allocator {
private List<Account> accounts = new ArrayList<>();
/**
* 资源统一调配中心
*
* @param fromAccount 转账账户
* @param toAccount 转入账户
*/
public boolean accountObtainIs(Account fromAccount,Account toAccount) {
if (accounts.contains(fromAccount) || accounts.contains(toAccount)) {
return false;
}
accounts.add(fromAccount);
accounts.add(toAccount);
return true;
}
/**
* 资源释放
*
* @param fromAccount 转账账户
* @param toAccount 转入账户
*/
public void free(Account fromAccount,Account toAccount) {
accounts.remove(fromAccount);
accounts.remove(toAccount);
}
}
破坏死锁产生的条件: 2
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @author xxx xshlxx@126.com
* @since 2020/12/21
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class TransferAccountSave2 implements Runnable {
private Account fromAccount;
private Account toAccount;
private int amount;
Allocator allocator;
@Override
public void run() {
try {
if (allocator.accountObtainIs(fromAccount, toAccount)) {
while (true) {
synchronized (fromAccount) {
synchronized (toAccount) {
if (fromAccount.getPrice() > amount) {
fromAccount.remainPrice(amount);
toAccount.transferPrice(amount);
}
}
}
System.out.println(fromAccount.getAccountName() + "--->" + fromAccount.getPrice());
System.out.println(toAccount.getAccountName() + "--->" + toAccount.getPrice());
}
}
} catch (Exception ex) {
ex.printStackTrace();
} finally {
allocator.free(fromAccount, toAccount);
}
}
public static void main(String[] args) {
Account fromAccount = new Account("小李", 100000);
Account toAccount = new Account("小白", 300000);
Allocator allocator = new Allocator();
TransferAccountSave2 from = new TransferAccountSave2(fromAccount, toAccount, 10, allocator);
TransferAccountSave2 to = new TransferAccountSave2(toAccount, fromAccount, 30, allocator);
Thread t1 = new Thread(from);
Thread t2 = new Thread(to);
t1.start();
t2.start();
}
}
运行结果如下:
说明: 其实仔细理解不难发现,这个东西吧,有点意思,可以从两方面去衡量:
1、是不是相当于把锁的范围扩大了?以前锁相当于锁当前的账户,小李或者
小白,现在相当于同时上锁,一起获取才能够执行,蛮有趣的。
2、对比下rua表达式,redis里面那个(这个我也不大熟),原理是保证原子性
操作,是不是和这个差不多?
···
2: 破坏不可强占的问题,死锁在这里的原因是因为sychronized把共享资
源给上了锁,导致其他线程无法获取上锁的资源,下面我们把这个条件
给破坏掉,采取疑问式写法
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author xxx xshlxx@126.com
* @since 2020/12/21
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class TransferAccountSave3 implements Runnable {
private Account fromAccount;
private Account toAccount;
private int amount;
private Lock fromLock = new ReentrantLock();
private Lock toLock = new ReentrantLock();
@Override
public void run() {
while (true) {
/**
* 这里面我们采用 ReentrantLock.tryLock()
* 意味: 获取到锁返回true, 获取不到返回false
*/
if (fromLock.tryLock()) {
if (toLock.tryLock()) {
if (fromAccount.getPrice() >= amount) {
fromAccount.remainPrice(amount);
toAccount.transferPrice(amount);
}
}
}
System.out.println(fromAccount.getAccountName() + "--->" + fromAccount.getPrice());
System.out.println(toAccount.getAccountName() + "--->" + toAccount.getPrice());
}
}
public TransferAccountSave3(Account fromAccount, Account toAccount, int amount) {
this.fromAccount = fromAccount;
this.toAccount = toAccount;
this.amount = amount;
}
public static void main(String[] args) {
Account fromAccount = new Account("小李", 100000);
Account toAccount = new Account("小白", 300000);
TransferAccountSave3 from = new TransferAccountSave3(fromAccount, toAccount, 10);
TransferAccountSave3 to = new TransferAccountSave3(toAccount, fromAccount, 30);
Thread t1 = new Thread(from);
Thread t2 = new Thread(to);
t1.start();
t2.start();
}
}
运行结果如下:
···
说明: 不采用sychronized这个关键字去加锁,而是采用ReentrantL
ock.tryLock()去尝试获取到,如果获取到了那么返回true,如果
为获取到返回false,看下这句话:
Acquires the lock only if it is not held by anoth
er thread at the time of invocation.
很清晰了,判断的是,在同一时间,当前这把锁是否被其他线程所
持有,ok继续下一个
···
3: 破坏循环等待问题, 我们可以想办法保证加锁的顺序,那样就可以解决掉
循环等待的问题,如下代码:
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @author xxx xshlxx@126.com
* @since 2020/12/21
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class TransferAccountSave4 implements Runnable {
private Account fromAccount;
private Account toAccount;
private int amount;
@Override
public void run() {
Account left = null;
Account right = null;
if (fromAccount.hashCode() > toAccount.hashCode()) {
left = toAccount;
right = fromAccount;
}
while (true) {
synchronized (left) {
synchronized (right) {
if (fromAccount.getPrice() > amount) {
fromAccount.remainPrice(amount);
toAccount.transferPrice(amount);
}
}
}
System.out.println(fromAccount.getAccountName() + "--->" + fromAccount.getPrice());
System.out.println(toAccount.getAccountName() + "--->" + toAccount.getPrice());
}
}
public static void main(String[] args) {
Account fromAccount = new Account("小李", 100000);
Account toAccount = new Account("小白", 300000);
TransferAccountSave4 from = new TransferAccountSave4(fromAccount, toAccount, 10);
TransferAccountSave4 to = new TransferAccountSave4(toAccount, fromAccount, 30);
Thread t1 = new Thread(from);
Thread t2 = new Thread(to);
t1.start();
t2.start();
}
}
···
运行结果如下:
···
分析: 解决了多线程中循环等待的问题,采取的方法是严格控制加锁的顺序
,至于为什么?留作评论区
gitlab代码地址如下: https://github.com/584918038/design-model.git