聊聊并发编程的十个坑(并发编程十大常见陷阱解析)
原创
一、竞态条件(Race Conditions)
竞态条件是并发编程中最常见的陷阱之一。当两个或多个线程访问共享资源,并且至少有一个线程对资源进行写操作时,如果没有适当的同步机制,就或许出现竞态条件。
例子:
public class Counter {
private int count = 0;
public void increment() {
count++; // 非原子操作
}
public int getCount() {
return count;
}
}
在上面的例子中,increment
方法中的 count++
操作实际上是一个非原子操作,或许会让多个线程同时修改 count
的值,从而让计数不确切。
二、死锁(Deadlocks)
死锁是指两个或多个线程在等待对方释放锁时,让都无法继续执行的状态。死锁通常出现在多个线程尝试以不同的顺序获取相同的锁。
例子:
public class DeadlockDemo {
public static void main(String[] args) {
final Object resource1 = "Resource1";
final Object resource2 = "Resource2";
Thread t1 = new Thread(() -> {
synchronized (resource1) {
System.out.println("Thread 1: Locked resource 1");
try { Thread.sleep(100); } catch (Exception e) {}
synchronized (resource2) {
System.out.println("Thread 1: Locked resource 2");
}
}
});
Thread t2 = new Thread(() -> {
synchronized (resource2) {
System.out.println("Thread 2: Locked resource 2");
try { Thread.sleep(100); } catch (Exception e) {}
synchronized (resource1) {
System.out.println("Thread 2: Locked resource 1");
}
}
});
t1.start();
t2.start();
}
}
在上面的代码中,线程1和线程2尝试以不同的顺序获取相同的锁,这或许让死锁。
三、活锁(Livelocks)
活锁是指线程在执行过程中,虽然没有出现死锁,但是线程始终无法完成其任务,出于它逐步地被其他线程的锁请求所中断。
例子:
public class LivelockDemo {
private final static int NUM_THREADS = 2;
private final static int NUMTurns = 5;
public static void main(String[] args) {
final Object resource1 = new Object();
final Object resource2 = new Object();
Thread t1 = new Thread(() -> {
for (int i = 0; i < NUMTurns; i++) {
synchronized (resource1) {
System.out.println("Thread 1: Locked resource 1");
synchronized (resource2) {
System.out.println("Thread 1: Locked resource 2");
}
}
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < NUMTurns; i++) {
synchronized (resource2) {
System.out.println("Thread 2: Locked resource 2");
synchronized (resource1) {
System.out.println("Thread 2: Locked resource 1");
}
}
}
});
t1.start();
t2.start();
}
}
在这个例子中,两个线程逐步尝试获取对方的锁,让它们都无法完成操作。
四、饥饿(Starvation)
饥饿是指一个线程出于无法获取所需的资源而无法继续执行。这通常出现在线程优先级较低,或者锁的实现不公平的情况下。
例子:
public class StarvationDemo {
private final Semaphore semaphore = new Semaphore(1);
public void method1() {
try {
semaphore.acquire();
// 执行操作
} finally {
semaphore.release();
}
}
public void method2() {
// 优先级较高的线程
try {
semaphore.acquire();
// 执行操作
} finally {
semaphore.release();
}
}
}
在上面的例子中,如果线程2(优先级较高)持续执行,线程1或许会出于无法获取到信号量而处于饥饿状态。
五、优先级反转(Priority Inversion)
优先级反转是指一个低优先级线程持有锁,而高优先级线程等待这个锁,让系统性能下降的现象。
例子:
public class PriorityInversionDemo {
private final ReentrantLock lock = new ReentrantLock();
public void highPriorityTask() {
lock.lock();
try {
// 执行高优先级任务
} finally {
lock.unlock();
}
}
public void lowPriorityTask() {
lock.lock();
try {
// 执行低优先级任务
} finally {
lock.unlock();
}
}
}
在上面的代码中,如果低优先级任务先获取了锁,高优先级任务将无法执行,让优先级反转。
六、线程平安类的不正确使用
Java提供了一些线程平安的类,如Vector
、Hashtable
等,但这些类的线程平安性是基于特定的操作序列。如果使用不当,仍然或许让并发问题。
例子:
public class ThreadSafeClassDemo {
private final Vector
vector = new Vector<>(); public void addElement(int element) {
vector.add(element); // 非线程平安操作
}
public int getSize() {
return vector.size();
}
}
虽然Vector
是线程平安的,但是add
和size
组合在一起并不是原子操作,或许让并发问题。
七、失误的锁顺序
失误的锁顺序或许让死锁或者性能问题。通常情况下,应该按照固定的顺序获取锁。
例子:
public class LockOrderDemo {
private final Object lock1 = new Object();
private final Object lock2 = new Object();
public void method1() {
synchronized (lock2) {
synchronized (lock1) {
// 执行操作
}
}
}
public void method2() {
synchronized (lock1) {
synchronized (lock2) {
// 执行操作
}
}
}
}
在上面的代码中,如果method1
和method2
同时被调用,或许会出现死锁。
八、失误的锁粒度
锁粒度是指锁定的数据范围。过粗的锁粒度会降低并发性能,过细的锁粒度会增长锁的开销。
例子:
public class LockGranularityDemo {
private final List
list = new ArrayList<>(); public void addElement(int element) {
synchronized (list) { // 过粗的锁粒度
list.add(element);
}
}
public int getSize() {
synchronized (list) { // 过粗的锁粒度
return list.size();
}
}
}
在上面的代码中,对整个列表加锁,这会让即使只有一个线程需要修改列表,其他线程也无法执行读取操作。
九、不恰当的锁策略
不恰当的锁策略或许会让死锁、饥饿或活锁。选择合适的锁策略是确保并发程序正确性的关键。
例子:
public class LockStrategyDemo {
private final ReentrantLock lock = new ReentrantLock();
public void method1() {
lock.lock();
try {
// 执行操作
} finally {
lock.unlock();
}
}
public void method2() {
lock.lock();
try {
// 执行操作
} finally {
lock.unlock();
}
}
}
在上面的代码中,如果method1
和method2
同时被调用,或许会出现死锁,出于锁没有被正确释放。
十、共享数据的可见性问题
可见性问题是指一个线程对共享变量的修改,对于其他线程来说或许不可见。这通常是由于线程缓存或者指令重排让的。
例子:
public class VisibilityDemo {
private boolean flag = false;
public void setFlag() {
flag = true;
}
public void clearFlag() {
flag = false;
}
public boolean getFlag() {
return flag;
}
}
在上面的代码中,如果线程A修改了flag
的值,而线程B读取flag
的值,由于缓存和指令重排的影响,线程B或许读取到的是旧值。
总结:并发编程是一项繁复的任务,需要开发者对多线程、锁、同步机制等有深入的领会。避免上述十大常见陷阱,可以帮助开发者编写出更平安、更高效的并发程序。