线程的生命周期和状态?
Java 线程在运行的生命周期中的指定时刻只可能处于下面 6 种不同状态的其中一个状态(图源《Java 并发编程艺术》4.1.4 节)。
线程在生命周期中并不是固定处于某一个状态而是随着代码的执行在不同状态之间切换。Java 线程状态变迁如下图所示(图源《Java 并发编程艺术》4.1.4 节):
订正(来自issue736):原图中 wait 到 runnable 状态的转换中,join实际上是Thread类的方法,但这里写成了Object。由上图可以看出:线程创建之后它将处于 NEW(新建) 状态,调用 start() 方法后开始运行,线程这时候处于 READY(可运行) 状态。可运行状态的线程获得了 CPU 时间片(timeslice)后就处于 RUNNING(运行) 状态。在操作系统中层面线程有 READY 和 RUNNING 状态,而在 JVM 层面只能看到 RUNNABLE 状态(图源:HowToDoInJava:Java Thread Life Cycle and Thread States),所以 Java 系统一般将这两个状态统称为 RUNNABLE(运行中) 状态 。为什么 JVM 没有区分这两种状态呢? (摘自:java线程运行怎么有第六种状态? - Dawell的回答** ) 现在的时分(time-sharing)多任务(multi-task)操作系统架构通常都是用所谓的“时间分片(time quantum or time slice)”方式进行抢占式(preemptive)轮转调度(round-robin式)。这个时间分片通常是很小的,一个线程一次最多只能在 CPU 上运行比如 10-20ms 的时间(此时处于 running 状态),也即大概只有 0.01 秒这一量级,时间片用后就要被切换下来放入调度队列的末尾等待再次调度。(也即回到 ready 状态)。线程切换的如此之快,区分这两种状态就没什么意义了。**
当线程执行 wait()方法之后,线程进入 WAITING(等待) 状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态,而 TIME_WAITING(超时等待) 状态相当于在等待状态的基础上增加了超时限制,比如通过 sleep(long millis)方法或 wait(long millis)方法可以将 Java 线程置于 TIMED WAITING 状态。当超时时间到达后 Java 线程将会返回到 RUNNABLE 状态。当线程调用同步方法时,在没有获取到锁的情况下,线程将会进入到 BLOCKED(阻塞) 状态。线程在执行 Runnable 的run()方法之后将会进入到 TERMINATED(终止) 状态。相关阅读:挑错 |《Java 并发编程的艺术》中关于线程状态的三处错误 。
ThreadLocal 关键字解析
前言
对于ThreadLocal,大家的第一反应可能是很简单呀,线程的变量副本,每个线程隔离。那这里有几个问题大家可以思考一下:
- ThreadLocal的 key 是弱引用,那么在 ThreadLocal.get()的时候,发生GC之后,key 是否为null?
- ThreadLocal中ThreadLocalMap的数据结构?
- ThreadLocalMap的Hash 算法?
- ThreadLocalMap中Hash 冲突如何解决?
- ThreadLocalMap的扩容机制?
- ThreadLocalMap中过期 key 的清理机制?探测式清理和启发式清理流程?
- ThreadLocalMap.set()方法实现原理?
- ThreadLocalMap.get()方法实现原理?
- 项目中ThreadLocal使用情况?遇到的坑?
- ......
注明: 本文源码基于JDK 1.8
ThreadLocal代码演示
我们先看下ThreadLocal使用示例:
public class ThreadLocalTest {
private List<String> messages = Lists.newArrayList();
public static final ThreadLocal<ThreadLocalTest> holder = ThreadLocal.withInitial(ThreadLocalTest::new);
public static void add(String message) {
holder.get().messages.add(message);
}
public static List<String> clear() {
List<String> messages = holder.get().messages;
holder.remove();
System.out.println("size: " + holder.get().messages.size());
return messages;
}
public static void main(String[] args) {
ThreadLocalTest.add("一枝花算不算浪漫");
System.out.println(holder.get().messages);
ThreadLocalTest.clear();
}
}
复制代码
clipboardErrorCopied打印结果:[一枝花算不算浪漫] size: 0Copy to clipboardErrorCopiedThreadLocal对象可以提供线程局部变量,每个线程Thread拥有一份自己的副本变量,多个线程互不干扰。
ThreadLocal的数据结构
Thread类有一个类型为ThreadLocal.ThreadLocalMap的实例变量threadLocals,也就是说每个线程有一个自己的ThreadLocalMap。ThreadLocalMap有自己的独立实现,可以简单地将它的key视作ThreadLocal,value为代码中放入的值(实际上key并不是ThreadLocal本身,而是它的一个弱引用)。每个线程在往ThreadLocal里放值的时候,都会往自己的ThreadLocalMap里存,读也是以ThreadLocal作为引用,在自己的map里找对应的key,从而实现了线程隔离。ThreadLocalMap有点类似HashMap的结构,只是HashMap是由数组+链表实现的,而ThreadLocalMap中并没有链表结构。我们还要注意Entry, 它的key是ThreadLocal<?> k ,继承自WeakReference, 也就是我们常说的弱引用类型。
GC 之后 key 是否为 null?
回应开头的那个问题, ThreadLocal 的key是弱引用,那么在ThreadLocal.get()的时候,发生GC之后,key是否是null?为了搞清楚这个问题,我们需要搞清楚Java的四种引用类型:
- 强引用:我们常常 new 出来的对象就是强引用类型,只要强引用存在,垃圾回收器将永远不会回收被引用的对象,哪怕内存不足的时候
- 软引用:使用 SoftReference 修饰的对象被称为软引用,软引用指向的对象在内存要溢出的时候被回收
- 弱引用:使用 WeakReference 修饰的对象被称为弱引用,只要发生垃圾回收,若这个对象只被弱引用指向,那么就会被回收
- 虚引用:虚引用是最弱的引用,在 Java 中使用 PhantomReference 进行定义。虚引用中唯一的作用就是用队列接收对象即将死亡的通知
接着再来看下代码,我们使用反射的方式来看看GC后ThreadLocal中的数据情况:(下面代码来源自:blog.csdn.net/thewindkee/… 本地运行演示 GC 回收场景)
public class ThreadLocalDemo {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, InterruptedException {
Thread t = new Thread(()->test("abc",false));
t.start();
t.join();
System.out.println("--gc后--");
Thread t2 = new Thread(() -> test("def", true));
t2.start();
t2.join();
}
private static void test(String s,boolean isGC) {
try {
new ThreadLocal<>().set(s);
if (isGC) {
System.gc();
}
Thread t = Thread.currentThread();
Class<? extends Thread> clz = t.getClass();
Field field = clz.getDeclaredField("threadLocals");
field.setAccessible(true);
Object ThreadLocalMap = field.get(t);
Class<?> tlmClass = ThreadLocalMap.getClass();
Field tableField = tlmClass.getDeclaredField("table");
tableField.setAccessible(true);
Object[] arr = (Object[]) tableField.get(ThreadLocalMap);
for (Object o : arr) {
if (o != null) {
Class<?> entryClass = o.getClass();
Field valueField = entryClass.getDeclaredField("value");
Field referenceField = entryClass.getSuperclass().getSuperclass().getDeclaredField("referent");
valueField.setAccessible(true);
referenceField.setAccessible(true);
System.out.println(String.format("弱引用key:%s,值:%s", referenceField.get(o), valueField.get(o)));
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
复制代码
clipboardErrorCopied结果如下:弱引用key:java.lang.ThreadLocal@433619b6,值:abc 弱引用key:java.lang.ThreadLocal@418a15e3,值:java.lang.ref.SoftReference@bf97a12 --gc后-- 弱引用key:null,值:defCopy to clipboardErrorCopied
如图所示,因为这里创建的ThreadLocal并没有指向任何值,也就是没有任何引用:new ThreadLocal<>().set(s);Copy to clipboardErrorCopied所以这里在GC之后,key就会被回收,我们看到上面debug中的referent=null, 如果改动一下代码:
这个问题刚开始看,如果没有过多思考,弱引用,还有垃圾回收,那么肯定会觉得是null。其实是不对的,因为题目说的是在做 ThreadLocal.get() 操作,证明其实还是有强引用存在的,所以 key 并不为 null,如下图所示,ThreadLocal的强引用仍然是存在的。
如果我们的强引用不存在的话,那么 key 就会被回收,也就是会出现我们 value 没被回收,key 被回收,导致 value 永远存在,出现内存泄漏。
ThreadLocal.set()方法源码详解
ThreadLocal中的set方法原理如上图所示,很简单,主要是判断ThreadLocalMap是否存在,然后使用ThreadLocal中的set方法进行数据处理。代码如下:
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
复制代码
y to clipboardErrorCopied主