聊聊Java中并发编程的十个坑("Java并发编程常见十大陷阱解析")
原创
一、共享数据的不当访问
在并发编程中,最常见的问题之一就是多个线程对共享数据的访问。如果不对共享数据进行适当的同步,就大概让数据不一致或竞态条件。下面是一个易懂的例子:
public class Counter {
private int count = 0;
public void increment() {
count++; // 这里没有进行同步
}
public int getCount() {
return count;
}
}
在这个例子中,如果多个线程同时调用`increment()`方法,由于`count++`操作不是原子性的,它实际上分为三步:读取`count`的值,增长1,然后写回新的值。如果两个线程同时读取相同的值,它们都会增长1并写回,从而让丢失一个增量。
二、死锁
死锁是指两个或多个线程由于互相等待对方释放锁而无法继续执行的状态。死锁通常出现在多个线程需要同时获取多个锁时。下面是一个易懂的死锁示例:
public class DeadlockDemo {
private static final Object resource1 = new Object();
private static final Object resource2 = new Object();
public static void main(String[] args) {
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都尝试获取两个锁,但它们的获取顺序不同。这大概让死锁,由于每个线程都在等待另一个线程释放锁。
三、线程可靠类的误用
Java提供了一些线程可靠的类,如`Vector`、`Hashtable`等,但如果使用不当,也大概让并发问题。例如,使用线程可靠的集合类时,仍然需要考虑复合操作的一致性。
Vector
vector = new Vector<>(); vector.add(1);
vector.add(2);
public void processVector() {
if (!vector.isEmpty()) { // 这里没有同步
vector.remove(0);
}
}
在这个例子中,虽然`Vector`是线程可靠的,但检查和删除操作不是原子性的。如果两个线程同时调用`processVector()`方法,大概会引发`ConcurrentModificationException`异常。
四、不正确的锁顺序
锁顺序不一致大概让死锁。正确的做法是确保所有线程获取锁的顺序一致。
public void method1() {
synchronized (lock1) {
synchronized (lock2) {
// do something
}
}
}
public void method2() {
synchronized (lock2) { // 不正确的锁顺序
synchronized (lock1) {
// do something
}
}
}
五、活锁
活锁是指线程虽然没有被阻塞,但仍然无法向前推进,由于其执行的操作总是被其他线程影响。下面是一个易懂的活锁示例:
public class LiveLockDemo {
private static final Object lock = new Object();
public void method1() {
synchronized (lock) {
System.out.println("Thread 1: Enter method1");
method2();
}
}
public void method2() {
synchronized (lock) {
System.out.println("Thread 1: Enter method2");
method1();
}
}
}
在这个例子中,`method1`和`method2`都在尝试获取同一个锁,但它们互相调用对方,让没有任何线程能够完成执行。
六、线程池使用不当
线程池是Java并发编程中常用的工具,但如果使用不当,也大概让问题。例如,创建过多的线程或线程池配置不当大概让内存溢出或性能下降。
ExecutorService executorService = Executors.newFixedThreadPool(1000); // 不合理的线程池大小
for (int i = 0; i < 10000; i++) {
executorService.submit(() -> {
// 执行任务
});
}
executorService.shutdown();
在这个例子中,创建了一个包含1000个线程的线程池,然后提交了10000个任务。这大概让内存溢出,由于线程池和任务都需要占用内存。
七、不正确的线程间通信
在Java中,线程间的通信通常使用`wait()`、`notify()`和`notifyAll()`方法。如果使用不当,大概让线程无法正确地被唤醒或通知。
public class ThreadCommunicationDemo {
private static final Object lock = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
synchronized (lock) {
try {
lock.wait();
} catch (InterruptedException e) {}
}
});
Thread t2 = new Thread(() -> {
synchronized (lock) {
lock.notify();
}
});
t1.start();
t2.start();
}
}
在这个例子中,线程t1尝试在锁对象上调用`wait()`方法,但线程t2在调用`notify()`方法之前已经释放了锁。故而,t1将永远等待,由于它没有收到通知。
八、不正确的线程休眠时间
在并发编程中,线程休眠是一种常见的操作,但如果休眠时间设置不当,大概让程序响应缓慢或资源浪费。
public void method() {
try {
Thread.sleep(1000); // 休眠时间太长
} catch (InterruptedException e) {}
// 执行任务
}
在这个例子中,线程休眠了1000毫秒,这大概是不必要的,尤其是当任务可以更快完成时。这会让程序响应变慢。
九、不正确的线程生命周期管理
线程的生命周期管理是一个重要的方面,如果管理不当,大概让线程无法正确终止或资源无法释放。
public void method() {
Thread thread = new Thread(() -> {
// 执行任务
});
thread.start();
// 忘记调用join()或适当的管理线程生命周期
}
在这个例子中,线程被启动但没有被正确地管理。如果主线程在子线程终止之前退出,子线程大概无法完成执行。
十、忽视JVM和硬件的差异性
Java虚拟机(JVM)和硬件平台之间的差异大概会影响并发程序的性能和稳定性。忽视这些差异大概让并发程序在某些环境中表现不佳。
例如,不同的JVM实现大概有不同的垃圾回收策略和线程调度算法。硬件平台的差异,如CPU核心数和缓存策略,也会影响并发程序的性能。
综上所述,Java并发编程中存在许多潜在的陷阱。要编写高效的并发程序,需要深入领会并发原理,合理使用线程和锁,并注意线程间的通信和同步。同时,也要考虑到JVM和硬件平台的差异性,以便在不同的环境中都能获得良好的性能。