并发编程的常见约束

Posted 淼淼之森

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了并发编程的常见约束相关的知识,希望对你有一定的参考价值。

并发处理 :
1. 【强制】获取单例对象需要保证线程安全,其中的方法也要保证线程安全。
  说明:资源驱动类、工具类、单例工厂类都需要注意。
2. 【强制】创建线程或线程池时请指定有意义的线程名称,方便出错时回溯。
  正例:
    public class TimerTaskThread extends Thread { 
    public TimerTaskThread() { 
      super.setName("TimerTaskThread");
       ...
    } 
3. 【强制】线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。
  说明:使用线程池的好处是减少在创建和销毁线程上所花的时间以及系统资源的开销,解决资源不足的问题。如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。
ExecutorService exec = Executors.newCachedThreadPool();  
exec.execute(new MyThread());     
4. 【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
  说明:Executors 返回的线程池对象的弊端如下:
    1)FixedThreadPool 和 SingleThreadPool:
      允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。
    2)CachedThreadPool 和 ScheduledThreadPool:
      允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。
//SingleThreadPool
new
ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>())
//CachedThreadPool 
new
ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>())

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

  正例:注意线程安全,使用 DateUtils。亦推荐如下处理:
    private static final ThreadLocal<DateFormat> df = new ThreadLocal<DateFormat>() { 
      @Override 
      protected DateFormat initialValue() { 
        return new SimpleDateFormat("yyyy-MM-dd"); 
      } 
    }; 
说明:如果是 JDK8 的应用,可以使用 Instant 代替 Date,LocalDateTime 代替 Calendar,DateTimeFormatter 代替 SimpleDateFormat,官方给出的解释:simple beautiful strong immutable thread-safe。
 6. 【强制】高并发时,同步调用应该去考量锁的性能损耗。能用无锁数据结构,就不要用锁;能锁区块,就不要锁整个方法体;能用对象锁,就不要用类锁。
  说明:尽可能使加锁的代码块工作量尽可能的小,避免在锁代码块中调用 RPC 方法。
7. 【强制】对多个资源、数据库表、对象同时加锁时,需要保持一致的加锁顺序,否则可能会造成死锁。
说明:线程一需要对表 A、B、C 依次全部加锁后才可以进行更新操作,那么线程二的加锁顺序也必须是 A、B、C,否则可能出现死锁。三线程打印ABC的问题
8. 【强制】并发修改同一记录时,避免更新丢失,需要加锁。要么在应用层加锁,要么在缓存加锁,要么在数据库层使用乐观锁,使用 version 作为更新依据。
  说明:如果每次访问冲突概率小于 20%,推荐使用乐观锁,否则使用悲观锁。乐观锁的重试次数不得小于 3 次。
9. 【强制】多线程并行处理定时任务时,Timer 运行多个 TimeTask 时,只要其中之一没有捕获抛出的异常,其它任务便会自动终止运行,使用 ScheduledExecutorService 则没有这个问题。
10. 【推荐】使用 CountDownLatch 进行异步转同步操作,每个线程退出前必须调用 countDown方法,线程执行代码注意 catch 异常,确保 countDown 方法被执行到,避免主线程无法执行至 await 方法,直到超时才返回结果。
  说明:注意,子线程抛出异常堆栈,不能在主线程 try-catch 到。
11. 【推荐】避免 Random 实例被多线程使用,虽然共享该实例是线程安全的,但会因竞争同一seed 导致的性能下降。
  说明:Random 实例包括 java.util.Random 的实例或者 Math.random()的方式。
  正例:在 JDK7 之后,可以直接使用 API ThreadLocalRandom,而在 JDK7 之前,需要编码保证每个线程持有一个实例
 1 package com.mmz.OtherTest;
 2 
 3 import java.util.Random;
 4 import java.util.concurrent.ThreadLocalRandom;
 5 
 6 /*
 7  * 
 8  */
 9 public class test4 implements Runnable {     
10         
11     private String name;     
12     //相当于是在池中创建的ThreadLocalRandom对象,只是第一次创建
13     //ThreadLocalRandom random = ThreadLocalRandom.current();//放到此处,和放到24行效果一样,每次random都一样,
14    
15     //使用Random则每次都会产生新的对象,而使用jdk7版本中的ThreadLocalRandom.current()则只创建一次对象
16     //Random random = new Random();//放到此处,和放到28行效果一样,每次random不一样
17     
18     private test4(String name) {     
19         this.name = name;     
20     }     
21     
22     @Override    
23     public void run() { 
24 //        ThreadLocalRandom random = ThreadLocalRandom.current();//放到此处,和放到12行效果一样,每次random都一样
25 //        System.out.println(random);
26 //      int count = random.nextInt(10,50);  
27         
28         Random random = new Random();//使用Random则每次都会产生新的对象,而使用jdk7版本中的ThreadLocalRandom.current()则只创建一次对象
29         System.out.println(random);
30 //        int count = random.nextInt(10);
31         
32         //使用下面的方式速度比前两种都慢,所以一般不使用;通过sleep(1000)都不能保证其执行顺序,当调整睡眠时间更长时才能保证,可以得出该结论。
33         int count = (int) (Math.random()*10);
34         System.out.println(count);//生成一个10~50之间的随机数
35         while (count > 0) {     
36             System.out.print(name);
37             try {
38                 Thread.sleep(1000);
39             } catch (InterruptedException e) {
40                 e.printStackTrace();
41             }
42             count--;    
43         }     
44         System.out.println();
45         }     
46     
47     
48     
49     public static void main(String[] args) throws Exception {     
50         Object a = new Object();     
51         Object b = new Object();     
52         Object c = new Object();     
53         test4 pa = new test4("A");     
54         test4 pb = new test4("B");     
55         test4 pc = new test4("C");     
56         
57         new Thread(pa).start();  
58         Thread.sleep(1000);  //确保按顺序A、B、C执行  
59         new Thread(pb).start();  
60         Thread.sleep(1000);   
61         new Thread(pc).start();     
62         Thread.sleep(1000);    
63         }     
64 }    
test4
12. 【推荐】在并发场景下,通过双重检查锁(double-checked locking)实现延迟初始化的优化问题隐患(可参考 The "Double-Checked Locking is Broken" Declaration),推荐解决方案中较为简单一种(适用于 JDK5 及以上版本),将目标属性声明为 volatile 型。
  反例:
  class Singleton { 
    private Helper helper = null; 
    public Helper getHelper() { 
      if (helper == null) 
        synchronized(this) { 
          if (helper == null) 
            helper = new Helper(); 
        } 
      return helper; 
    } 
    // other methods and fields... 
  } 
13. 【参考】volatile 解决多线程内存不可见问题。
  对于一写多读,是可以解决变量同步问题,但是如果多写,同样无法解决线程安全问题。如果是 count++操作,使用如下类实现:
    AtomicInteger count = new AtomicInteger(); 
    count.addAndGet(1); 
  如果是 JDK8,推荐使用 LongAdder 对象,比 AtomicLong 性能更好(减少乐观锁的重试次数)。
14. 【参考】 HashMap 在容量不够进行 resize 时由于高并发可能出现死链,导致 CPU 飙升,在开发过程中可以使用其它数据结构或加锁来规避此风险。
15. 【参考】ThreadLocal 无法解决共享对象的更新问题,ThreadLocal 对象建议使用 static修饰。这个变量是针对一个线程内所有操作共享的,所以设置为静态变量,所有此类实例共享此静态变量 ,也就是说在类第一次被使用时装载,只分配一块存储空间,所有此类的对象(只
要是这个线程内定义的)都可以操控这个变量。

 如需了解更多,请看《阿里巴巴Java开发手册》;也可以加QQ群了解更多:603654340

以上是关于并发编程的常见约束的主要内容,如果未能解决你的问题,请参考以下文章

Day624.并发编程代码加锁注意点 -Java业务开发常见错误

Java并发编程——常见的线程安全问题

JUC并发编程 共享模式之工具 JUC CountdownLatch(倒计时锁) -- CountdownLatch应用(等待多个线程准备完毕( 可以覆盖上次的打印内)等待多个远程调用结束)(代码片段

golang代码片段(摘抄)

Go十大常见错误第8篇:并发编程中Context使用常见错误

Java 并发编程 常见面试总结