聊聊并发编程的十个坑(并发编程十大常见陷阱解析)

原创
ithorizon 7个月前 (10-20) 阅读数 24 #后端开发

并发编程十大常见陷阱解析

一、竞态条件(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提供了一些线程平安的类,如VectorHashtable等,但这些类的线程平安性是基于特定的操作序列。如果使用不当,仍然或许让并发问题。

例子:

public class ThreadSafeClassDemo {

private final Vector vector = new Vector<>();

public void addElement(int element) {

vector.add(element); // 非线程平安操作

}

public int getSize() {

return vector.size();

}

}

虽然Vector是线程平安的,但是addsize组合在一起并不是原子操作,或许让并发问题。

七、失误的锁顺序

失误的锁顺序或许让死锁或者性能问题。通常情况下,应该按照固定的顺序获取锁。

例子:

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) {

// 执行操作

}

}

}

}

在上面的代码中,如果method1method2同时被调用,或许会出现死锁。

八、失误的锁粒度

锁粒度是指锁定的数据范围。过粗的锁粒度会降低并发性能,过细的锁粒度会增长锁的开销。

例子:

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();

}

}

}

在上面的代码中,如果method1method2同时被调用,或许会出现死锁,出于锁没有被正确释放。

十、共享数据的可见性问题

可见性问题是指一个线程对共享变量的修改,对于其他线程来说或许不可见。这通常是由于线程缓存或者指令重排让的。

例子:

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或许读取到的是旧值。

总结:并发编程是一项繁复的任务,需要开发者对多线程、锁、同步机制等有深入的领会。避免上述十大常见陷阱,可以帮助开发者编写出更平安、更高效的并发程序。


本文由IT视界版权所有,禁止未经同意的情况下转发

文章标签: 后端开发


热门