并发专题-多线程死锁问题
回顾
在阅读该篇文章时,我们首先回顾一下《并发编程专题-多线程基础》相关的知识点。我们知道了什么是线程、线程的作用、线程的生命状态模型等。这些都是围绕着线程自身的学习。本章开始,我们学习一下线程与线程之间的相关知识点。
线程安全问题
什么是线程安全问题
当多个线程同时共享,同一个全局变量或静态变量,做写的操作时,可能会发生数据冲突问题,也就是线程安全问题。但是做读操作是不会发生数据冲突问题。
制造线程安全问题
- 创建两个线程
- 两个线程共享count
- 两个线程都对共享的全局变量进行了写操作
- 产生了数据冲突问题:出现了两次100
public class Demo001Runnable implements Runnable{
private int count;
public Demo001Runnable(int count) {
this.count=count;
}
@Override
public void run() {
while (count>1){
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("当前的线程:"+Thread.currentThread().getName()+":"+count--);
}
}
}
public class ThreadSafeDemo001 {
public static void main(String[] args) {
int count =100;
Demo001Runnable demo001Runnable = new Demo001Runnable(count);
Thread thread = new Thread(demo001Runnable,"线程1");
Thread thread1 = new Thread(demo001Runnable,"线程2");
thread.start();
thread1.start();
}
}
如何解决线程安全问题
要想解决一个问题,我们要弄清楚为什么会产生这个问题,问题的表现是什么样,在什么样的知识背景下解决。下面我们就按照这个思路去解决线程安全问题
问题名称:线程安全
问题描述:多个线程共同操作同一共享数据,进行写操作时,会出现数据冲突问题。
问题产生条件:
- 多线程
- 共享同一全局变量或者静态变量
- 对数据进行写操作
知识背景:在Java知识体系下解决该问题-使用关键字 synchronized 或者lock(锁)
解决思路:在只有一个线程的情况下,不会出现线程安全问题,如果我们把可能出现线程安全的地方通过某种方式让其表现的就像一个线程执行一样,就不会出现线程安全问题了。
这里我们使用同步锁去解决线程安全问题。
同步锁
什么是同步锁
同步锁是为了保证每个线程都能正常执行原子不可更改操作,同步监听对象/同步锁/同步监听器/互斥锁的一个标记锁.
每个Java对象都有且只有一个同步锁,在任何时刻,最多只允许一个线程拥有这把锁,当消费者线程试图执行以带有synchronized(this)标记的代码块时,消费者线程必需先获得this关键字引用的Stack对象的锁.
上面是百度给出的说法。我感觉太过抽象了,我们用通俗的方式来理解。
采用比喻的方式去理解:
厕所:共享变量
上厕所的人:线程
厕所的锁:同步锁
当小红进入了厕所时,他就会把厕所给锁起来,其他人只能在外面等待小红。保证每次只有一个人能进入厕所。
同步锁的作用就是:保证出现线程安全问题的代码只能有一个线程去执行,其他线程在同步锁外面等待。
Java中提供了哪些实现同步锁的方式
Java中的每个Java对象都拥有唯一的同步锁,所以任意对象都可以当做同步锁去使用。Java提供了关键字synchronized来标记代码使用了同步锁。
特殊的对象锁
-
this
this表示的是当前调用的对象
-
类.class
表示的是class对象,用于静态方法中
-
同步代码块
示意模型
synchronized (任意对象) { 多条语句操作共享数据的代码 }
改造案例
public Demo002Runnable(int count) { this.count=count; } @Override public void run() { while (count>1){ try { Thread.sleep(300); } catch (InterruptedException e) { e.printStackTrace(); } // 加锁 synchronized (this){ System.out.println("当前的线程:"+Thread.currentThread().getName()+":"+count--); } } }
-
同步方法
示意模型
修饰符 synchronized 返回值类型 方法名(方法参数) { 方法体; }
改造案例
public class Demo003Runnable implements Runnable{ private int count; private Lock lock = new ReentrantLock(); public Demo003Runnable(int count) { this.count=count; } @Override public void run() { while (count>1){ try { Thread.sleep(300); } catch (InterruptedException e) { e.printStackTrace(); } this.run2(); } } /** * 同步方法 */ private synchronized void run2(){ System.out.println("当前的线程:"+Thread.currentThread().getName()+":"+count--); } }
总结:如果是普通方法,则synchronized使用的是this对象(也就是当前对象),如果是静态方法则使用当前类的class对象
-
lock锁
虽然我们可以理解同步代码块和同步方法的锁对象问题,但是我们并没有直接看到在哪里加上了锁,在哪里释放了锁,为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock。Lock是接口不能直接实例化,这里采用它的实现类**ReentrantLock(重入锁)**来实例化。我们就来了解一下lock常用的API
老规矩,了解一个对象的使用,首先从构造方法开始。
ReentrantLock的构造方法
方法名 说明 ReentrantLock() 创建一个ReentrantLock的实例 常用API
方法名 说明 void lock() 获得锁 void unlock() 释放锁 示意模型
Lock lock = new ReentrantLock();// 创建ReentrantLock对象 lock.lock();// 加锁 try { 多条语句操作共享数据的代码 }catch (Exception e){ e.printStackTrace(); }finally { lock.unlock();// 释放锁 }
改造案例
public class Demo004Runnable implements Runnable{ private int count; private Lock lock = new ReentrantLock(); public Demo004Runnable(int count) { this.count=count; } @Override public void run() { while (count>1){ try { Thread.sleep(300); } catch (InterruptedException e) { e.printStackTrace(); } // 加锁 lock.lock(); try { System.out.println("当前的线程:"+Thread.currentThread().getName()+":"+count--); }catch (Exception e){ e.printStackTrace(); }finally { lock.unlock();// 释放锁 } } } }
提示:synchronized关键字的加锁和释放锁都是JVM自动完成的,不需要我们去管理。我们为了能够自己去管理,选择使用lock,但是要注意,lock加锁以后,需要手动释放锁,而且释放锁最好放在finally中,以免出现异常,锁不释放,线程一直阻塞。
使用同步锁解决线程安全问题存在的问题
死锁
什么是死锁
是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去
产生死锁的条件
1、互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用
2、不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
3、请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。
4、循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成了一个等待环路。
死锁案例
public class MyThreadDieSock { public static void main(String[] args) { final Object a = new Object(); final Object b = new Object(); Thread threadA = new Thread(() -> { synchronized (a) { try { System.out.println(Thread.currentThread().getName()+"拿到a锁资源,正在试图获取b锁资源"); Thread.sleep(1000l); synchronized (b) { System.out.println(Thread.currentThread().getName()+"拿到b锁资源"); } } catch (Exception e) { // ignore } } }); Thread threadB = new Thread(() -> { synchronized (b) { try { System.out.println(Thread.currentThread().getName()+"拿到b锁资源,正在试图获取a锁资源"); Thread.sleep(1000l); synchronized (a) { System.out.println(Thread.currentThread().getName()+"拿到a锁资源"); } } catch (Exception e) { // ignore } } }); threadA.start(); threadB.start(); } }
解决死锁问题
上面说到需要满足四个条件才能造成死锁现象,那日常开发中避免和解决死锁问题就需要从这四个条件入手,任意破坏其中一个就可以破坏死锁问题。
-
按顺序加锁
上个例子线程间加锁的顺序各不一致,导致死锁,如果每个线程都按同一个的加锁顺序这样就不会出现死锁。
-
获取锁时限
每个获取锁的时候加上个时限,如果超过某个时间就放弃获取锁之类的。
-
死锁检测
按线程间获取锁的关系检测线程间是否发生死锁,如果发生死锁就执行一定的策略,如终断线程或回滚操作等。
解决死锁问题
上面说到需要满足四个条件才能造成死锁现象,那日常开发中避免和解决死锁问题就需要从这四个条件入手,任意破坏其中一个就可以破坏死锁问题。
-
按顺序加锁
上个例子线程间加锁的顺序各不一致,导致死锁,如果每个线程都按同一个的加锁顺序这样就不会出现死锁。
-
获取锁时限
每个获取锁的时候加上个时限,如果超过某个时间就放弃获取锁之类的。
-
死锁检测
按线程间获取锁的关系检测线程间是否发生死锁,如果发生死锁就执行一定的策略,如终断线程或回滚操作等。
当然最好日常开发中尽量避免嵌套同步。
-