多线程时容易出现线程安全问题,其中一种解决方法是使用锁,但是加锁很可能带来另外一个线程安全问题——死锁。
死锁的概念(来自百度百科)
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
死锁产生的条件(来自百度百科)
1)互斥条件:指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放。
2)请求和保持条件:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。
3)不剥夺条件:指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。
4)环路等待条件:指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合{P0,P1,P2,···,Pn}中的P0正在等待一个P1占用的资源;P1正在等待P2占用的资源,……,Pn正在等待已被P0占用的资源。
死锁解决方法
解决死锁最好的方式就是不让死锁产生,也就是说只要不让死锁产生的四个条件同时满足就行了。
1)、确定获取资源(锁)的顺序;
2)、采用尝试获取锁的机制。
简单死锁
例:假设需要同时获得笔和纸才能画画,且现在只有一支笔和一张纸,小明和小芳想画画,我们这里设置了两把锁,所以对应获得笔,锁2对应获得纸,小明和小芳去获取锁,小明获得了锁1(笔),小芳获得了锁2(纸),如果他们都不愿意放弃获得的锁(笔/纸),意味着对方永远无法获得缺少的锁(纸/笔),都不能画画。这就是死锁。
代码:
package com.su.mybatis.oracle.controller;
public class Test {
//锁1
private static Object obj1 = new Object();
//锁2
private static Object obj2 = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (obj1) {
getPen();
synchronized (obj2) {
getPaper();
System.out.println(Thread.currentThread().getName() + ":画画");
}
}
}
});
t1.setName("小明");
t1.start();
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (obj2) {
getPaper();
synchronized (obj1) {
getPen();
System.out.println(Thread.currentThread().getName() + ":画画");
}
}
}
});
t2.setName("小芳");
t2.start();
}
public static void getPen() {
try {
Thread.sleep(1500);
System.out.println(Thread.currentThread().getName() + ":获得笔");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void getPaper() {
try {
Thread.sleep(1500);
System.out.println(Thread.currentThread().getName() + ":获得纸");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
程序会一直运行,运行结果:
小明:获得笔
小芳:获得纸
对于这种死锁情况怎么样解决呢?很简单。只需要将小明小芳拿两个锁的顺序改成一致即可。
package com.su.mybatis.oracle.controller;
public class Test {
//锁1
private static Object obj1 = new Object();
//锁2
private static Object obj2 = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (obj1) {
getPen();
synchronized (obj2) {
getPaper();
System.out.println(Thread.currentThread().getName() + ":画画");
}
}
}
});
t1.setName("小明");
t1.start();
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (obj1) {
getPen();
synchronized (obj2) {
getPaper();
System.out.println(Thread.currentThread().getName() + ":画画");
}
}
}
});
t2.setName("小芳");
t2.start();
}
public static void getPen() {
try {
Thread.sleep(1500);
System.out.println(Thread.currentThread().getName() + ":获得笔");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void getPaper() {
try {
Thread.sleep(1500);
System.out.println(Thread.currentThread().getName() + ":获得纸");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
输出结果:
小明:获得笔
小明:获得纸
小明:画画
小芳:获得笔
小芳:获得纸
小芳:画画
当然,这里只需要考虑小明和小芳获得锁的顺序,至于获得锁后是获得笔还是获得纸都是不影响的。
动态的死锁
一个经典的问题--转账。
package com.su.mybatis.oracle.controller;
public class Test {
public static void main(String[] args) {
User xiaoming = new User("小明", "A1", 20000);//小明,账号A1,20000元
User xiaofang = new User("小芳", "A2", 15000);//小芳,账号A2,15000元
Thread t1 = new ThreadTest(xiaoming,xiaofang,3000);//小明转账3000给小芳
t1.setName("小明");
t1.start();
Thread t2 = new ThreadTest(xiaofang,xiaoming,2000);//小芳转账2000给小明
t2.setName("小芳");
t2.start();
}
}
class ThreadTest extends Thread{
private User sourceUser;
private User destUser;
private int money;
public ThreadTest(User sourceUser, User destUser, int money) {
this.sourceUser = sourceUser;
this.destUser = destUser;
this.money = money;
}
@Override
public void run() {
String threadName = Thread.currentThread().getName();
synchronized (sourceUser) {//来源(转出钱的账号)
try {
System.out.println(threadName + "获得锁1");
Thread.sleep(1500);
sourceUser.setMoney(sourceUser.getMoney() - money);
synchronized (destUser) {//目的地(转入钱的账号)
System.out.println(threadName + "获得锁2");
destUser.setMoney(destUser.getMoney() + money);
System.out.println(threadName + "转账完成");
System.out.println("source:" + sourceUser.toString());
System.out.println("dest:" + destUser.toString());
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class User{
public User(String name, String amount, int money) {
this.name = name;
this.amount = amount;
this.money = money;
}
private String name;
private String amount;
private int money;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAmount() {
return amount;
}
public void setAmount(String amount) {
this.amount = amount;
}
public int getMoney() {
return money;
}
public void setMoney(int money) {
this.money = money;
}
@Override
public String toString() {
return "name:" + name + ",amount:" + amount + ",money:" + money;
}
}
运行结果:
小明获得锁1
小芳获得锁1
解决方法:使用显示锁中尝试获取锁的机制(方法不止一种)。只有同时获取到两把锁的时候,才执行转账业务。如果获得第一把锁后,没有获取到第二把锁,则释放第一把锁,然后重新尝试。因为存在反复尝试的过程,所以会使用到while 循环,使用break跳出循环,跳出循环的条件是转账完成。
package com.su.mybatis.oracle.controller;
import java.util.Random;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Test {
public static void main(String[] args) {
User xiaoming = new User("小明", "A1", 20000);// 小明,账号A1,20000元
User xiaofang = new User("小芳", "A2", 15000);// 小芳,账号A2,15000元
Thread t1 = new ThreadTest(xiaoming, xiaofang, 3000);// 小明转账3000给小芳
t1.setName("小明");
t1.start();
Thread t2 = new ThreadTest(xiaofang, xiaoming, 2000);// 小芳转账2000给小明
t2.setName("小芳");
t2.start();
}
}
class ThreadTest extends Thread {
private User sourceUser;
private User destUser;
private int money;
public ThreadTest(User sourceUser, User destUser, int money) {
this.sourceUser = sourceUser;
this.destUser = destUser;
this.money = money;
}
@Override
public void run() {
String threadName = Thread.currentThread().getName();
while (true) {
if (sourceUser.getLock().tryLock()) {// 来源(转出钱的账号)
System.out.println(threadName + "获得锁1");
try {
if (destUser.getLock().tryLock()) {// 目的地(转入钱的账号)
try {
sourceUser.setMoney(sourceUser.getMoney() - money);
destUser.setMoney(destUser.getMoney() + money);
System.out.println(threadName + "转账完成");
System.out.println("source:" + sourceUser.toString());
System.out.println("dest:" + destUser.toString());
break;
} finally {
destUser.getLock().unlock();
}
}
} finally {
sourceUser.getLock().unlock();
}
}
try {
Thread.sleep( new Random().nextInt(5));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class User {
public User(String name, String amount, int money) {
this.name = name;
this.amount = amount;
this.money = money;
}
private String name;
private String amount;
private int money;
private final Lock lock = new ReentrantLock();
public Lock getLock() {
return lock;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAmount() {
return amount;
}
public void setAmount(String amount) {
this.amount = amount;
}
public int getMoney() {
return money;
}
public void setMoney(int money) {
this.money = money;
}
@Override
public String toString() {
return "name:" + name + ",amount:" + amount + ",money:" + money;
}
}
输出结果:
小明获得锁1
小芳获得锁1
小芳转账完成
source:name:小芳,amount:A2,money:13000
dest:name:小明,amount:A1,money:22000
小明获得锁1
小明转账完成
source:name:小明,amount:A1,money:19000
dest:name:小芳,amount:A2,money:16000
Thread.sleep( new Random().nextInt(5))的作用:
让线程进行短暂休眠,使它们错开拿锁时间,减少尝试拿锁过程中的碰撞,杜绝活锁的产生,避免资源(eg:CPU等)的浪费。
活锁:并发编程中,线程没有被阻塞,因为某些必要条件不满足,一直处于尝试获取资源(eg:锁)→获取资源失败→尝试获取资源...的过程。活锁状态的线程会不断的改变状态,需要消耗一定的资源,活锁有可能自行解开(将Thread.sleep( new Random().nextInt(5))注掉运行即可复现)。
如果有写的不对的地方,请大家多多批评指正,非常感谢!