正例:自定义线程工厂,并且根据外部特征进行分组,比如,来自同一机房的调用,把机房编号赋值给whatFeaturOfGroup
public class UserThreadFactory implements ThreadFactory {
private final String namePrefix;
private final AtomicInteger nextId = new AtomicInteger(1);
// 定义线程组名称,在jstack问题排查时,非常有帮助
UserThreadFactory(String whatFeaturOfGroup) {
namePrefix = "From UserThreadFactory's " + whatFeaturOfGroup + "-Worker-";
}
@Override
public Thread newThread(Runnable task) {
String name = namePrefix + nextId.getAndIncrement();
Thread thread = new Thread(null, task, name, 0, false);
System.out.println(thread.getName());
return thread;
}
}
说明:jstack(Stack Trace for Java)命令用于生成虚拟机当前时刻的线程快照。线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合.
生成线程快照的目的主要是定位线程长时间出现停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等都是导致线程长时间停顿的原因。
指定线程名称后,可以在线程快照里看到是哪些线程受到阻碍,及时定位到出现问题的代码位置,进行错误修复。
说明:线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决资源不足的问题。如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题
。
《Java并发编程的艺术》中提到,使用线程池可以:降低资源消耗;提高响应速度;提高线程的可管理性.
说明:Executors返回的线程池对象的弊端如下:
1) FixedThreadPool和SingleThreadPool:
允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM。
2) CachedThreadPool: 允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM。
正例:注意线程安全,使用DateUtils。亦推荐如下使用ThreadLocal,确保每个线程都可以得到单独的一个 SimpleDateFormat 的对象,自然也就不存在竞争问题了:
private static final ThreadLocal df = new ThreadLocal() {
@Override
protected DateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd");
}
};
说明:如果是JDK8的应用,可以使用Instant代替Date,LocalDateTime代替Calendar,DateTimeFormatter代替SimpleDateFormat,官方给出的解释:simple beautiful strong immutable thread-safe。
正例:
objectThreadLocal.set(userInfo);
try {
// ...
} finally {
objectThreadLocal.remove();
}
说明:ThreadLocalMap中使用的key为ThreadLocal的弱引用,而 value是强引用。如果ThreadLocal没有被外部强引用,在垃圾回收的时候,key会被清理掉,而value不会被清理掉。这样一来,ThreadLocalMap中就会出现key为null的Entry。
假如我们不做任何措施的话,value 永远无法被GC回收,这个时候就可能会产生内存泄露。ThreadLocalMap实现中已经考虑了这种情况,在调用set()、get()、remove() 方法的时候,会清理掉key为null的记录。使用完ThreadLocal方法后 最好手动调用remove()方法。
说明:线程一需要对表A、B、C依次全部加锁后才可以进行更新操作,那么线程二的加锁顺序也必须是A、B、C,否则可能出现死锁。
死锁产生的四个条件:
互斥条件:该资源任意一个时刻只由一个线程占用。该条件无法破坏,因为我们就是需要产生互斥。
请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。通过一次性申请所有资源来破坏。
不剥夺条件:线程已获得的资源在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。通过线程申请资源失败后主动释放资源来破坏。
循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。通过按序申请资源来破坏。按某一顺序申请资源,释放资源则反序释放。
该要求就是通过破坏循环等待条件来预防死锁产生。
说明一:如果在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();
}
说明:Lock对象的unlock方法在执行时,它会调用AQS的tryRelease方法(取决于具体实现类),如果当前线程不持有锁,则抛出IllegalMonitorStateException异常。
正例:
Lock lock = new XxxLock();
// ...
boolean isLocked = lock.tryLock();
if (isLocked) {
try {
doSomething();
doOthers();
} finally {
lock.unlock();
}
}
说明:如果每次访问冲突概率小于20%,推荐使用乐观锁,否则使用悲观锁。乐观锁的重试次数不得小于3次。乐观锁适合读多写少,冲突少的情况,悲观锁相反。
说明:ScheduledThreadPoolExecutor主要用来在给定的延迟后运行任务,或者定期执行任务。ScheduledThreadPoolExecutor使用的任务队列DelayQueue中封装了一个PriorityQueue,PriorityQueue会对队列中的任务进行排序。
执行所需时间短的放在前面先被执行(ScheduledFutureTask的time变量小的先执行),如果执行所需时间相同则先提交的任务将被先执行(ScheduledFutureTask的squenceNumber 变量小的先执行)。
ScheduledThreadPoolExecutor和Timer的比较:
1)Timer 对系统时钟的变化敏感,ScheduledThreadPoolExecutor不敏感;
2)Timer 只有一个执行线程,因此长时间运行的任务可以延迟其他任务。 ScheduledThreadPoolExecutor 可以配置任意数量的线程。
3)在TimerTask 中抛出的运行时异常会杀死一个线程,从而导致 Timer 死机,即计划任务将不再运行。ScheduledThreadExecutor 不仅捕获运行时异常,还允许在需要时处理它们(通过重写 afterExecute 方法ThreadPoolExecutor)。抛出异常的任务将被取消,但其他任务将继续运行。
说明:如果是count++操作,使用如下原子类实现:AtomicInteger count = new AtomicInteger(); count.addAndGet(1);
如果是JDK8,推荐使用LongAdder对象,比AtomicLong性能更好(减少乐观锁的重试次数)。
volatile只能保证变量的可见性,不能保证原子性。
说明:HashMap不是线程安全的,要保证线程安全可以使用ConcurrentHashMap。出现死链的原因主要是jdk1.7中HashMap扩容时使用头插法。
具体可见:https://blog.csdn.net/swpu_ocean/article/details/88917958
这部分主要是高并发项目的一些要求和说明,涉及线程池,锁,AQS等,推荐学习《Java并发编程的艺术》和《Java并发编程实战》。