优质博文:IT-BLOG-CN
【1】【强制】获取单例对象需要保证线程安全,其中的方法也要保证线程安全。
说明: 资源驱动类、工具类、单例工厂类都需要注意。
【2】【强制】创建线程或线程池时请指定有意义的线程名称,方便出错时回溯。
正例: 自定义线程工厂,并且根据外部特征进行分组,比如,来自同一机房的调用,把机房编号赋值给whatFeaturOfGroup
public class UserThreadFactory implements ThreadFactory {
private final String namePrefix; private final AtomicInteger nextId = new AtomicInteger(1);
// 定义线程组名称,在 jstack 问题排查时,非常有帮助
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;
}
}
【3】【强制】线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。
说明: 线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决资源不足的问题。 如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。
【4】【强制】线程池不允许使用Executors
去创建,而是通过ThreadPoolExecutor
的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
说明:Executors
返回的线程池对象的弊端如下:
1)FixedThreadPool
和SingleThreadPool
:允许的请求队列长度为Integer.MAX_VALUE
,可能会堆积大量的请求,从而导致OOM
。
2)CachedThreadPool
: 允许的创建线程数量为Integer.MAX_VALUE
,可能会创建大量的线程,从而导致OOM
。
【5】【强制】SimpleDateFormat
是线程不安全的类,一般不要定义为static
变量,如果定义为static
, 必须加锁,或者使用DateUtils
工具类。
正例:注意线程安全,使用DateUtils
。亦推荐如下处理:
private static final ThreadLocal df = new ThreadLocal() {
@Override
protected DateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd");
}
};
说明: 如果是JDK8
的应用,可以使用Instant
代替Date
,LocalDateTime
代替Calendar
,DateTimeFormatter
代替SimpleDateFormat
,官方给出的解释:simple beautiful strong immutable thread-safe
。
【6】【强制】必须回收自定义的ThreadLocal
变量,尤其在线程池场景下,线程经常会被复用, 如果不清理自定义的ThreadLocal
变量,可能会影响后续业务逻辑和造成内存泄露等问题。 尽量在代理中使用try-finally
块进行回收。
// 正例:
objectThreadLocal.set(userInfo);
try {
// ...
} finally {
objectThreadLocal.remove();
}
【7】【强制】高并发时,同步调用应该去考量锁的性能损耗。能用无锁数据结构,就不要用锁;能 锁区块,就不要锁整个方法体;能用对象锁,就不要用类锁。
说明: 尽可能使加锁的代码块工作量尽可能的小,避免在锁代码块中调用RPC
方法。
【8】【强制】对多个资源、数据库表、对象同时加锁时,需要保持一致的加锁顺序,否则可能会造 成死锁。
说明: 线程一需要对表A
、B
、C
依次全部加锁后才可以进行更新操作,那么线程二的加锁顺序也必须是A
、B
、C
,否则可能出现死锁。
【9】【强制】在使用阻塞等待获取锁的方式中,必须在try
代码块之外,并且在加锁方法与try
代码块之间没有任何可能抛出异常的方法调用,避免加锁成功后,在finally
中无法解锁。
说明一:如果在lock
方法与try
代码块之间的方法调用抛出异常,那么无法解锁,造成其它线程无法成功 获取锁。
说明二:如果lock
方法在try
代码块之内,可能由于其它方法抛出异常,导致在finally
代码块中,unlock
对未加锁的对象解锁,它会调用AQS
的tryRelease
方法(取决于具体实现类),抛出IllegalMonitorStateException
异常。
说明三:在Lock
对象的lock
方法实现中可能抛出unchecked
异常,产生的后果与说明二相同。
//正例:
Lock lock = new XxxLock(); // ...
lock.lock();
try {
doSomething();
doOthers();
} finally {
lock.unlock();
}
//反例:
Lock lock = new XxxLock();
// ...
try {
// 如果此处抛出异常,则直接执行 finally 代码块
doSomething();
// 无论加锁是否成功,finally 代码块都会执行
lock.lock();
doOthers();
} finally {
lock.unlock();
}
【10】【强制】在使用尝试机制来获取锁的方式中,进入业务代码块之前,必须先判断当前线程是否持有锁。锁的释放规则与锁的阻塞等待方式相同。
说明: Lock
对象的unlock
方法在执行时,它会调用AQS
的tryRelease
方法(取决于具体实现类),如果 当前线程不持有锁,则抛出 IllegalMonitorStateException
异常。
// 正例:
Lock lock = new XxxLock();
// ...
boolean isLocked = lock.tryLock();
if (isLocked) {
try {
doSomething();
doOthers();
} finally {
lock.unlock();
}
}
【11】【强制】并发修改同一记录时,避免更新丢失,需要加锁。要么在应用层加锁,要么在缓存加锁,要么在数据库层使用乐观锁,使用version
作为更新依据。
说明: 如果每次访问冲突概率小于20%
,推荐使用乐观锁,否则使用悲观锁。乐观锁的重试次数不得小于3
次。
【12】【强制】多线程并行处理定时任务时,Timer
运行多个TimeTask
时,只要其中之一没有捕获抛出的异常,其它任务便会自动终止运行,使用 ScheduledExecutorService
则没有这个问题。
【13】【推荐】资金相关的金融敏感信息,使用悲观锁策略。
说明: 乐观锁在获得锁的同时已经完成了更新操作,校验逻辑容易出现漏洞,另外,乐观锁对冲突的解决策 略有较复杂的要求,处理不当容易造成系统压力或数据异常,所以资金相关的金融敏感信息不建议使用乐观锁更新。
正例: 悲观锁遵循一锁二判三更新四释放的原则
【14】【推荐】使用CountDownLatch
进行异步转同步操作,每个线程退出前必须调用countDown
方法,线程执行代码注意catch
异常,确保countDown
方法被执行到,避免主线程无法执行至await
方法,直到超时才返回结果。
说明: 注意,子线程抛出异常堆栈,不能在主线程try-catch
到。
【15】【推荐】避免Random
实例被多线程使用,虽然共享该实例是线程安全的,但会因竞争同一seed
导致的性能下降。
说明: Random
实例包括java.util.Random
的实例或者Math.random()
的方式。
正例: 在JDK7
之后,可以直接使用API ThreadLocalRandom
,而在JDK7
之前,需要编码保证每个线 程持有一个单独的Random
实例。