目录
强制
1.获取单例对象需要保证线程安全,其中的方法也要保证线程安全。
2.创建线程或线程池时指定有意义的线程名称,方便出错时回溯。
3.线程资源必须通过线程池提供,不允许在应用中自行显式地创建线程。线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决资源不足的问题。如果不使用线程池,有可能会造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。
4.线程池不允许使用Executors创建,而是通过ThreadPoolExecutor的方式,这样的处理方式能够让写的人更加明确线程池的运行规则,规避资源耗尽的风险。
Executors返回的线程池对象的弊端如下:
FixedThreadPool和SingleThreadPool;
允许的请求队列长度为Integer.MAX>VALUE,可能会堆积大量的请求,从而导致OOM。
CachedThreadPool:
允许的请求队列长度为Integer.MAX>VALUE,可能会创建大量的线程,从而导致OOM。5.SimpleDateFormat是线程不安全的类,一般不要定义为static变量,如果定义为static,必须加锁,或者使用DateUtils工具类。
6.必须回收子自定义的ThreadLoacal变量,尤其在线程池的场景下,线程经常会被复用,如果不清理自定义的ThreadLocal变量,可能会影响后续业务逻辑和造成内存泄漏等问题。尽量在代理中使用try-finally块进行回收。
7.高并发时,同步调用应该去考量锁的性能损耗。能用无锁数据结构,就不要用锁;能锁区块就不要锁整个方法体;能用对象锁就不要用类锁。
8.对多个资源、数据库表、对象同时加锁时,需要保持一致的加锁顺序,否则可能会造成死锁。
9.在使用阻塞等待获取锁的方式中,必须在try代码块之外,并且在加锁方法与try代码块之间没有任何可能抛出异常的方法调用,避免加锁成功后,在finally中无法解锁。
如果在lock方法与try代码块之间的方法调用抛出异常,那么无法解锁,造成其他线程无法成功获取锁。
如果lock方法在try代码块之内,可能会由于其他方法抛出异常,导致在finally代码块中,unlock对未加锁的对象解锁,会调用AQS的tryRelease方法,抛出IllegaMonitorStateException异常。
在Lock对象的lock方法实现中可能会抛出uncheckd异常,产生的后果与第二点相同。10.在使用尝试机制来获取锁的方式中,进入业务代码块之前,必须先判断当前线程是否持有锁。锁的释放规则与锁的阻塞等待方式相同。
11.并发修改同一记录时,避免更新丢失,需要加锁。要么在应用层加锁,要么在缓存加锁,要么在数据库层使用乐观锁,使用version作为更新数据。
12.多线程并行处理定时任务时,Timer运行多个TimeTask时,只要其中之一没有捕获抛出的异常,其他任务变回自动终止运行,如果在处理定时任务时使用ScheduledExecutorService则没有这个问题。
推荐
1.资金相关的金融敏感信息,使用悲观锁策略。乐观锁在获得锁的同时已经完成了更新操作,效验逻辑容易出现漏洞,另外,乐观锁对冲突的解决策略有比较复杂的要求,处理不当容易造成系统压力或数据异常,所以资金相关的金融敏感信息不建议使用乐观锁更新。
2.使用CountDownLatch进行异步转同步操作,每个线程退出前必须调用countDown方法,线程执行代码注意catch异常,确保countDown方法被执行到,避免主线程无法执行至await方法,直到超时才返回结果。
3.避免Random实例被多线程使用,虽然共享该实例是线程安全的,但会因为竞争统一seed导致性能下降。Random实例包括java.util.Random的实例或者Math.random()的方式。
4.在并发场景下,通过双重检查锁实现延迟初始化的优化问题隐患,可将目标属性声明为volatile型。
参考
1.volatile解决多线程内存不可见的问题。对于一写多读,可以解决变量同步问题,但如果是多写,也无法解决线程安全的问题。
2.HashMap在容器不够进行resize时由于高并发可能出现死链,导致CPU飙升,在开发过程中可以使用其他数据结构或者加锁来规避此风险。
3.ThreadLocal对象使用static修饰,ThreadLocal无法解决共享对象的更新问题。