先看一下线程池里面很重要的两个成员变量
/** * The queue used for holding tasks and handing off to worker * threads. */ private final BlockingQueue<Runnable> workQueue; /** * Set containing all worker threads in pool. Accessed only when * holding mainLock. */ private final HashSet<Worker> workers = new HashSet<Worker>(); 复制代码
这里的Worker即持有具体thread的工作线程,workQueue(阻塞队列)则是我们传入的一个个需要被执行的任务(run方法里面的内容)。其他的结构性的说明可以瞅瞅这篇美团技术团队的文章,写的特别好: https://tech.meituan.com/2020/04/02/java-pooling-pratice-in-meituan.html
来看看线程池的runWorker方法
工作线程在执行完成一个任务以后,就会去阻塞队列获取新的任务,执行任务的run方法。
有一个挺有意思的地方:线程池在新任务进来时候,如果核心线程数没有满,则会去再开一个线程,而不是复用已存在的空闲的核心线程,因为runWork的方法,在getTask的地方会阻塞,但是他是阻塞在了获取队列中的task。
上面说到,getTask会阻塞线程。这和回收保留有什么关系呢?我们来看看这段代码
可以很清晰的看到:核心线程永久阻塞,非核心线程则计时阻塞
我们先看看它的存在方式,在Thread类里面有一个成员变量:
/* ThreadLocal values pertaining to this thread. This map is maintained * by the ThreadLocal class. */ ThreadLocal.ThreadLocalMap threadLocals = null; 复制代码
它被一个线程所携带,存放在一个map里面,这个map的key为ThreadLocal对象,value为你需要设置的值。
当我们调用ThreadLocal的set方法时,其实是把当前的threadlocal作为key,加上你的value,放入了当前线程的那个Map里面。
我们来一步步看一下如下场景(模拟大量请求得到服务的情况,在这条请求链路中,我们都需要使用相同的格式进行打印时间:从0-999):
public class ThreadLocalNormalUsage { public static ExecutorService threadPool = Executors.newFixedThreadPool(10); public static void main(String[] args) { for (int i = 0; i < 1000; i++) { int finalI = i; threadPool.submit(() -> { String date = new ThreadLocalNormalUsage().date(finalI); System.out.println(date); // 更多的service层任务 }); } threadPool.shutdown(); } public String date(int seconds) { //参数的单位是毫秒,从1970.1.1 00:00:00 GMT计时 Date date = new Date(1000 * seconds); SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); return dateFormat.format(date); } } 复制代码
public class ThreadLocalNormalUsage { public static ExecutorService threadPool = Executors.newFixedThreadPool(10); static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); public static void main(String[] args) { for (int i = 0; i < 1000; i++) { int finalI = i; threadPool.submit(() -> { String date = new ThreadLocalNormalUsage().date(finalI); System.out.println(date); // 更多的service层任务 }); } threadPool.shutdown(); } public String date(int seconds) { //参数的单位是毫秒,从1970.1.1 00:00:00 GMT计时 Date date = new Date(1000 * seconds); return dateFormat.format(date); } } 复制代码
现在问题就来了,居然出现了相同的时间打印,显然这是不应该的呀。
因为SimpleDateFormat在多线程访问下就会出现问题,因为他本身并不是线程安全的类。
public class ThreadLocalNormalUsage { public static ExecutorService threadPool = Executors.newFixedThreadPool(10); static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); public static void main(String[] args) { for (int i = 0; i < 1000; i++) { int finalI = i; threadPool.submit(() -> { String date = new ThreadLocalNormalUsage().date(finalI); System.out.println(date); // 更多的service层任务 }); } threadPool.shutdown(); } public String date(int seconds) { //参数的单位是毫秒,从1970.1.1 00:00:00 GMT计时 Date date = new Date(1000 * seconds); String s; synchronized (ThreadLocalNormalUsage.class) { s = dateFormat.format(date); } return s; } } 复制代码
没错,这次结果正常了。
但是这样不行呀,使用同步保护后,所有并发的线程都在这排队,性能损耗岂不是很严重,这还得了。
public class ThreadLocalNormalUsage { public static ExecutorService threadPool = Executors.newFixedThreadPool(10); public static void main(String[] args) { for (int i = 0; i < 1000; i++) { int finalI = i; threadPool.submit(() -> { String date = new ThreadLocalNormalUsage().date(finalI); System.out.println(date); // 更多的service层任务 }); } threadPool.shutdown(); } public String date(int seconds) { //参数的单位是毫秒,从1970.1.1 00:00:00 GMT计时 Date date = new Date(1000 * seconds); SimpleDateFormat dateFormat = ThreadSafeFormatter.dateFormatThreadLocal2.get(); return dateFormat.format(date); } } /** * 两种效果一样的写法,都是重写initialValue方法 * initialValue方法会延迟加载,在使用get方法时候才会触发 */ class ThreadSafeFormatter { public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>() { @Override protected SimpleDateFormat initialValue() { return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); } }; public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal2 = ThreadLocal .withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")); } 复制代码
再一种场景就是需要在整条链路传参(用户信息)了,我们虽然可以使用方法参数的方式,但是并不优雅,ThreadLocal的set、get了解一下
public class ThreadLocalNormalUsage { public static void main(String[] args) { new Service1().process("mrhe"); } } class Service1 { public void process(String name) { User user = new User(name); UserContextHolder.holder.set(user); new Service2().process(); } } class Service2 { public void process() { User user = UserContextHolder.holder.get(); System.out.println("Service2拿到用户名:" + user.name); new Service3().process(); } } class Service3 { public void process() { User user = UserContextHolder.holder.get(); System.out.println("Service3拿到用户名:" + user.name); UserContextHolder.holder.remove(); } } class UserContextHolder { public static ThreadLocal<User> holder = new ThreadLocal<>(); } class User { String name; public User(String name) { this.name = name; } } 复制代码
为什么会内存泄露呢?会发生在哪儿?
我们先来分析一下ThreadLocal里面的那个Map
/** * The entries in this hash map extend WeakReference, using * its main ref field as the key (which is always a * ThreadLocal object). Note that null keys (i.e. entry.get() * == null) mean that the key is no longer referenced, so the * entry can be expunged from table. Such entries are referred to * as "stale entries" in the code that follows. */ static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } } 复制代码
显然,这个key使用的是弱引用,既然是弱引用,那内存泄露应该不是发生在这里(那就只有value咯~)。
正常情况下,当线程终止,保存在ThreadLocal里的value就会被垃圾回收,因为没有强引用了。但是,如果线程不终止(比如线程池中反复使用并保持的线程),那么key对应的value就不能被回收,因为有如下的调用链:
Thread -> ThreadLocalMap -> Entry(key为null) -> Value
因为这个强引用链路还存在,所以value就无法被回收,就可能出现OOM。JDK已经考虑到这个问题,所以在set、remove和rehash方法中会扫描key为null的Entry,进而把value置为null:
/** * Double the capacity of the table. */ private void resize() { Entry[] oldTab = table; int oldLen = oldTab.length; int newLen = oldLen * 2; Entry[] newTab = new Entry[newLen]; int count = 0; for (int j = 0; j < oldLen; ++j) { Entry e = oldTab[j]; if (e != null) { ThreadLocal<?> k = e.get(); if (k == null) { e.value = null; // Help the GC } else { int h = k.threadLocalHashCode & (newLen - 1); while (newTab[h] != null) h = nextIndex(h, newLen); newTab[h] = e; count++; } } } setThreshold(newLen); size = count; table = newTab; } 复制代码
但是如果一个ThreadLocal不被使用,那么实际上set、rehash等方法也不再被调用,这时线程又不停止的话,就会内存泄漏了。也就是说,需要我们手动去remove。
话说回来,我们一般使用的是static的ThreadLocal,那JDK这个机制也就无效了。而且我们在使用线程池的时候,线程是会复用的,那这个时候为了防止无用value不断堆积又该怎么办呢?
那我们最好在每个任务执行完成的时候做一下必要的清理工作:
/** * 重写线程池中的方法 */ protected void afterExecute(Runnable r, Throwable t) { Thread.currentThread().threadLocals = null; } 复制代码