并发处理是Java编程中一个复杂且重要的领域。正确地处理并发问题可以显著提高应用程序的性能和稳定性,而错误的并发处理则可能导致难以调试的问题,如死锁、资源竞争和内存泄漏。接下來将深入探讨Java并发处理的最佳实践,并通过反例和正例代码来更好地理解和应用这些规则。
单例模式是设计模式中最常见的一种,但在多线程环境下,单例对象的创建和访问可能会出现问题。如果单例对象的创建过程不是线程安全的,可能会导致多个线程创建多个实例,从而破坏单例的唯一性。
public class Singleton {
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
在反例中,getInstance
方法不是线程安全的,多个线程可能同时进入if
语句块,导致创建多个实例。
为线程指定有意义的名称可以在调试和排查问题时更快地定位问题。特别是在多线程环境下,线程名称可以理解线程的用途和上下文。
public class UserThreadFactory implements ThreadFactory {
private final String namePrefix;
private final AtomicInteger nextId = new AtomicInteger(1);
UserThreadFactory(String whatFeaturOfGroup) {
namePrefix = "From UserThreadFactory's " + whatFeaturOfGroup + "-Worker-";
}
@Override
public Thread newThread(Runnable task) {
String name = namePrefix + nextId.getAndIncrement();
Thread thread = new Thread(null, task, name, 0, false);
System.out.println(thread.getName());
return thread;
}
}
ExecutorService executor = Executors.newFixedThreadPool(10);
在反例中,线程池中的线程没有指定名称,调试时难以区分不同线程的用途。
线程池可以有效地管理线程资源,减少线程创建和销毁的开销。显式创建线程可能会导致系统资源耗尽或线程过度切换的问题。
ExecutorService executor = Executors.newFixedThreadPool(10);
executor.submit(() -> {
// 任务逻辑
});
new Thread(() -> {
// 任务逻辑
}).start();
在反例中,显式创建线程可能会导致系统资源耗尽。
Executors
提供的线程池工厂方法虽然方便,但隐藏了线程池的配置细节,容易导致资源耗尽的风险。通过ThreadPoolExecutor
可以明确线程池的运行规则,规避这些风险。
ThreadPoolExecutor executor = new ThreadPoolExecutor(
5, // 核心线程数
10, // 最大线程数
60L, // 空闲线程存活时间
TimeUnit.SECONDS, // 时间单位
new ArrayBlockingQueue<>(100) // 任务队列
);
ExecutorService executor = Executors.newFixedThreadPool(10);
在反例中,Executors.newFixedThreadPool
创建的线程池使用无界队列,可能会导致任务堆积,最终导致内存溢出。
SimpleDateFormat
不是线程安全的,多个线程共享同一个SimpleDateFormat
实例可能会导致日期解析错误或异常。
private static final ThreadLocal<DateFormat> df = new ThreadLocal<DateFormat>() {
@Override
protected DateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd");
}
};
private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
在反例中,多个线程共享同一个SimpleDateFormat
实例,可能会导致日期解析错误。
ThreadLocal
变量是线程局部变量,如果不及时清理,可能会导致内存泄漏,特别是在线程池中,线程会被复用,ThreadLocal
变量可能会影响后续任务。
objectThreadLocal.set(userInfo);
try {
// 业务逻辑
} finally {
objectThreadLocal.remove();
}
objectThreadLocal.set(userInfo);
// 业务逻辑
在反例中,ThreadLocal
变量没有被清理,可能会导致内存泄漏。
锁的使用会带来性能开销,特别是在高并发环境下。尽量减少锁的粒度,避免在锁代码块中调用耗时的操作,如RPC调用。
synchronized (this) {
// 最小化锁代码块
}
public synchronized void method() {
// 整个方法加锁
}
在反例中,整个方法被加锁,锁的粒度过大,可能会导致性能问题。
死锁通常发生在多个线程以不同的顺序获取锁时。保持一致的加锁顺序可以避免死锁的发生。
synchronized (lockA) {
synchronized (lockB) {
// 业务逻辑
}
}
synchronized (lockB) {
synchronized (lockA) {
// 业务逻辑
}
}
在反例中,加锁顺序不一致,可能会导致死锁。
如果在加锁和try
代码块之间有可能会抛出异常的方法调用,可能会导致锁无法释放,从而引发死锁或其他问题。
Lock lock = new XxxLock();
lock.lock();
try {
// 业务逻辑
} finally {
lock.unlock();
}
Lock lock = new XxxLock();
try {
// 可能抛出异常的方法调用
lock.lock();
// 业务逻辑
} finally {
lock.unlock();
}
在反例中,如果在lock.lock()
之前的方法调用抛出异常,finally
块中的unlock
方法将无法执行,导致锁无法释放。
尝试获取锁的方式可以避免线程阻塞,但在进入业务代码块之前,必须确保当前线程已经成功获取锁,否则可能会导致业务逻辑错误。
Lock lock = new XxxLock();
boolean isLocked = lock.tryLock();
if (isLocked) {
try {
// 业务逻辑
} finally {
lock.unlock();
}
}
Lock lock = new XxxLock();
lock.tryLock();
try {
// 业务逻辑
} finally {
lock.unlock();
}
在反例中,没有判断tryLock
是否成功,可能会导致业务逻辑错误。
并发修改同一记录时,如果没有加锁机制,可能会导致更新丢失问题。乐观锁和悲观锁是常见的解决方案。
// 使用乐观锁
int version = record.getVersion();
record.setValue(newValue);
record.setVersion(version + 1);
updateRecord(record);
// 不加锁
record.setValue(newValue);
updateRecord(record);
在反例中,没有加锁机制,可能会导致更新丢失。
Timer
在处理多个任务时,如果其中一个任务抛出异常,其他任务也会被终止。而ScheduledExecutorService
可以更好地处理这种情况。
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(10);
scheduler.scheduleAtFixedRate(() -> {
try {
// 任务逻辑
} catch (Exception e) {
// 异常处理
}
}, 0, 1, TimeUnit.SECONDS);
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
// 任务逻辑
}
}, 0, 1000);
在反例中,如果任务抛出异常,其他任务也会被终止。
乐观锁在更新时可能会产生冲突,导致更新失败。对于资金相关的敏感信息,使用悲观锁可以确保数据的一致性。
synchronized (account) {
account.withdraw(amount);
}
int version = account.getVersion();
account.setBalance(account.getBalance() - amount);
account.setVersion(version + 1);
updateAccount(account);
在反例中,使用乐观锁可能会导致更新失败,影响资金安全。
CountDownLatch
可以用于将异步操作转换为同步操作,确保所有线程完成任务后再继续执行主线程。
CountDownLatch latch = new CountDownLatch(10);
for (int i = 0; i < 10; i++) {
executor.submit(() -> {
try {
// 任务逻辑
} finally {
latch.countDown();
}
});
}
latch.await();
CountDownLatch latch = new CountDownLatch(10);
for (int i = 0; i < 10; i++) {
executor.submit(() -> {
// 任务逻辑
latch.countDown();
});
}
latch.await();
在反例中,如果任务抛出异常,countDown
方法可能不会被执行,导致主线程无法继续执行。
Random
实例在多线程环境下虽然线程安全,但多个线程竞争同一个seed
会导致性能下降。可以使用ThreadLocalRandom
来避免这个问题。
int random = ThreadLocalRandom.current().nextInt();
Random random = new Random();
int randomValue = random.nextInt();
在反例中,多个线程共享同一个Random
实例,可能会导致性能下降。
双重检查锁可以用于实现延迟初始化,但在JDK5之前,由于内存模型的问题,可能会导致初始化不完全。将目标属性声明为volatile
可以解决这个问题。
public class LazyInitDemo {
private volatile Helper helper;
public Helper getHelper() {
if (helper == null) {
synchronized (this) {
if (helper == null) {
helper = new Helper();
}
}
}
return helper;
}
}
public class LazyInitDemo {
private Helper helper;
public Helper getHelper() {
if (helper == null) {
synchronized (this) {
if (helper == null) {
helper = new Helper();
}
}
}
return helper;
}
}
在反例中,helper
属性没有声明为volatile
,可能会导致初始化不完全。
volatile
关键字可以确保变量的可见性,但不能保证原子性。对于多写操作,仍然需要使用锁或其他同步机制来保证线程安全。
private volatile boolean flag;
public void setFlag(boolean flag) {
this.flag = flag;
}
public boolean isFlag() {
return flag;
}
private int count;
public void increment() {
count++; // 非原子操作
}
在反例中,count++
操作不是原子操作,即使count
声明为volatile
,仍然无法保证线程安全。
HashMap
在扩容时可能会出现死链问题,特别是在高并发环境下。可以使用ConcurrentHashMap
或其他线程安全的集合来避免这个问题。
ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>();
HashMap<String, String> map = new HashMap<>();
在反例中,HashMap
在高并发环境下可能会出现死链问题。
ThreadLocal
是线程局部变量,每个线程都有自己独立的副本。使用static
修饰ThreadLocal
对象可以确保所有线程共享同一个ThreadLocal
实例,但每个线程的变量副本仍然是独立的。
private static final ThreadLocal<SimpleDateFormat> dateFormat = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
private final ThreadLocal<SimpleDateFormat> dateFormat = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
在反例中,ThreadLocal
对象没有使用static
修饰,可能会导致每个线程创建多个ThreadLocal
实例,浪费资源。
Java并发处理是一个复杂且重要的领域,正确地处理并发问题可以显著提高应用程序的性能和稳定性。通过遵循最佳实践,开发者可以避免常见的错误,并编写出高效、健壮的并发代码。