image.png

2. 【强制】创建线程或线程池时请指定有意义的线程名称,方便出错时回溯。

正例:自定义线程工厂,并且根据外部特征进行分组,比如机房信息。

  1. public class UserThreadFactory implements ThreadFactory {
  2. private final String namePrefix;
  3. private final AtomicInteger nextId = new AtomicInteger(1);
  4. / 定义线程组名称,在 jstack 问题排查时,非常有帮助
  5. UserThreadFactory(String whatFeaturOfGroup) {
  6. namePrefix = "From UserThreadFactory's " + whatFeaturOfGroup + "-Worker-";
  7. }
  8. @Override
  9. public Thread newThread(Runnable task) {
  10. String name = namePrefix + nextId.getAndIncrement(); Thread thread = new Thread(null, task, name, 0, false);
  11. System.out.println(thread.getName());
  12. return thread;
  13. }
  14. }

image.png

4.【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

说明:Executors 返回的线程池对象的弊端如下:
1) FixedThreadPool 和SingleThreadPool:
允许的请求队列长度为 Integer.MAXVALUE,可能会堆积大量的请求,从而导致 OOM。
2) CachedThreadPool和_newScheduledThreadPool

允许的创建线程数量为 Integer.MAXVALUE,可能会创建大量的线程,从而导致 OOM。
解释:看代码。thread包下ExecutorsTest。点进去:newFixedThreadPool,发现其实Executor都是基于ThreadPoolExecutor创建的,只不过传入不同的参数,
参数含义如下:
corePoolSize:线程池中核心线程数量。
maximumPoolSize:线程池最大线程数量。
keepAliveTime:超出核心线程数量时,多出的空闲线程的存活时间。
unit: keepAliveTime的单位。
workQueue:任务队列,当没有空闲线程时,任务存储在该队列。
threadFactory:线程工厂,被用于创建线程。一般使用Executors.defaultThreadFactory()即可。若想清楚的了解自己任务的线程,可以利用该工厂定制化线程。
handler:拒绝策略,当任务太多,如何拒绝任务。
其他参数容易理解:重点:workQueue和handler。
有界队列:ArrayBlockingQueue
创建时必须指定容量。当有新任务时,如果即将使用线程数量 < corePoolSize,则会创建新的线程执行,如果 > corePoolSize,则会将新的任务加入到任务队列中。当任务队列已满,如果线程数量无界队列:LinkedBlockingQueue
该队列没有界限,除非是系统资源耗尽,否则它不会出现入队失败情况。当新的任务提交,如果使用线程数量直接提交队列:SynchronousQueue
该队列没有容量,也就是说它不存储任务。当有新的任务来时,如果没有空闲的线程,则会创建新的线程,如果线程数量已经达到最大值,就会执行拒绝策略。newCachedThreadPool()就是使用该队列,使用该队列时,应该把最大线程数量设置尽量大,否则很容易就会执行拒绝策略,newCachedThreadPool()就设置的为Integer.MAX_VALUE。
优先任务队列:PriorityBlockingQueue
控制任务的执行顺序,它属于无界队列,但是比较特殊。不论是无界还是有界队列都是按照先进先出的顺序处理任务,而PriorityBlockingQueue则可以根据任务自身的优先级顺序执行,它总能确保高优先级的任务先执行。
拒绝策略:
AbortPolicy:直接抛出异常,阻止应用正常工作。(默认使用该策略)
CallerRunsPolicy:不会真正丢弃任务,当线程池未关闭,该策略直接在调用者线程中运行当前丢弃任务。
DiscardPolicy:丢弃无法处理的任务,不做任何处理。
DiscardOldestPolicy:丢弃最老的任务,也就是即将执行的任务,并尝试提交当前任务。
也可以自定义拒绝策略。
newFixedThreadPool和newSingleThreadExecutor点参数队列进去:
public LinkedBlockingQueue() {
this(Integer.**_MAX_VALUE
);
}
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.
SECONDS,
new** SynchronousQueue());
}

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

正例:注意线程安全,使用 DateUtils。亦推荐如下处理:

  1. private static final ThreadLocal<DateFormat> df = new ThreadLocal<DateFormat>() {
  2. @Override
  3. protected DateFormat initialValue() {
  4. return new SimpleDateFormat("yyyy-MM-dd");
  5. }
  6. };

说明:如果是 JDK8 的应用,可以使用 Instant 代替 Date,LocalDateTime 代替 Calendar, DateTimeFormatter 代替 SimpleDateFormat,官方给出的解释:simple beautiful strong immutable thread-safe。
解释:看代码:SimpleDateFormatTest
代码中有两种方式,不安全和安全。
ThreadLocal 是线程的局部变量, 是每一个线程所单独持有的,其他线程不能对其进行访问。
当使用ThreadLocal维护变量的时候 为每一个使用该变量的线程提供一个独立的变量副本,即每个线程内部都会有一个该变量,这样同时多个线程访问该变量并不会彼此相互影响,因此他们使用的都是自己从内存中拷贝过来的变量的副本, 这样就不存在线程安全问题,也不会影响程序的执行性能。
但是要注意,虽然ThreadLocal能够解决上面说的问题,但是由于在每个线程中都创建了副本,所以要考虑它对资源的消耗,比如内存的占用会比不使用ThreadLocal要大。

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

尽量在代理中使用 try-finally 块进行回收。
正例:
objectThreadLocal.set(userInfo);
try {
// …
} finally {
objectThreadLocal.remove();
}
解释:simpleDateFormatSalfe.remove();

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

说明:尽可能使加锁的代码块工作量尽可能的小,避免在锁代码块中调用 RPC 方法。
解释:调用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();
}

解释:保持锁的完整性。网约车中会讲到这部分,有分布锁讲解。比如锁订单号出现了异常,下个线程进来正常执行完,释放锁,结果把上次的订单锁释放掉。

  1. 【强制】在使用尝试机制来获取锁的方式中,进入业务代码块之前,必须先判断当前线程是否持有锁。锁的释放规则与锁的阻塞等待方式相同。
    说明: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 次。
    解释:概率计算,看资源和访问资源的情况。
    重试:
    11. 【强制】多线程并行处理定时任务时,Timer 运行多个 TimeTask 时,只要其中之一没有捕获抛出的异常,其它任务便会自动终止运行,如果在处理定时任务时使用 ScheduledExecutorService 则没有这个问题。
    解释:在我们讲eureka源码时可以看到这种用法。com.netflix.discovery.DiscoveryClient 1267行
    private void initScheduledTasks() {
    点scheduler,private final ScheduledExecutorService scheduler;
    13. 【推荐】资金相关的金融敏感信息,使用悲观锁策略。
    说明:乐观锁在获得锁的同时已经完成了更新操作,校验逻辑容易出现漏洞,另外,乐观锁对冲突的解决策略有较复杂的要求,处理不当容易造成系统压力或数据异常,所以资金相关的金融敏感信息不建议使用乐观锁更新。
    解释:https://blog.csdn.net/qq_34337272/article/details/81072874
    14. 【推荐】使用 CountDownLatch 进行异步转同步操作,每个线程退出前必须调用 countDown 方法,线程执行代码注意 catch 异常,确保 countDown 方法被执行到,避免主线程无法执行至 await 方法,直到超时才返回结果。
    说明:注意,子线程抛出异常堆栈,不能在主线程 try-catch 到。
    解释:看代码CountDownLatchTest
    15. 【推荐】避免 Random 实例被多线程使用,虽然共享该实例是线程安全的,但会因竞争同一 seed 导致的性能下降。
    说明:Random 实例包括 java.util.Random 的实例或者 Math.random()的方式。
    正例:在 JDK7 之后,可以直接使用 API ThreadLocalRandom,而在 JDK7 之前,需要编码保证每个线程持有一个实例。
    解释:JDK7之后,可以使用ThreadLocalRandom来获取随机数。
    public Random(long seed) {
    if (getClass() == Random.class)
    this.seed = new AtomicLong(initialScramble(seed));
    else {
    // subclass might have overriden setSeed
    this.seed = new AtomicLong();
    setSeed(seed);
    }
    }
    private static long initialScramble(long seed) {
    return (seed ^ multiplier) & mask;
    }
    multiplier & mask都是static的,所以共享。
    seed含义见代码:RandomTest,seed一样,一组产生的随机数一样。
    16. 【推荐】在并发场景下,通过双重检查锁(double-checked locking)实现延迟初始化的优化问题隐患(可参考 The “Double-Checked Locking is Broken” Declaration),推荐解决方案中较为简单一种(适用于 JDK5 及以上版本),将目标属性声明为 volatile 型。
    反例:
    public class LazyInitDemo {
    private Helper helper = null;
    public Helper getHelper() {
    if (helper == null) synchronized(this) {
    if (helper == null)
    helper = new Helper();
    }
    return helper;
    }
    // other methods and fields…
    }
    解释:1.线程的可见性
    2.防止指令重排
    感兴趣 Java基础课。
    17. 【参考】volatile 解决多线程内存不可见问题。对于一写多读,是可以解决变量同步问题,但是如果多写,同样无法解决线程安全问题。
    说明:如果是 count++操作,使用如下类实现:AtomicInteger count = new AtomicInteger(); count.addAndGet(1); 如果是 JDK8,推荐使用 LongAdder 对象,比 AtomicLong 性能更好(减少乐观锁的重试次数)。
    18. 【参考】HashMap 在容量不够进行 resize 时由于高并发可能出现死链,导致 CPU 飙升,在开发过程中可以使用其它数据结构或加锁来规避此风险。
    19. 【参考】ThreadLocal 对象使用 static 修饰,ThreadLocal 无法解决共享对象的更新问题。
    说明:这个变量是针对一个线程内所有操作共享的,所以设置为静态变量,所有此类实例共享此静态变量,也就是说在类第一次被使用时装载,只分配一块存储空间,所有此类的对象(只要是这个线程内定义的)都可以操控这个变量。