深入解析死锁的产生和避免以及内存不可见问题
在JavaEE的开发中,多线程编程非常常见,但同时也可能引发一些棘手的问题。其中,死锁和内存不可见性是两个常见的问题,下面将对此进行深入分析。
一、死锁的产生
死锁是指两个或多个线程在执行过程中,因为争夺资源而造成的一种相互等待的现象,导致线程无法继续执行。死锁的产生通常需要满足以下四个条件:
- 互斥条件:至少有一个资源处于非共享状态,即某个资源只能被一个线程使用。
- 占有且等待:一个线程已经占有了至少一个资源,但又请求其他资源,并且不释放已占有的资源。
- 不可抢占:已分配给某个线程的资源,在该线程使用完之前,不能被其他线程抢占。
- 循环等待:存在一种线程资源的循环关系,使得每个线程都在等待下一个线程持有的资源。
示例代码
下面是一个简单的死锁示例:
public class DeadlockExample {
private static final Object lockA = new Object();
private static final Object lockB = new Object();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
synchronized (lockA) {
System.out.println("Thread 1: Holding lock A...");
try { Thread.sleep(100); } catch (InterruptedException e) {}
System.out.println("Thread 1: Waiting for lock B...");
synchronized (lockB) {
System.out.println("Thread 1: Acquired lock B!");
}
}
});
Thread thread2 = new Thread(() -> {
synchronized (lockB) {
System.out.println("Thread 2: Holding lock B...");
try { Thread.sleep(100); } catch (InterruptedException e) {}
System.out.println("Thread 2: Waiting for lock A...");
synchronized (lockA) {
System.out.println("Thread 2: Acquired lock A!");
}
}
});
thread1.start();
thread2.start();
}
}
在上面的代码中,thread1
占有了lockA
并试图获取lockB
,而thread2
占有了lockB
并试图获取lockA
,从而导致了死锁。
二、避免死锁
避免死锁的方法主要有以下几种:
-
资源有序分配:规定一个资源的获取顺序,所有线程都按照同样的顺序来申请资源,这样就能避免循环等待的情况。
-
尝试锁(TryLock):使用
tryLock()
方法来尝试获取锁,如果获取失败,则可以选择放弃或者进行其他逻辑处理。 -
超时机制:在尝试获取锁时设定一个超时时间,如果在超时时间内未能获得锁,则释放占用的资源,并稍后重试。
示例代码(避免死锁)
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class AvoidDeadlockExample {
private static final Lock lockA = new ReentrantLock();
private static final Lock lockB = new ReentrantLock();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
try {
if (lockA.tryLock()) {
try {
Thread.sleep(100);
if (lockB.tryLock()) {
try {
System.out.println("Thread 1: Acquired lock A and B!");
} finally {
lockB.unlock();
}
}
} finally {
lockA.unlock();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
Thread thread2 = new Thread(() -> {
try {
if (lockB.tryLock()) {
try {
Thread.sleep(100);
if (lockA.tryLock()) {
try {
System.out.println("Thread 2: Acquired lock B and A!");
} finally {
lockA.unlock();
}
}
} finally {
lockB.unlock();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
thread1.start();
thread2.start();
}
}
三、内存不可见性问题
内存不可见性是指一个线程对共享变量的修改,其他线程无法立即看到。在Java中,每个线程都有自己的工作内存(栈),对共享变量的修改可能会在工作内存中,而不是直接反映到主内存中。这种情况下,多个线程之间的交互可能会出现问题。
解决方法
-
使用
volatile
关键字:对于共享变量,加上volatile
关键字,确保每次读取变量时都从主内存读取,而不是从线程的工作内存中读取。 -
使用
synchronized
关键字:通过同步方法或同步块,保证对共享资源的访问是线程安全的。
示例代码(内存不可见性问题)
public class VisibilityExample {
private static volatile boolean flag = false;
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
while (!flag) {
// 等待flag变为true
}
System.out.println("Thread 1: flag is now true!");
});
Thread thread2 = new Thread(() -> {
try { Thread.sleep(1000); } catch (InterruptedException e) {}
flag = true;
System.out.println("Thread 2: set flag to true!");
});
thread1.start();
thread2.start();
}
}
总结
在JavaEE开发中,掌握并发编程的基本理论和实践是非常重要的。通过对死锁和内存不可见性问题的深入理解以及对相应解决方案的应用,可以有效地提升应用程序的稳定性和性能。在设计多线程程序时,应该时刻关注线程间的资源竞争,合理使用锁机制,以避免潜在的问题。