死锁产生的原因和解锁的方法
-
1、产生死锁的四个必要条件:
- 互斥条件:一个资源每次只能被一个进程使用。
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
举例说明
/**
* 一个简单的死锁类
* 当DeadLock类的对象flag==1时(td1),先锁定o1,睡眠500毫秒
* 而td1在睡眠的时候另一个flag==0的对象(td2)线程启动,先锁定o2,睡眠500毫秒
* td1睡眠结束后需要锁定o2才能继续执行,而此时o2已被td2锁定;
* td2睡眠结束后需要锁定o1才能继续执行,而此时o1已被td1锁定;
* td1、td2相互等待,都需要得到对方锁定的资源才能继续执行,从而死锁。
*/
@Slf4j
public class DeadLock implements Runnable {
public int flag = 1;
//静态对象是类的所有对象共享的
private static Object o1 = new Object(), o2 = new Object();
@Override
public void run() {
log.info("flag:{}", flag);
if (flag == 1) {
synchronized (o1) {
try {
Thread.sleep(500);
} catch (Exception e) {
e.printStackTrace();
}
synchronized (o2) {
log.info("1");
}
}
}
if (flag == 0) {
synchronized (o2) {
try {
Thread.sleep(500);
} catch (Exception e) {
e.printStackTrace();
}
synchronized (o1) {
log.info("0");
}
}
}
}
public static void main(String[] args) {
DeadLock td1 = new DeadLock();
DeadLock td2 = new DeadLock();
td1.flag = 1;
td2.flag = 0;
//td1,td2都处于可执行状态,但JVM线程调度先执行哪个线程是不确定的。
//td2的run()可能在td1的run()之前运行
new Thread(td1).start();
new Thread(td2).start();
}
}
-
2、如何避免死锁
-
1、死锁检测和恢复(deadlock detection and recovery):
死锁检测(deadlock detection)即探查和识别死锁的方法。这种策略并不采取任何动作来使死锁不出现,而是系统事件触发执行一个检测算法。,也即在系统运行过程中,及时地探查和识别死锁的存在,并识别出处于死锁之中的进程和资源等。死锁恢复(deadlock recovery)是指当检测并识别出系统中出现处于死锁之中的一组进程时,如何使系统回复到正常状态并继续执行下去。死锁恢复常采用下述两种方法。
-
1、 撤消进程即当发现死锁时,就撤消(夭折)一些处于死锁状态的进程,并收回它的占用的资源,以解除死锁,使其它进程能继续运行,或者在提供检查点(checkpoint)信息情况下回退(rolled back)到一个较早的状态。这里有一个开销问题,即撤消哪个(些)进程比较“划算”。
-
2、 挂起进程即当发现死锁时,挂起一些进程,抢占它们占用的资源,使得处于死锁之中的其它进程继续执行。待以后条件满足后,再恢复被挂起的进程。
-
常利用资源分配图、进程等待图来协助这种检测。
- 2、死锁预防(deadlock prevention):
- 1、死锁预防(deadlock prevention)是在系统运行之前,事先考虑防止死锁发生的对策,即在最初设计各种资源调度算法时,就没法防止在系统运行过程中可能产生的死锁。Coffmman 等人[1971]曾提出进程在利用可重用性资源时产生死锁的四个必要条件:
- 1、 **互斥使用(mutual exclusion)**:系统中存在一次只能给一个进程使用的资源。
- 2、**占用并等待(resource holding and waiting)**:系统中存在这样的进程,它(们)已占有部分资源并等待得到另外的资源,而这些资源又被其它进程所占用还未释放。
- 3、**非抢占分配(nonpreemption)**:资源在占有它的进程自愿交出之前,不可被其它进程所强行占用。
- 4、**部分地分配(partial allocation)或循环等待(circular waiting)**:存在一个两个或都个进程的循环链,链中每一个进程等待被链中下一个进程占有的资源。即在一定条件下,若干进程进入了相互无休止地等待所需资源的状态。
所有这四个必要条件都应该在死锁时出现。
- 1、每个进程必须一次性地请求它所需要的所有资源。若系统无法满足这一要求,则它不能执行。这是一种预分资源方法,它破坏了产生死锁的第三个必要条件。
- 2、一个已占有资源的进程若要再申请新资源,它必须先释放已占资源。若随后它还需要它们,则需要重新提出申请。换言之,一个进程在使用某资源过程中可以放弃该资源,从而破坏了产生死锁的第二个必要条件。
- 3、将系统中所有资源顺序编号,规定进程只能依次申请资源,这就是说,一个进程只有在前面的申请满足后,才能提出对其后面序号的资源的请求。这是一种有序使用资源法,它破坏了产生死锁的第四个必要条件。
- 3、死锁避免(deadlock avoidence):
死锁避免(deadlock avoidence)是在系统运行过程中注意避免死锁的发生。这就要求每当申请一个资源时,系统都应根据一定的算法判断是否认可这次申请,使得在今后一段时间内系统不会出现死锁。
2、对象锁和类锁是否会互相影响?
-
1、对象锁:Java的所有对象都含有1个互斥锁,这个锁由JVM自动获取和释放。线程进入synchronized方法的时候获取该对象的锁,当然如果已经有线程获取了这个对象的锁,那么当前线程会等待;synchronized方法正常返回或者抛异常而终止,JVM会自动释放对象锁。这里也体现了用synchronized来加锁的1个好处,方法抛异常的时候,锁仍然可以由JVM来自动释放。
-
2、类锁:对象锁是用来控制实例方法之间的同步,类锁是用来控制静态方法(或静态变量互斥体)之间的同步。其实类锁只是一个概念上的东西,并不是真实存在的,它只是用来帮助我们理解锁定实例方法和静态方法的区别的。我们都知道,java类可能会有很多个对象,但是只有1个Class对象,也就是说类的不同实例之间共享该类的Class对象。Class对象其实也仅仅是1个java对象,只不过有点特殊而已。由于每个java对象都有1个互斥锁,而类的静态方法是需要Class对象。所以所谓的类锁,不过是Class对象的锁而已。获取类的Class对象有好几种,最简单的就是MyClass.class的方式。
-
3、类锁和对象锁不是同1个东西,一个是类的Class对象的锁,一个是类的实例的锁。也就是说:1个线程访问静态synchronized的时候,允许另一个线程访问对象的实例synchronized方法。反过来也是成立的,因为他们需要的锁是不同的。
3、线程同步机制
线程同步是为了确保线程安全,所谓线程安全指的是多个线程对同一资源进行访问时,有可能产生数据不一致问题,导致线程访问的资源并不是安全的。如果多线程程序运行结果和单线程运行的结果是一样的,且相关变量的值与预期值一样,则是线程安全的。
Java中与线程同步有关的关键字/类包括:
volatile、synchronized、Lock、AtomicInteger等concurrent包下的原子类。。。等
- 1、volatile
volatile一般用在多个线程访问同一个变量时,对该变量进行唯一性约束,volatile保证了变量的可见性,不能保证原子性。
用法(例):
private volatile booleanflag = false
-
保证变量的可见性:volatile本质是告诉JVM当前变量在线程寄存器(工作内存)中的值是不确定的,需要从主存中读取,每个线程对该变量的修改是可见的,当有线程修改该变量时,会立即同步到主存中,其他线程读取的是修改后的最新值。
-
不能保证原子性:原子性指的是不会被线程调度机制打断的操作,在java中,对基本数据类型的变量的读取和赋值操作是原子性操作。自增/自减操作不是原子性操作。例如:i++,其实是分成三步来操作的:1)从主存中读取i的值;2)执行+1操作;3)回写i的值。volatile关键字并不能保证原子性操作。非原子操作都会产生线程安全的问题,那么如何实现自增/自减的原子性呢?后续将有讲解。
-
2、synchronized
-
synchronized提供了一种独占的加锁方式,是比较常用的线程同步的关键字,一般在“线程安全的单例”中普遍使用。该关键字能够保证代码块的同步性和方法层面的同步。
-
代码块同步
-
方法同步
//使用synchronized关键字实现线程安全的单例模式 代码块同步 private static Singleton instance; public static Singleton getInstance(){ if(instance == null){ synchronized (Singleton.class) { if(instance == null){ instance = new Singleton(); } } } return instance; } privateSingleton(){ } //方法同步 public static synchronized Singleton getInstance2(){ if(instance == null){ instance = new Singleton(); } return instance; } private Singleton(){ }
-
-
volatile和synchronized的区别
-
volatile通过变量的可见性,指定线程必须从主存中读取变量的最新值;synchronized通过阻塞线程的方式,只有当前线程能访问该变量,锁定了当前变量。
-
volatile使用在变量级别;synchronized可以使用在变量、方法、类级别
-
volatile不会造成线程阻塞;synchronized可能会造成线程阻塞
-
volatile不能保证原子性;synchronized能保证原子性
-
volatile标记的变量不会被编译器优化;synchronized标记的变量有可能会被编译器优化(指令重排)。
-
-
3、如何保证自增/自减的原子性
-
使用java.util.concurrent包下提供的原子类,如AtomicInteger、AtomicLong、AtomicReference等。用法:
AtomicInteger atomicInteger = new AtomicInteger();
atomicInteger.getAndIncrement();//实现原子自增
atomicInteger.getAndDecrement();//实现原子自减
- 2、使用synchronized同步代码块
Synchronized(this){
value++;
}
- 3、使用Lock显示锁同步代码块
private Lock lock = new ReentrantLock();
private final int incrementAndGet(){
lock.lock();
try {
return value++;
} finally {
// TODO: handle finally clause
lock.unlock();
}
}
4、如何保证多线程读写文件的安全
-
沉睡唤醒机制:
- 需求:两个线程写一个TXT文件,线程1:I love you 线程2:so do I 。保证线程1、2有序执行,一句I love you,对应一句so do I。
- 第一步,先创建WRFile类。这一步是关键的。
public class WRFile {
//String str;
boolean flag;
public WRFile(){
}
public synchronized void read1(){
if(this.flag){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
RandomAccessFile ra = null;
try {
ra = new RandomAccessFile("love.txt", "rw");
ra.seek(ra.length());
ra.writeBytes("I love you");
ra.writeBytes("\r\n");
} catch (Exception e) {
e.printStackTrace();
}
finally {
try {
ra.close();
} catch (IOException e) {
e.printStackTrace();
}
}
//修改标记 唤醒线程
this.flag = true;
this.notify();
}
public synchronized void read2(){
if(!this.flag){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
RandomAccessFile ra = null;
try {
ra = new RandomAccessFile("love.txt", "rw");
ra.seek(ra.length());
ra.writeBytes("so do I");
ra.writeBytes("\r\n");
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
ra.close();
} catch (IOException e) {
e.printStackTrace();
}
}
//修改标记 唤醒线程
this.flag = false;
this.notify();
}
}
- 第二步,创建我们的两个线程类,第一个FirstThread。
public class FirstThread implements Runnable {
private WRFile wr = new WRFile();
public FirstThread(WRFile wr) {
this.wr = wr;
}
@Override
public void run() {
while(true){
wr.read1();
}
}
}
public class SecondThrad implements Runnable{
private WRFile wr = new WRFile();
public SecondThrad(WRFile wr) {
this.wr = wr;
}
@Override
public void run() {
while(true)
{
wr.read2();
}
}
}
- 第三步:
public static void main(String[] args) {
//创建数据对象
WRFile wr = new WRFile();
//设置和获取类
FirstThread ft = new FirstThread(wr);
SecondThrad st = new SecondThrad(wr);
//线程类
Thread th1 = new Thread(ft);
Thread th2 = new Thread(st);
//启动线程
th1.start();
th2.start();
}
145天以来,Java架构更新了 428个主题,已经有91位同学加入。微信扫码关注java架构,获取Java面试题和架构师相关题目和视频。上述相关面试题答案,尽在Java架构中。
网友评论