
【强制】获取单例对象需要保证线程安全其中的方法也要保证线程安全。说明资源驱动类、工具类、单例工厂类都需要注意。【强制】创建线程或线程池时请指定有意义的线程名称方便出错时回溯。正例自定义线程工厂并且根据外部特征进行分组比如来自同一机房的调用把机房编号赋值给whatFeatureOfGrouppublicclassUserThreadFactoryimplementsThreadFactory{privatefinalStringnamePrefix;privatefinalAtomicIntegernextIdnewAtomicInteger(1);// 定义线程组名称在利用 jstack 来排查问题时非常有帮助UserThreadFactory(StringwhatFeatureOfGroup){namePrefixFrom UserThreadFactorys whatFeatureOfGroup-Worker-;}OverridepublicThreadnewThread(Runnabletask){StringnamenamePrefixnextId.getAndIncrement();ThreadthreadnewThread(null,task,name,0,false);System.out.println(thread.getName());returnthread;}}【强制】线程资源必须通过线程池提供不允许在应用中自行显式创建线程。说明线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销解决资源不足的问题。如果不使用线程池有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。【强制】线程池不允许使用 Executors 去创建而是通过 ThreadPoolExecutor 的方式这样的处理方式让写的同学更加明确线程池的运行规则规避资源耗尽的风险。说明Executors 返回的线程池对象的弊端如下1 FixedThreadPool 和 SingleThreadPool允许的请求队列长度为 Integer.MAX_VALUE可能会堆积大量的请求从而导致 OOM。2 CachedThreadPool允许的创建线程数量为 Integer.MAX_VALUE可能会创建大量的线程从而导致 OOM【强制】SimpleDateFormat 是线程不安全的类一般不要定义为 static 变量如果定义为 static必须加锁或者使用 DateUtils 工具类。正例注意线程安全使用 DateUtils。亦推荐如下处理privatestaticfinalThreadLocalDateFormatdfnewThreadLocalDateFormat(){OverrideprotectedDateFormatinitialValue(){returnnewSimpleDateFormat(yyyy-MM-dd);}};说明如果是 JDK8 的应用可以使用 Instant 代替 DateLocalDateTime 代替 CalendarDateTimeFormatter 代替 SimpleDateFormat官方给出的解释simple beautiful strong immutable thread-safe。【强制】必须回收自定义的 ThreadLocal 变量尤其在线程池场景下线程经常会被复用如果不清理自定义的 ThreadLocal 变量可能会影响后续业务逻辑和造成内存泄露等问题。尽量在代理中使用 try-finally 块进行回收。objectThreadLocal.set(userInfo);try{// ...}finally{objectThreadLocal.remove();}【强制】高并发时同步调用应该去考量锁的性能损耗。能用无锁数据结构就不要用锁能锁区块就不要锁整个方法体能用对象锁就不要用类锁。说明尽可能使加锁的代码块工作量尽可能的小避免在锁代码块中调用 RPC 方法。【强制】对多个资源、数据库表、对象同时加锁时需要保持一致的加锁顺序否则可能会造成死锁。说明线程一需要对表 A、B、C 依次全部加锁后才可以进行更新操作那么线程二的加锁顺序也必须是 A、B、C否则可能出现死锁。【强制】在使用阻塞等待获取锁的方式中必须在 try 代码块之外并且在加锁方法与 try 代码块之间没有任何可能抛出异常的方法调用避免加锁成功后在 finally 中无法解锁。说明一如果在 lock 方法与 try 代码块之间的方法调用抛出异常那么无法解锁造成其它线程无法成功取锁。说明二如果 lock 方法在 try 代码块之内可能由于其它方法抛出异常导致在 finally 代码块中unlock对未加锁的对象解锁它会调用 AQS 的 tryRelease 方法取决于具体实现类抛出IllegalMonitorStateException 异常。说明三在 Lock 对象的 lock 方法实现中可能抛出 unchecked 异常产生的后果与说明二相同。10. 【强制】在使用尝试机制来获取锁的方式中进入业务代码块之前必须先判断当前线程是否持有锁。锁的释放规则与锁的阻塞等待方式相同。说明Lock 对象的 unlock 方法在执行时它会调用 AQS 的 tryRelease 方法取决于具体实现类如果当前线程不持有锁则抛出 IllegalMonitorStateException 异常。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 实例。16.【推荐】通过双重检查锁double-checked locking在并发场景下存在延迟初始化的优化问题隐患可参考 The “Double-Checked Locking is Broken” Declaration推荐解决方案中较为简单一种适用于 JDK5 及以上版本将目标属性声明为 volatile 型比如将 helper 的属性声明修改为private volatile Helper helper null;。publicclassLazyInitDemo{privatevolatileHelperhelpernull;publicHelpergetHelper(){if(helpernull){synchronized(this){if(helpernull){helpernewHelper();}}}returnhelper;}// other methods and fields...}为啥helper判断两次以及为啥被修饰为volatile类型的变量呢双重检查锁定Double-Checked Locking, DCL 模式用于实现线程安全的懒加载Lazy Initialization它的核心目的是在保证线程安全的前提下尽可能减少同步锁synchronized带来的性能开销。1. 为什么要判断两次双重检查的逻辑代码中有两个 if (helper null) 判断第一次判断锁外if (helper null)第二次判断锁内if (helper null)第一次判断为了性能避免不必要的锁竞争场景当 helper 已经被初始化后绝大多数线程调用 getHelper() 时helper 都不为 null。作用如果第一次判断发现 helper 不为 null直接返回对象完全不需要进入 synchronized 代码块。收益synchronized 是有性能开销的涉及用户态/内核态切换、获取锁、释放锁等。如果没有第一次判断每次调用都要排队抢锁即使对象已经创建好了这会严重拖慢系统性能。第二次判断为了安全防止重复创建场景假设有两个线程 A 和 B 同时第一次调用 getHelper()线程 A 执行第一次判断发现 helper 为 null准备进入锁就在 A 进入锁之前线程 B 也执行了第一次判断发现 helper 依然为 null因为 A 还没创建完于是 B 也准备进入锁。线程 A 抢到锁进入内部执行第二次判断此时仍为 null于是 new Helper()赋值释放锁。线程 B 抢到锁进入内部。如果没有第二次判断B 也会执行 new Helper()导致对象被创建了两次且可能覆盖 A 创建的对象造成逻辑错误或资源浪费。作用确保在多线程并发竞争锁的情况下只有一个线程能真正执行创建对象的操作。总结外层判断优化性能大部分时间不走锁。内层判断保证正确性防止并发下多次创建。2. 为什么要定义为 volatile这是 DCL 模式中最关键、也最容易被忽略的一点。如果没有 volatile这段代码在多线程环境下是不安全的可能会返回一个未初始化完成的“半成品的” Helper 对象。根本原因指令重排序Instruction Reordering在 Java 中helper new Helper(); 这行代码并不是原子操作它在底层字节码层面大致分为三步分配内存在堆内存中为 Helper 对象分配空间。初始化对象调用构造函数填充对象属性执行 new Helper() 的逻辑。引用赋值将 helper 变量指向这块分配的内存地址。在没有 volatile 修饰时JVM 和 CPU 为了优化性能可能会进行指令重排序。合法的执行顺序可能是正常顺序1 - 2 - 3重排序后1 - 3 - 2 先分配内存立刻把地址赋给 helper 变量最后再执行构造函数初始化发生的灾难场景如果没有 volatile假设线程 A 正在初始化 helper发生了重排序1 - 3 - 2线程 A 分配了内存。线程 A 将helper 指向了内存地址此时 helper ! null。但是线程 A 还没来得及执行第 3 步构造函数初始化对象属性。此时线程 B 调用了 getHelper()。线程 B 执行第一次判断 if (helper null)。由于步骤 2 已经执行helper 已经不是 null 了。线程 B 直接跳过 锁和第二次判断返回了这个 helper 对象。线程 B 开始使用这个对象但此时对象的属性还没有被初始化步骤 3 还没跑导致程序出现难以排查的脏数据错误或崩溃。volatile 的作用volatile 关键字有两个核心语义正好解决了这个问题禁止指令重排序volatile 会插入内存屏障Memory Barrier强制保证上述三个步骤按 1 - 2 - 3 的顺序执行。只有当对象完全初始化好后引用才会被赋值给 helper 变量。保证可见性当线程 A 修改了 helper 的值其他线程如线程 B能立即看到这个最新的值而不是从自己的工作缓存中读取旧值。问题答案为什么判断两次外层是为了高性能避免无谓加锁内层是为了线程安全防止并发重复创建。为什么用 volatile防止new Helper()过程中的指令重排序避免其他线程拿到一个未初始化完成的对象。一句话记忆双重检查是为了快且对volatile 是为了防止拿到半成品。17.【参考】volatile 解决多线程内存不可见问题。对于一写多读是可以解决变量同步问题但是如果多写同样无法解决线程安全问题。说明如果是 count操作使用如下类实现AtomicInteger count new AtomicInteger(); count.addAndGet(1);如果是 JDK8推荐使用 LongAdder 对象比 AtomicLong 性能更好减少乐观锁的重试次数。18.【参考】HashMap 在容量不够进行 resize 时由于高并发可能出现死链导致 CPU 飙升在开发过程中注意规避此风险。19.【参考**】ThreadLocal 对象使用 static 修饰ThreadLocal 无法解决共享对象的更新问题**。说明这个变量是针对一个线程内所有操作共享的所以设置为静态变量所有此类实例共享此静态变量也就是说在类第一次被使用时装载只分配一块存储空间所有此类的对象(只要是这个线程内定义的)都可以操控这个变量。