ThreadLocal原理和思想浅析
基本思想、原理、应用
首先我们知道,如果多个线程共同操作同一块 共享内存 或者说 共享变量 的时候,那么很有可能发生错误。最典型的就是
public class MyAtomicInteger {
private int value;
public void increaseAndGet() {
value += 1;
return value;
}
}
那么比较典型的思想有:
-
用时间换安全
锁
-
用空间换安全
每个线程单独有需要操作的对象。
第一种方式是通用的。
第二种方式有时候可以单独使用,有时候需要配合使用。
锁就不举例子了。
用空间换安全就是说把每个变量复制一份放到各个线程里,线程当然操作的就是自己的变量,不再是共享变量了。
因为多线程发生问题的地方就是 操作了共享变量。
所以我们可以使用ThreadLocal
应用在相同初始配置的变量但是会被多线程并发的情况,一个线程无论怎样操作自己的变量都不会有问题。
例如:
-
使用
SimpleDateFormat
,因为其内部是非线程安全的,没有使用锁来控制,所以就会出现问题。如果我们想要对于每个线程按照相同的格式化标准,比如public class Test { private static final SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMdd"); public void test() { // 假设有多个线程,出问题 simpleDateFormat.parse(""); // } private static final ThreadLocal<SimpleDateFormat> threadLocal = new ThreadLocal<SimpleDateFormat>(){ // 只要当前线程没有该SimpleDateFormat变量,就会创建,否则就会取出当前线程对应的SimpleDateFormat对象。 // 对于每一个线程都创建一个SimpleDateFormat对象。 @Override public SimpleDateFormat initialValue() { return new SimpleDateFormat("yyyyMMdd"); } }; public void threadLocal() { // 假设有多个线程,每个线程操作的是自己的SimpleDateFormat对象 threadLocal.get().parse(); } }
其实你也可以每次方法创建一个对象,不过有点浪费,因为方法结束该对象就会被销毁
public void threadLocal() { // 假设有多个线程,每个线程操作该方法都会使用新建的SimpleDateFormat对象,不会发生错误问题 new SimpleDateFormat("yyyyMMdd").parse(); }
-
Connection对象的创建
由于想要保证对象初始配置相同,且在整个线程运行期间不会变,而且当前线程connection改变不会影响别的。
一个线程一个Connection。
private static final ThreadLocal<Connection> threadLocal = new ThreadLocal<Connection>(){ @Override public Connection initialValue() { // ... return new Connection(); } };
源码思路
关于get和set其实比较简单,就是获取当前线程的ThreadLocalMap
对象,然后如果是第一次get
,就调用initialValue
方法初始化即可。
网上文章很多了。
重点在于为什么ThreadLocalMap
的key
是虚引用。
这得从内存泄露说。
是说如果我们在使用完ThreadLocal对象存储的值后,如果我们不再使用值了,只是进行了如下操作。
private static final ThreadLocal<SimpleDateFormat> threadLocal = new ThreadLocal<SimpleDateFormat>(){
// 只要当前线程没有该SimpleDateFormat变量,就会创建,否则就会取出当前线程对应的SimpleDateFormat对象。
// 对于每一个线程都创建一个SimpleDateFormat对象。
@Override
public SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyyMMdd");
}
};
threadLocal = null;
只是把threadLocal
的指向置为空,但是由于ThreadLocalMap
被当前线程引用,所以当前的threadLocal
还是可以在当前线程存活的情况下被找到,对应存储的Entry对象也会被找到。
弱引用的情况:
既然是这样,那么强引用和弱引用解决的方案都是删除Entry就可以了,那么为什么还需要弱引用呢?
还是怕我们忘记删除Entry对象
所以JDK是这样操作的:
改成弱引用就能保证ThreadLocal不被强引用时,ThreadLocal会帮我们去清理掉key为null的value,这样就避免内存泄露了。
所以什么时候会去清理呢。
就是set/getEntry时候。
因为解决ThreadLocalMap冲突的方式是线性探测再散列,所以会把遍历到的都检测一遍。