Alibaba开发规范_编程规约之并发处理:最佳实践与常见陷阱

文章目录

  • 引言
  • 1. 单例对象的线程安全
    • 1.1 规则
    • 1.2 解释
    • 1.3 代码示例
      • 正例
      • 反例
  • 2. 线程命名
    • 2.1 规则
    • 2.2 解释
    • 2.3 代码示例
      • 正例
      • 反例
  • 3. 线程池的使用
    • 3.1 规则
    • 3.2 解释
    • 3.3 代码示例
      • 正例
      • 反例
  • 4. 线程池的创建
    • 4.1 规则
    • 4.2 解释
    • 4.3 代码示例
      • 正例
      • 反例
  • 5. SimpleDateFormat的线程安全
    • 5.1 规则
    • 5.2 解释
    • 5.3 代码示例
      • 正例
      • 反例
  • 6. ThreadLocal的回收
    • 6.1 规则
    • 6.2 解释
    • 6.3 代码示例
      • 正例
      • 反例
  • 7. 锁的性能优化
    • 7.1 规则
    • 7.2 解释
    • 7.3 代码示例
      • 正例
      • 反例
  • 8. 加锁顺序
    • 8.1 规则
    • 8.2 解释
    • 8.3 代码示例
      • 正例
      • 反例
  • 9. 锁的释放
    • 9.1 规则
    • 9.2 解释
    • 9.3 代码示例
      • 正例
      • 反例
  • 10. 尝试获取锁
    • 10.1 规则
    • 10.2 解释
    • 10.3 代码示例
      • 正例
      • 反例
  • 11. 并发修改记录
    • 11.1 规则
    • 11.2 解释
    • 11.3 代码示例
      • 正例
      • 反例
  • 12. 定时任务的并发处理
    • 12.1 规则
    • 12.2 解释
    • 12.3 代码示例
      • 正例
      • 反例
  • 13. 资金相关的锁策略
    • 13.1 规则
    • 13.2 解释
    • 13.3 代码示例
      • 正例
      • 反例
  • 14. CountDownLatch的使用
    • 14.1 规则
    • 14.2 解释
    • 14.3 代码示例
      • 正例
        • 反例
  • 15. Random实例的多线程使用
    • 15.1 规则
    • 15.2 解释
    • 15.3 代码示例
      • 正例
      • 反例
  • 16. 双重检查锁
    • 16.1 规则
    • 16.2 解释
    • 16.3 代码示例
      • 正例
      • 反例
  • 17. volatile的使用
    • 17.1 规则
    • 17.2 解释
    • 17.3 代码示例
      • 正例
      • 反例
  • 18. HashMap的并发问题
    • 18.1 规则
    • 18.2 解释
    • 18.3 代码示例
      • 正例
      • 反例
  • 19. ThreadLocal的静态修饰
    • 19.1 规则
    • 19.2 解释
    • 19.3 代码示例
      • 正例
      • 反例
  • 小结

Alibaba开发规范_编程规约之并发处理:最佳实践与常见陷阱_第1张图片

引言

并发处理是Java编程中一个复杂且重要的领域。正确地处理并发问题可以显著提高应用程序的性能和稳定性,而错误的并发处理则可能导致难以调试的问题,如死锁、资源竞争和内存泄漏。接下來将深入探讨Java并发处理的最佳实践,并通过反例和正例代码来更好地理解和应用这些规则。

1. 单例对象的线程安全

1.1 规则

  • 获取单例对象需要保证线程安全,其中的方法也要保证线程安全

1.2 解释

单例模式是设计模式中最常见的一种,但在多线程环境下,单例对象的创建和访问可能会出现问题。如果单例对象的创建过程不是线程安全的,可能会导致多个线程创建多个实例,从而破坏单例的唯一性。

1.3 代码示例

正例

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语句块,导致创建多个实例。


2. 线程命名

2.1 规则

  • 创建线程或线程池时请指定有意义的线程名称,方便出错时回溯

2.2 解释

为线程指定有意义的名称可以在调试和排查问题时更快地定位问题。特别是在多线程环境下,线程名称可以理解线程的用途和上下文。

2.3 代码示例

正例

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

在反例中,线程池中的线程没有指定名称,调试时难以区分不同线程的用途。


3. 线程池的使用

3.1 规则

  • 线程资源必须通过线程池提供,不允许在应用中自行显式创建线程

3.2 解释

线程池可以有效地管理线程资源,减少线程创建和销毁的开销。显式创建线程可能会导致系统资源耗尽或线程过度切换的问题。

3.3 代码示例

正例

ExecutorService executor = Executors.newFixedThreadPool(10);
executor.submit(() -> {
    // 任务逻辑
});

反例

new Thread(() -> {
    // 任务逻辑
}).start();

在反例中,显式创建线程可能会导致系统资源耗尽。


4. 线程池的创建

4.1 规则

  • 线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式

4.2 解释

Executors提供的线程池工厂方法虽然方便,但隐藏了线程池的配置细节,容易导致资源耗尽的风险。通过ThreadPoolExecutor可以明确线程池的运行规则,规避这些风险。

4.3 代码示例

正例

ThreadPoolExecutor executor = new ThreadPoolExecutor(
    5, // 核心线程数
    10, // 最大线程数
    60L, // 空闲线程存活时间
    TimeUnit.SECONDS, // 时间单位
    new ArrayBlockingQueue<>(100) // 任务队列
);

反例

ExecutorService executor = Executors.newFixedThreadPool(10);

在反例中,Executors.newFixedThreadPool创建的线程池使用无界队列,可能会导致任务堆积,最终导致内存溢出。


5. SimpleDateFormat的线程安全

5.1 规则

  • SimpleDateFormat是线程不安全的类,一般不要定义为static变量,如果定义为static,必须加锁,或者使用DateUtils工具类

5.2 解释

SimpleDateFormat不是线程安全的,多个线程共享同一个SimpleDateFormat实例可能会导致日期解析错误或异常。

5.3 代码示例

正例

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实例,可能会导致日期解析错误。


6. ThreadLocal的回收

6.1 规则

  • 必须回收自定义的ThreadLocal变量,尤其在线程池场景下,线程经常会被复用,如果不清理自定义的ThreadLocal变量,可能会影响后续业务逻辑和造成内存泄露等问题

6.2 解释

ThreadLocal变量是线程局部变量,如果不及时清理,可能会导致内存泄漏,特别是在线程池中,线程会被复用,ThreadLocal变量可能会影响后续任务。

6.3 代码示例

正例

objectThreadLocal.set(userInfo);
try {
    // 业务逻辑
} finally {
    objectThreadLocal.remove();
}

反例

objectThreadLocal.set(userInfo);
// 业务逻辑

在反例中,ThreadLocal变量没有被清理,可能会导致内存泄漏。


7. 锁的性能优化

7.1 规则

  • 高并发时,同步调用应该去考量锁的性能损耗。能用无锁数据结构,就不要用锁;能锁区块,就不要锁整个方法体;能用对象锁,就不要用类锁

7.2 解释

锁的使用会带来性能开销,特别是在高并发环境下。尽量减少锁的粒度,避免在锁代码块中调用耗时的操作,如RPC调用。

7.3 代码示例

正例

synchronized (this) {
    // 最小化锁代码块
}

反例

public synchronized void method() {
    // 整个方法加锁
}

在反例中,整个方法被加锁,锁的粒度过大,可能会导致性能问题。


8. 加锁顺序

8.1 规则

  • 对多个资源、数据库表、对象同时加锁时,需要保持一致的加锁顺序,否则可能会造成死锁

8.2 解释

死锁通常发生在多个线程以不同的顺序获取锁时。保持一致的加锁顺序可以避免死锁的发生。

8.3 代码示例

正例

synchronized (lockA) {
    synchronized (lockB) {
        // 业务逻辑
    }
}

反例

synchronized (lockB) {
    synchronized (lockA) {
        // 业务逻辑
    }
}

在反例中,加锁顺序不一致,可能会导致死锁。


9. 锁的释放

9.1 规则

  • 在使用阻塞等待获取锁的方式中,必须在try代码块之外,并且在加锁方法与try代码块之间没有任何可能抛出异常的方法调用,避免加锁成功后,在finally中无法解锁

9.2 解释

如果在加锁和try代码块之间有可能会抛出异常的方法调用,可能会导致锁无法释放,从而引发死锁或其他问题。

9.3 代码示例

正例

Lock lock = new XxxLock();
lock.lock();
try {
    // 业务逻辑
} finally {
    lock.unlock();
}

反例

Lock lock = new XxxLock();
try {
    // 可能抛出异常的方法调用
    lock.lock();
    // 业务逻辑
} finally {
    lock.unlock();
}

在反例中,如果在lock.lock()之前的方法调用抛出异常,finally块中的unlock方法将无法执行,导致锁无法释放。


10. 尝试获取锁

10.1 规则

  • 在使用尝试机制来获取锁的方式中,进入业务代码块之前,必须先判断当前线程是否持有锁

10.2 解释

尝试获取锁的方式可以避免线程阻塞,但在进入业务代码块之前,必须确保当前线程已经成功获取锁,否则可能会导致业务逻辑错误。

10.3 代码示例

正例

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是否成功,可能会导致业务逻辑错误。


11. 并发修改记录

11.1 规则

  • 并发修改同一记录时,避免更新丢失,需要加锁。要么在应用层加锁,要么在缓存加锁,要么在数据库层使用乐观锁,使用version作为更新依据

11.2 解释

并发修改同一记录时,如果没有加锁机制,可能会导致更新丢失问题。乐观锁和悲观锁是常见的解决方案。

11.3 代码示例

正例

// 使用乐观锁
int version = record.getVersion();
record.setValue(newValue);
record.setVersion(version + 1);
updateRecord(record);

反例

// 不加锁
record.setValue(newValue);
updateRecord(record);

在反例中,没有加锁机制,可能会导致更新丢失。


12. 定时任务的并发处理

12.1 规则

  • 多线程并行处理定时任务时,Timer运行多个TimeTask时,只要其中之一没有捕获抛出的异常,其它任务便会自动终止运行,如果在处理定时任务时使用ScheduledExecutorService则没有这个问题

12.2 解释

Timer在处理多个任务时,如果其中一个任务抛出异常,其他任务也会被终止。而ScheduledExecutorService可以更好地处理这种情况。

12.3 代码示例

正例

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

在反例中,如果任务抛出异常,其他任务也会被终止。


13. 资金相关的锁策略

13.1 规则

  • 资金相关的金融敏感信息,使用悲观锁策略

13.2 解释

乐观锁在更新时可能会产生冲突,导致更新失败。对于资金相关的敏感信息,使用悲观锁可以确保数据的一致性。

13.3 代码示例

正例

synchronized (account) {
    account.withdraw(amount);
}

反例

int version = account.getVersion();
account.setBalance(account.getBalance() - amount);
account.setVersion(version + 1);
updateAccount(account);

在反例中,使用乐观锁可能会导致更新失败,影响资金安全。


14. CountDownLatch的使用

14.1 规则

  • 使用CountDownLatch进行异步转同步操作,每个线程退出前必须调用countDown方法,线程执行代码注意catch异常,确保countDown方法被执行到,避免主线程无法执行至await方法,直到超时才返回结果

14.2 解释

CountDownLatch可以用于将异步操作转换为同步操作,确保所有线程完成任务后再继续执行主线程。

14.3 代码示例

正例

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方法可能不会被执行,导致主线程无法继续执行。


15. Random实例的多线程使用

15.1 规则

  • 避免Random实例被多线程使用,虽然共享该实例是线程安全的,但会因竞争同一seed导致的性能下降

15.2 解释

Random实例在多线程环境下虽然线程安全,但多个线程竞争同一个seed会导致性能下降。可以使用ThreadLocalRandom来避免这个问题。

15.3 代码示例

正例

int random = ThreadLocalRandom.current().nextInt();

反例

Random random = new Random();
int randomValue = random.nextInt();

在反例中,多个线程共享同一个Random实例,可能会导致性能下降。


16. 双重检查锁

16.1 规则

  • 在并发场景下,通过双重检查锁(double-checked locking)实现延迟初始化的优化问题隐患,推荐解决方案中较为简单一种(适用于JDK5及以上版本),将目标属性声明为volatile型

16.2 解释

双重检查锁可以用于实现延迟初始化,但在JDK5之前,由于内存模型的问题,可能会导致初始化不完全。将目标属性声明为volatile可以解决这个问题。

16.3 代码示例

正例

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,可能会导致初始化不完全。


17. volatile的使用

17.1 规则

  • volatile解决多线程内存不可见问题。对于一写多读,是可以解决变量同步问题,但是如果多写,同样无法解决线程安全问题

17.2 解释

volatile关键字可以确保变量的可见性,但不能保证原子性。对于多写操作,仍然需要使用锁或其他同步机制来保证线程安全。

17.3 代码示例

正例

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,仍然无法保证线程安全。


18. HashMap的并发问题

18.1 规则

  • HashMap在容量不够进行resize时由于高并发可能出现死链,导致CPU飙升,在开发过程中可以使用其它数据结构或加锁来规避此风险

18.2 解释

HashMap在扩容时可能会出现死链问题,特别是在高并发环境下。可以使用ConcurrentHashMap或其他线程安全的集合来避免这个问题。

18.3 代码示例

正例

ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>();

反例

HashMap<String, String> map = new HashMap<>();

在反例中,HashMap在高并发环境下可能会出现死链问题。


19. ThreadLocal的静态修饰

19.1 规则

  • ThreadLocal对象使用static修饰,ThreadLocal无法解决共享对象的更新问题

19.2 解释

ThreadLocal是线程局部变量,每个线程都有自己独立的副本。使用static修饰ThreadLocal对象可以确保所有线程共享同一个ThreadLocal实例,但每个线程的变量副本仍然是独立的。

19.3 代码示例

正例

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并发处理是一个复杂且重要的领域,正确地处理并发问题可以显著提高应用程序的性能和稳定性。通过遵循最佳实践,开发者可以避免常见的错误,并编写出高效、健壮的并发代码。

Alibaba开发规范_编程规约之并发处理:最佳实践与常见陷阱_第2张图片

你可能感兴趣的:(【开发规范】,开发规范,并发处理)