1. 单例对象
获取单例对象需要保证线程安全,其中的方法也要保证线程安全
资源驱动类、工具类、单例工厂类都需要注意
2. 线程、线程池命名要有意义
创建线程或线程池时请指定有意义的线程名称,方便出错时回溯
正例
public class TimerTaskThread extends Thread {
public TimerTaskThread(){
super.setName("TimerTaskThread"); ...
}
}
3. 线程资源必须通过线程池提供
线程资源必须通过线程池提供,不允许在应用中自行显式创建线程
使用线程池的好处是减少在创建和销毁线程上所花的时间以及系统资源的开销,解决资源不足的问题。如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题
4. 线程池不允许使用Executors去创建
线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的构造方法方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险
说明:
- FixedThreadPool和SingleThread:允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量请求导致OOM
- CachedThreadPool和ScheduledThreadPool:允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM
使用ThreadPoolEecutor创建线程池的示例
public final class MyThreadPoolFactory {
private volatile static ExecutorService executor;
public static ExecutorService getExecutorService() {
if (executor == null) {
synchronized (MyThreadPoolFactory.class) {
if (executor == null) {
executor = new ThreadPoolExecutor(1, 5, 30000, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(3),
new MyThreadFactory("自定义线程"),
new ThreadPoolExecutor.AbortPolicy());
}
}
}
return executor;
}
private static class MyThreadFactory implements ThreadFactory {
/**
* 参数 代表是第1个ThreadPoolExecutor产生的
*/
private final AtomicInteger poolNumber = new AtomicInteger(1);
/**
* 线程组
*/
private final ThreadGroup threadGroup;
/**
* 代表该线程池创建的第1个线程
*/
private final AtomicInteger threadNumber = new AtomicInteger(1);
/**
* 线程名称前缀
*/
public final String namePrefix;
private MyThreadFactory(String name) {
SecurityManager s = System.getSecurityManager();
threadGroup = (s != null) ? s.getThreadGroup() :
Thread.currentThread().getThreadGroup();
if (null == name || "".equals(name.trim())) {
name = "pool";
}
namePrefix = name + "-" + poolNumber.getAndIncrement() + "-thread-";
}
@Override
public Thread newThread(@NotNull Runnable r) {
// 线程组,任务,线程名称, 创建该线程需要的堆栈大小,0代表忽略该参数
Thread thread = new Thread(threadGroup, r, namePrefix + threadNumber.getAndIncrement(), 0);
if (thread.isDaemon()) {
thread.setDaemon(false);
}
if (thread.getPriority() != Thread.NORM_PRIORITY) {
thread.setPriority(Thread.NORM_PRIORITY);
}
return thread;
}
}
public static void main(String[] args) {
ExecutorService executorService = MyThreadPoolFactory.getExecutorService();
for (int i = 0; i < 50; i++) {
executorService.execute(() -> System.out.println(Thread.currentThread().getName()));
}
}
}
测试结果
自定义线程-1-thread-1
自定义线程-1-thread-2
自定义线程-1-thread-2
自定义线程-1-thread-3
自定义线程-1-thread-1
自定义线程-1-thread-2
自定义线程-1-thread-4
自定义线程-1-thread-5
Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task com.maycur.test.MyThreadPoolFactory$$Lambda$1/824318946@246b179d rejected from java.util.concurrent.ThreadPoolExecutor@7a07c5b4[Running, pool size = 5, active threads = 2, queued tasks = 0, completed tasks = 6]
at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2063)
at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:830)
at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1379)
at com.maycur.test.MyThreadPoolFactory.main(MyThreadPoolFactory.java:74)
拓展:submit()和execute()的区别
submit()和execute()都属于线程池ThreadPoolExecutor的方法
区别:
execute所属顶层接口是Executor;submit所属顶层接口是ExecutorService
实现类ThreadPoolExecutor重写了execute方法,抽象类AbstractExecutorService重写了submit方法
execute只能提交Runnable类型的任务;而submit既能提交Runnable类型任务也能提交Callable类型任务
execute会直接抛出任务执行时的异常;submit会吃掉异常,可通过Future的get方法将任务执行时的异常重新抛出
public static void main(String[] args) { ExecutorService executorService = MyThreadPoolFactory.getExecutorService(); // for (int i = 0; i < 50; i++) { // executorService.execute(() -> System.out.println(Thread.currentThread().getName())); // } Future<?> f = executorService.submit(() -> 1 / 0); try { Object o = f.get(); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } }
备注:从上到下Executor->ExecutorService ->AbstractExecutorService ->ThreadPoolExecutor
submit和execute由于参数不同有四种实现形式
<T> Future<T> submit(Callable<T> task);
<T> Future<T> submit(Runnable task, T result);
Future<?> submit(Runnable task);
void execute(Runnable command);
5. SimpleDateFormat线程不安全
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代替Simpledateformatter,官方给出的解释:simple beautiful strong immutable thread-safe
6. 慎用加锁
高并发时,同步调用应该去考虑锁的性能损耗。
能用无锁的数据结构,就不要用锁;能锁区块,就不要锁整个方法体;能锁区块,就不要锁整个方法体
7. 加锁顺序保持一致
对多个资源、数据库表、对象同时加锁时,需要保持一致的加锁顺序,否则可能会造成死锁;
线程一需要对表A、B、C依次全部加锁后才可以进行更新操作,那么线程二的加锁顺序也必须是A、B、C,否则可能出现死锁。
8. 并发修改记录要加锁
为了避免更新丢失,要么在应用层加锁,要么在缓存加锁,要么在数据层用乐观锁(使用version)作为更新记录
如果每次访问冲突概率较低推荐使用乐观锁,否则使用悲观锁。乐观锁的重试次数不得小于3次
9. 慎用Timer定时任务类
多线程并行处理定时任务时,Timer运行多个TimeTask时,只要其中之一没有捕获抛出的异常,其它任务便会自动终止运行,使用ScheduledExecutorService则没有这个问题
10. 使用CountDownLatch的注意事项
使用CountDownLatch进行异步转同步操作,每个线程退出前必须调用countDown方法,线程执行代码注意catch异常,确保countDown方法可以执行,避免主线程无法执行至countDown方法,直到超时才返回结果
拓展:CountDownLatch用法
概念
CountDownLatch是一个同步工具类,能够起到线程通信的作用。可以使一个线程能够等待其他线程都完成各自的工作之后再继续执行,通过一个计数器来实现:1. 计数器初始值为等待的其他线程的数量; 2. 每当一个线程完成自己的任务之后,计数器数量-1; 3. 当计数器变为0后说明别的线程都完成了自己的任务,挂在CountDownLatch上等待的线程就可以继续向下执行了
重要方法
CountDownLatch(int ThreadCount); // 唯一构造方法,指定了需要等待多少个线程执行完毕
public void await() throws InterruptedException; // 当前线程等待,直到计数器值变为0
public boolean await(long timeout, TimeUnit unit) throws InterruptedException; //当前线程等待,直到计数器值变为0或者达到了超时时间
public void countDown(); // 计数器值-1
示例:百米赛跑,4名运动员选手到达场地等待裁判口令,裁判一声口令,选手听到后同时起跑,当所有选手到达终点,裁判进行汇总排名
package com.example.demo.CountDownLatchDemo;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class CountdownLatchTest2 {
public static void main(String[] args) {
ExecutorService service = Executors.newCachedThreadPool();
final CountDownLatch cdOrder = new CountDownLatch(1);
final CountDownLatch cdAnswer = new CountDownLatch(4);
for (int i = 0; i < 4; i++) {
Runnable runnable = new Runnable() {
@Override
public void run() {
try {
System.out.println("选手" + Thread.currentThread().getName() + "正在等待裁判发布口令");
cdOrder.await();
System.out.println("选手" + Thread.currentThread().getName() + "已接受裁判口令");
Thread.sleep((long) (Math.random() * 10000));
System.out.println("选手" + Thread.currentThread().getName() + "到达终点");
cdAnswer.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
service.execute(runnable);
}
try {
Thread.sleep((long) (Math.random() * 10000));
System.out.println("裁判"+Thread.currentThread().getName()+"即将发布口令");
cdOrder.countDown();
System.out.println("裁判"+Thread.currentThread().getName()+"已发送口令,正在等待所有选手到达终点");
cdAnswer.await();
System.out.println("所有选手都到达终点");
System.out.println("裁判"+Thread.currentThread().getName()+"汇总成绩排名");
} catch (InterruptedException e) {
e.printStackTrace();
}
service.shutdown();
}
}
11. 使用双重检查锁懒加载要加volatile关键字
通过双重检查锁(double-checked locking)(在并发场景)实现延迟初始化的优化问题隐患(可参考 The “Double-Checked Locking is Broken” Declaration),推荐问题解决方案为:将目标属性声明为 volatile型
executor = new ThreadPoolExecutor这一步实际上分为两步执行的:在内存中开辟一块内存空间将地址值给executor;在该地址中进行初始化。在高并发的情况下,可能第二步还没有执行,但executor直接被拿去使用,此时会出问题
public final class MyThreadPoolFactory {
private volatile static ExecutorService executor;
public static ExecutorService getExecutorService() {
if (executor == null) {
synchronized (MyThreadPoolFactory.class) {
if (executor == null) {
executor = new ThreadPoolExecutor(1, 5, 30000, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(3),
new MyThreadFactory("自定义线程"),
new ThreadPoolExecutor.AbortPolicy());
}
}
}
return executor;
}
}
12. volatile解决多线程内存不可见问题
对于一写多读,是可以解决变量同步问题,但是如果多写,同样无法解决线程安全问题
- 如果是count++操作,使用如下类实现:
AtomicInteger count = new AtomicInteger(); count.addAndGet(1);
- 如果是JDK8,推荐使用LongAdder对象,比AtomicLong性能更好(减少乐观锁的重试次数)
13. HashMap在容量不够进行resize时由于高并发可能出现死链
HashMap在容量不够进行resize时由于高并发可能出现死链,导致CPU飙升,在开发过程中注意规避此风险