阿里巴巴Java开发手册:编程规约.并发处理

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

正例:自定义线程工厂,并且根据外部特征进行分组,比如,来自同一机房的调用,把机房编号赋值给whatFeaturOfGroup

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

说明:jstack(Stack Trace for Java)命令用于生成虚拟机当前时刻的线程快照。线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合.

生成线程快照的目的主要是定位线程长时间出现停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等都是导致线程长时间停顿的原因。

指定线程名称后,可以在线程快照里看到是哪些线程受到阻碍,及时定位到出现问题的代码位置,进行错误修复。

3.【强制】线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。

说明:线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决资源不足的问题。如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题 。

《Java并发编程的艺术》中提到,使用线程池可以:降低资源消耗;提高响应速度;提高线程的可管理性.

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

说明:Executors返回的线程池对象的弊端如下:

1) FixedThreadPool和SingleThreadPool: 允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM。

2) CachedThreadPool: 允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM。

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

正例:注意线程安全,使用DateUtils。亦推荐如下使用ThreadLocal,确保每个线程都可以得到单独的一个 SimpleDateFormat 的对象,自然也就不存在竞争问题了:

扫描二维码关注公众号,回复: 11550188 查看本文章
  1. privatestaticfinalThreadLocal<DateFormat> df =newThreadLocal<DateFormat>(){
  2. @Override
  3. protectedDateFormat initialValue(){
  4. returnnewSimpleDateFormat("yyyy-MM-dd");
  5. }
  6. };

说明:如果是JDK8的应用,可以使用Instant代替Date,LocalDateTime代替Calendar,DateTimeFormatter代替SimpleDateFormat,官方给出的解释:simple beautiful strong immutable thread-safe。

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

正例:

  1. objectThreadLocal.set(userInfo);
  2. try{
  3. // ...
  4. }finally{
  5. objectThreadLocal.remove();
  6. }

说明:ThreadLocalMap中使用的key为ThreadLocal的弱引用,而 value是强引用。如果ThreadLocal没有被外部强引用,在垃圾回收的时候,key会被清理掉,而value不会被清理掉。这样一来,ThreadLocalMap中就会出现key为null的Entry。

假如我们不做任何措施的话,value 永远无法被GC回收,这个时候就可能会产生内存泄露。ThreadLocalMap实现中已经考虑了这种情况,在调用set()、get()、remove() 方法的时候,会清理掉key为null的记录。使用完ThreadLocal方法后 最好手动调用remove()方法。

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

说明:线程一需要对表A、B、C依次全部加锁后才可以进行更新操作,那么线程二的加锁顺序也必须是A、B、C,否则可能出现死锁。

死锁产生的四个条件:

互斥条件:该资源任意一个时刻只由一个线程占用。该条件无法破坏,因为我们就是需要产生互斥。

请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。通过一次性申请所有资源来破坏。

不剥夺条件:线程已获得的资源在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。通过线程申请资源失败后主动释放资源来破坏。

循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。通过按序申请资源来破坏。按某一顺序申请资源,释放资源则反序释放。

该要求就是通过破坏循环等待条件来预防死锁产生。

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

说明一:如果在lock方法与try代码块之间的方法调用抛出异常,那么无法解锁,造成其它线程无法成功获取锁。

说明二:如果lock方法在try代码块之内,可能由于其它方法抛出异常,导致在finally代码块中,unlock对未加锁的对象解锁,它会调用AQS的tryRelease方法(取决于具体实现类),抛出IllegalMonitorStateException异常。

说明三:在Lock对象的lock方法实现中可能抛出unchecked异常,产生的后果与说明二相同。

正例:

  1. Locklock=newXxxLock();
  2. // ...
  3. lock.lock();
  4. try{
  5. doSomething();
  6. doOthers();
  7. }finally{
  8. lock.unlock();
  9. }

反例:

  1. Locklock=newXxxLock();
  2. // ...
  3. try{
  4. // 如果此处抛出异常,则直接执行finally代码块
  5. doSomething();
  6. // 无论加锁是否成功,finally代码块都会执行
  7. lock.lock();
  8. doOthers();
  9. }finally{
  10. lock.unlock();
  11. }

10.【强制】在使用尝试机制来获取锁的方式中,进入业务代码块之前,必须先判断当前线程是否持有锁。锁的释放规则与锁的阻塞等待方式相同。

说明:Lock对象的unlock方法在执行时,它会调用AQS的tryRelease方法(取决于具体实现类),如果当前线程不持有锁,则抛出IllegalMonitorStateException异常。

正例:

  1. Locklock=newXxxLock();
  2. // ...
  3. boolean isLocked =lock.tryLock();
  4. if(isLocked){
  5. try{
  6. doSomething();
  7. doOthers();
  8. }finally{
  9. lock.unlock();
  10. }
  11. }

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

说明:如果每次访问冲突概率小于20%,推荐使用乐观锁,否则使用悲观锁。乐观锁的重试次数不得小于3次。乐观锁适合读多写少,冲突少的情况,悲观锁相反。

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

说明:ScheduledThreadPoolExecutor主要用来在给定的延迟后运行任务,或者定期执行任务。ScheduledThreadPoolExecutor使用的任务队列DelayQueue中封装了一个PriorityQueue,PriorityQueue会对队列中的任务进行排序。

执行所需时间短的放在前面先被执行(ScheduledFutureTask的time变量小的先执行),如果执行所需时间相同则先提交的任务将被先执行(ScheduledFutureTask的squenceNumber 变量小的先执行)。

ScheduledThreadPoolExecutor和Timer的比较:

1)Timer 对系统时钟的变化敏感,ScheduledThreadPoolExecutor不敏感;

2)Timer 只有一个执行线程,因此长时间运行的任务可以延迟其他任务。 ScheduledThreadPoolExecutor 可以配置任意数量的线程。

3)在TimerTask 中抛出的运行时异常会杀死一个线程,从而导致 Timer 死机,即计划任务将不再运行。ScheduledThreadExecutor 不仅捕获运行时异常,还允许在需要时处理它们(通过重写 afterExecute 方法ThreadPoolExecutor)。抛出异常的任务将被取消,但其他任务将继续运行。

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

说明:如果是count++操作,使用如下原子类实现:AtomicInteger count = new AtomicInteger(); count.addAndGet(1);

如果是JDK8,推荐使用LongAdder对象,比AtomicLong性能更好(减少乐观锁的重试次数)。

volatile只能保证变量的可见性,不能保证原子性。

18.【参考】HashMap在容量不够进行resize时由于高并发可能出现死链,导致CPU飙升,在开发过程中注意规避此风险。

说明:HashMap不是线程安全的,要保证线程安全可以使用ConcurrentHashMap。出现死链的原因主要是jdk1.7中HashMap扩容时使用头插法。

具体可见:https://blog.csdn.net/swpu_ocean/article/details/88917958

总结

这部分主要是高并发项目的一些要求和说明,涉及线程池,锁,AQS等,推荐学习《Java并发编程的艺术》和《Java并发编程实战》。

猜你喜欢

转载自blog.csdn.net/yiyihuazi/article/details/107854646