讲到死锁,首先我们需要知道什么是锁?只有清楚了锁的概念,我们才会明白死锁的真正含义。下面我会按照我的理解,分成几部分来阐述:
- 我们为什么需要锁以及锁的概念
- 为什么会出现死锁
- 怎么解决死锁
- 产生死锁的代码案例
- synchronized 实现死锁以及解决办法
- Lock 实现死锁以及解决办法
1、 我们为什么需要锁以及锁的概念
在单线程的时候,我们往往不需要考虑到锁的概念,因为单线程的时候,系统和变量资源是被一个线程使用,按照代码顺序执行。但是当我们考虑到多线程的时候,就需要考虑到资源的安全性了。比如,多个线程共同操作一个变量资源(我们称之为共享变量),但是根据java的内存模型,每个线程会把共享变量拷贝到自己的工作内存(称为副本),当每个线程操作这个共享变量的时候,都会在自己的工作内存中进行操作,而后在主内存中进行刷新变量的值,但是别的线程往往并不能及时看到这个刷新的值,而是对旧值进行操作,所以会出现我们不希望的结果。所以当多个线程会对同一个资源进行操作时,我们希望当一个线程对这个资源进行操作时,其它线程不能进行操作(拿不到这个资源),此时就只有一个线程对这个资源进行操作,然后及时更新到主内存。这个过程我们可以称之为锁,我们对这个资源进行了上锁。当然我们可以对变量,对象,以及整个方法过程等进行上锁。上锁的目的是保证资源的安全性。
2、为什么会出现死锁
简单的来说,当某个线程想要操作某个资源的时候,发现这个资源已经被上锁了(被其他的线程使用并且上锁),但是自己又需要完成对这个资源的操作,才能进行对自己锁的释放,或者程序的完成。但是另外一个使用这个资源的线程,出现了不能对这个资源进行锁的释放,比如陷入了死循环或者需要调用另外一个线程已经上锁的资源,这个时候,某个线程或者很多线程始终结束不了代码的运行,这种情况称之为死锁。
其实换句话说,也就是某个线程对某个资源进行了独占,导致其它的线程一直访问不到这个资源,使得代码陷入了死循环。
3、怎么解决死锁
- 避免某个线程对某个资源进行独占,及时释放对该资源的锁。
- 设置计时,某个线程长时间不能结束,就要及时的释放对某个资源的锁。
- 避免把代码写成闭环的状态,避免死锁。
4、产生死锁的代码案例
synchronized 实现死锁的思路:
- 分别定义两个资源类ResA和ResB
- 定义一个使用资源的类UseRes,并继承Thread类
- 在UseRes类中定义ResA和ResB的对象,并用statis修饰
- 定义成员变量id,记录该线程的id号
- 定义UseRes的有参构造方法,参数为int id,并传给成员变量id
- 重写UseRes类中的run方法
- run方法的实现
当是id=0的线程对象时,首先synchronized(ResA){
// 在这个锁中,我们对ResB继续上锁
synchronized(ResB){
// to do
}
}
当是id=1的线程对象时,首先synchronized(ResB){
// 在这个锁中,我们对ResB继续上锁
synchronized(ResA){
// to do
}
}
public class UseRes extends Thread{
static ResA A = new ResA();
static ResB B = new ResB();
// int resA=0;
// int resB=0;
// 线程的编号
int id;
// 构造方法,传入线程的编号
public UseRes(int i) {
this.id=i;
}
// 内部类A
public static class ResA{
String resA="A";
ResA(){};
}
// 内部类B
public static class ResB{
String resA="B";
ResB(){};
}
@Override
public void run() {
if(id==0){
// 给某个对象加锁
synchronized (A){
System.out.println("线程id0正在使用A资源");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (B){
System.out.println("线程id0正在使用B资源");
}
}
}else {
synchronized (B){
System.out.println("线程id1正在使用B资源");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (A){
System.out.println("线程id1正在使用A资源");
}
}
}
}
public static void main(String[] args) {
UseRes use01 = new UseRes(0);
UseRes use02 = new UseRes(1);
use01.start();
use02.start();
}
}
改进方法:
把每个嵌套的synchronized()移到外面,避免了锁的嵌套和锁的及时释放。
Lock 实现死锁的思路
因为Lock是个接口,我们使用ReentrantLock这个实现类来创建对象
- 定义一个类并实现Runnable 接口
- 创建两个Lock 的对象,lock1和lock2
- 定义一个flag的布尔成员变量
- 定义构造方法,传入的值为flag
- 重写run方法
- run方法的实现思路
if(flag){
// 线程1获得锁1
lock1.lock()
// 线程1获得锁2
lock1.lock()
}else{
// 线程2获得锁2
lock2.lock()
// 线程2获得锁1
lock2.lock()
}
// 释放锁
lock1.unlock()
lock2.unlock()
public class TestLockDemo implements Runnable{
private boolean flag;
private static ReentrantLock lock1= new ReentrantLock();
private static ReentrantLock lock2= new ReentrantLock();
public TestLockDemo(boolean flag) {
this.flag = flag;
}
@Override
public void run() {
try {
if(flag){
lock1.lock();
System.out.println(Thread.currentThread().getName()+"获得了锁lock1");
lock2.lock();
System.out.println(Thread.currentThread().getName()+"获得了锁lock2");
}else {
lock2.lock();
System.out.println(Thread.currentThread().getName()+"获得了锁lock2");
lock1.lock();
System.out.println(Thread.currentThread().getName()+"获得了锁lock1");
}
}catch (Exception e){
e.printStackTrace();
}finally {
// isHeldByCurrentThread() 查询当前线程是否保持此锁。
if(lock1.isHeldByCurrentThread()){
lock1.unlock();
}
if(lock2.isHeldByCurrentThread()){
lock2.unlock();
}
}
}
public static void main(String[] args) {
TestLockDemo lock1= new TestLockDemo(true);
TestLockDemo lock2= new TestLockDemo(false);
Thread th1 = new Thread(lock1,"线程1");
Thread th2 = new Thread(lock2,"线程2");
th1.start();
th2.start();
}
}
改进方法:在每个线程使用一把锁的时候,把上把锁及时释放,避免死锁情况的发生。