ThreadLocal浅析

编程入门 行业动态 更新时间:2024-10-05 23:22:59

<a href=https://www.elefans.com/category/jswz/34/1762419.html style=ThreadLocal浅析"/>

ThreadLocal浅析

ThreadLocal是一个存储线程本地变量的对象,在ThreadLocal中存储的对象在其他线程中是不可见的,本文介绍ThreadLocal的原理。

1、threadLocal使用

1.1 案例一

多个线程中使用1个threadLocal:

@Slf4j
public class TestThreadLocal {public static void main(String[] args) {ThreadLocal<Integer> threadLocal = new ThreadLocal<>();threadLocal.set(999);log.info(threadLocal.get().toString());//使用线程池创建一个线程ExecutorService service = Executors.newSingleThreadExecutor();service.execute(()->{log.info(threadLocal.get().toString());//threadLocal.get()将为nullthreadLocal.set(888);log.info(threadLocal.get().toString());});log.info(threadLocal.get().toString());}
}
输出:
2023-03-09 09:03:40.572 [main] INFO -- 999
2023-03-09 09:03:40.598 [main] INFO -- 999
Exception in thread "pool-1-thread-1" java.lang.NullPointerExceptionat com.iwat.arithmetic.thread.TestThreadLocal.lambda$main$0(TestThreadLocal.java:24)at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)at java.lang.Thread.run(Thread.java:748)

从结果中可以看到,在新线程中无法获取到999,主线程中set的数,只有祝线程能get到,这就保证了变量的线程唯一。

1.2 案例二

一个线程中使用多个threadLocal:

public static void main(String[] args) {ThreadLocal<String> t1 = new ThreadLocal<>();t1.set("123");System.out.println(t1.get());m2();}public static void m2(){ThreadLocal<String> t2 = new ThreadLocal<>();System.out.println("m2: "+t2.get());t2.remove();}
//输出:
123
m2: null

可以看到两个threadLocal中的值不共享。

2、原理

2.1 ThreadLocal.set(T value)

首先看threadLocal.set(999);的源码

	//ThreadLocal类中public void set(T value) {//1.获取当前线程Thread t = Thread.currentThread();//2. 获取线程中的ThreadLocalMap对象ThreadLocalMap map = getMap(t);if (map != null)//线程中的ThreadLocalMap对象不为空,直接set值,this就是当前ThreadLocal对象没,值就是值map.set(this, value);else//线程中的ThreadLocalMap对象为空,创建并set值createMap(t, value);}

很简单,获取当前线程,然后获取当前线程的ThreadLocalMap对象,这里的ThreadLocalMap可以就当做一个Map处理,但是这个Map不一样的地方在于key只能是ThreadLocal对象。现在我们就可以分析一下了。

2.2 ThreadLocalMap

每一线程都有一个ThreadLocalMap对象,在Thread类中有一个ThreadLocal.ThreadLocalMap threadLocals = null;,对于每一个ThreadLocal对象来说它可以在每一个线程中ThreadLocalMap中存一个以它为key的值。例如:

  • 在线程1中,ThreadLocal对象1执行set方法,实际上就是在线程1中的ThreadLocalMap中添加一个k-v(1号红色箭头),k就是ThreadLocal对象1。
  • 在线程1中,ThreadLocal对象2执行get方法,就是在线程1的ThreadLocalMap中获取以ThreadLocal对象2为key的值(2号红色箭头)

每个ThreadLocal在某个线程中只能有一个对应value,因为Map中key不能重复,这就实现了线程唯一变量的功能。

下面看一下ThreadLocal中的get方法验证一下分析:

//ThreadLocal类中public T get() {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null) {ThreadLocalMap.Entry e = map.getEntry(this);//在ThreadLocal类中,this就是一个ThreadLocal对象if (e != null) {@SuppressWarnings("unchecked")T result = (T)e.value;return result;}}return setInitialValue();}

同时这也解释了,为什么上面代码中出现Exception in thread "pool-1-thread-1" java.lang.NullPointerException,原因就是在新创建的线程中的ThreadLocalMap中没有以当前ThreadLocal对象为key的值。

2.3 Entry

上面源码中ThreadLocalMap.Entry e = map.getEntry(this);,出现了一个Entry,Entry是ThreadLocalMap的一个内部类,Entry继承自WeakReference<ThreadLocal<?>>,素以相当于一个Entry对象会引用一个ThreadLocal对象同时存储一个值,就类似一个k-v的结构(和HashMap中的Entry类似),ThreadLocalMap中有一个Entry数组,这就是ThreadLocalMap的实现原理(一个Entry是一个k-v,一个Entry数组就是多个k-v,也就是一个Map),这也是为什么称之为Map。

static class Entry extends WeakReference<ThreadLocal<?>> {/** The value associated with this ThreadLocal. */Object value;Entry(ThreadLocal<?> k, Object v) {super(k);value = v;}}

接着看一下 ThreadLocalMap.Entry e = map.getEntry(this);//this就是threadLoacl对像的 map.getEntry(this)源码

private Entry getEntry(ThreadLocal<?> key) {int i = key.threadLocalHashCode & (table.length - 1);Entry e = table[i];if (e != null && e.get() == key)return e;elsereturn getEntryAfterMiss(key, i, e);}

前面说了,ThreadLocalMap中有一个Entry数组,getEntry中就是找到当前ThreadLocal所对应Entry。

2.4 小结

每一个线程有一个:ThreadLocalMap threadLocals,其中有一个Entry数组,一个Entry可以保存一个kv。
在一个线程中每创建一个ThreadLocal,就会在threadLocals中创建一个Entry,key就是刚刚创建的ThreadLocal,v可以任意设置。

ThreadLocal<String> t1 = new ThreadLocal<>();
t1.set("123");

上述代码的执行逻辑:
ThreadLocal<String> t1 = new ThreadLocal<>();:创建一个ThreadLocal t1,在ThreadLocalMap threadLocals中添加一个Entry对象,k为t1,v为null
t1.set("123");:在threadLocals的Entry[ ]里,为key为t1的Entry中添加v=123

3、java引用类型

3.1 GC回收对象

java垃圾回收器多采用可达性分析算法来确定一个java对象需不需要回收,过程是这样的:

  • 首先确定一些一定不能回收的对象,叫做GCRoot对象
  • 查找GCRoot对象引用的对象
  • 然后沿着引用链一直找(注意:引用有多种类型)

最终根据对象被引用的类型、有没有被引用,来决定是不是回收这个对象。

3.2 引用类型

那么引用类型有哪些呢?总体而言分为四种:

  1. 强引用
    沿着GC Root引用链可以找到的对象就是强引用对象
  2. 软引用 (SoftReference)
    垃圾回收后仍内存不足,就会回收掉
  3. 弱引用 (WeakReference)
    垃圾回收就将其回收掉
  4. 虚引用 (PhantomReference)
    必须配合引用队列使用,主要配合 ByteButfer 使用,被引用对象回收时,会将虛引用入队,由
    Reference Handler 线程调用虚引用相关方法释放直接内存

3.3 弱引用

上面说到了弱引用,弱引用的作用是:当一个对象A引用了另一个对象B的弱引用,如果对象B在系统不存在其他引用了,垃圾回收时就会回收掉对象B。
例如在下列的缓存实现中,Map<K, WeakReference> cache中的V是一个对象的弱引用,当对象V不再被除了缓存的其他对象引用了,执行垃圾回收时就会将V回收掉(垃圾回收也不一定什么时候,所以V即使不被其他对象引用了也不会立刻被回收),这样就防止了无用对象一直占用缓存,造成内存泄漏。什么是内存泄漏?就是指一部分对象一直占用内存,也清不掉,就好像这块内存空间没了,漏掉了。
接着继续查看下面的get方法,缓存没有命中时要清理掉Key cache.remove(key);,否则也会导致内存泄漏。

import java.lang.ref.WeakReference;
import java.util.HashMap;
import java.util.Map;class Cache<K, V> {private final Map<K, WeakReference<V>> cache = new HashMap<>();public V get(K key) {WeakReference<V> weakRef = cache.get(key);if (weakRef != null) {V value = weakRef.get();if (value != null) {// 缓存命中,返回缓存的对象return value;} else {// 对象已经被垃圾回收,从缓存中移除cache.remove(key);}}// 缓存未命中,返回null或执行加载操作return null;}public void put(K key, V value) {// 使用WeakReference封装value并放入缓存cache.put(key, new WeakReference<>(value));}public void remove(K key) {cache.remove(key);}public void clear() {cache.clear();}
}public class CacheExample {public static void main(String[] args) {// 创建一个缓存Cache<String, String> cache = new Cache<>();// 向缓存中放入对象cache.put("key1", "value1");cache.put("key2", "value2");// 从缓存中获取对象String value1 = cache.get("key1");String value2 = cache.get("key2");String value3 = cache.get("key3"); // 返回null,因为key3不在缓存中或已被回收System.out.println("Value1: " + value1);System.out.println("Value2: " + value2);System.out.println("Value3: " + value3);// 清空缓存cache.clear();}
}

4、内存泄漏问题

在看了java中几种引用类型、弱引用、内存泄漏之后在回头看一下Entry的实现,在ThreadLocalMap的内部使用的是Entry类存储k-v。

static class ThreadLocalMap {static class Entry extends WeakReference<ThreadLocal<?>> {/** The value associated with this ThreadLocal. */Object value;Entry(ThreadLocal<?> k, Object v) {super(k);value = v;}}/*** The initial capacity -- MUST be a power of two.*/private static final int INITIAL_CAPACITY = 16;//省略若干代码
}

在ThreadLocal的使用时,线程的ThreadLocalMap中都存储了以ThreadLocal为key的值,如果ThreadLocal被清理了,那么
ThreadLocalMap中对应的数据会被清理吗?
例如:下列的m2方法执行完毕,局部变量t1就会被回收,此时线程的ThreadLocalMap中的某个Entry仍然引用这t1对象,但是Entry中是t1的一个弱引用,因此在下次垃圾回收时,这个Entry中的t1就会被回收。

public static void m2(){ThreadLocal<String> t1 = new ThreadLocal<>();t1.set("value");System.out.println("m2: "+t1.get());}

但是!!!!!!!!此处任然会有内存泄漏的问题,因为t1.set(“value”);中的"value"还被Entry引用着,只是key变成了null,所以Entry和其中的value还会一直占用内存,这也导致了内存泄漏,那应该怎么做呢?很简单,使用完毕之后将Entry中value手动移除就行了。

public static void m2(){ThreadLocal<String> t1 = new ThreadLocal<>();System.out.println("m2: "+t1.get());t1.remove();
}

索引使用ThreadLocal一定要记着remove。



以上就是ThreadLocal的全部内容,如有不足欢迎指正!

更多推荐

ThreadLocal浅析

本文发布于:2024-02-13 09:55:52,感谢您对本站的认可!
本文链接:https://www.elefans.com/category/jswz/34/1758380.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:ThreadLocal

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!