今天深入学习了一下ThreadLocal,这里做一个记录。
目录:
一。ThreadLocal简介
二。ThreadLocal的简单使用
三。ThreadLocal的实现原理
3.1ThreadLocal的set方法
3.2ThreadLocal的get方法
3.3 ThreadLocal的setInitialValue()方法
3.4ThreadLocal的remove()方法
四。ThreadLocal的实际应用
五。ThreadLocal不支持继承性
六。InheritableThreadLocal类
七。ThreadLocal和同步方法解决线程安全问题的区别
一。ThreadLocal简介
我们都知道,多线程访问共享变量的时候容易发生线程不安全的情况,为了解决线程不安全的问题,我们可以通过同步机制避免非线程安全。还有一种方式是使用ThreadLocal,也就是可以在不同的线程中维护一份共享变量的副本。这样可以在某些场景下使用可以避免非线程安全。
ThreadLocal是JDK包提供的,它提供线程本地变量,如果创建了一个ThreadLocal变量,那么访问这个变量的每一个线程都会有这个变量的一个副本,在实际多线程操作的时候,操作的是自己本地内存中的变量,从而规避了线程安全问题。
二。ThreadLocal的简单使用
public class ThreadLocalDemo {
private static String currentStr = null;
private static void init(){
currentStr = "init";
}
public static void main(String[] args) {
System.out.println("main start");
init();
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("thread1.start");
System.out.println("thread1-1:"+currentStr);
currentStr = "thread1-temp-end";
System.out.println("thread1-2::"+currentStr);
System.out.println("thread1.end");
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("thread2.start");
System.out.println("thread2-1:"+currentStr);
currentStr = "thread2-temp-end";
System.out.println("thread2-2:"+currentStr);
System.out.println("thread2.end");
}
});
thread1.start();
thread2.start();
System.out.println("main start");
}
}
代码运行结果如下:
main start
main start
thread1.start
thread1-1:init
thread2.start
thread2-1:init
thread2-2:thread2-temp-end
thread1-2::thread2-temp-end
thread1.end
thread2.end
由上看到
thread2-2:thread2-temp-end
thread1-2::thread2-temp-end
这两行就可以看出这是一个典型的存在线程安全问题的代码,两个线程操作了同一个变量,有可能会得到与臆想不一致的情况。
下面给出一个相同场景下使用了ThreadLocal的例子:
public class ThreadLocalDemo2 {
private static ThreadLocal<String> currentStr = new ThreadLocal<>();
public static void main(String[] args) {
System.out.println("main start");
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("thread1.start");
System.out.println("thread1:currentStr1-1:"+currentStr.get());
currentStr.set("thread1-temp-end");
System.out.println("thread1:currentStr1-2:"+currentStr.get());
System.out.println("thread1.end");
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("thread2.start");
System.out.println("thread2:currentStr2-1:"+currentStr.get());
currentStr.set("thread2-temp-end");
System.out.println("thread2:currentStr2-2:"+currentStr.get());
System.out.println("thread2.end");
}
});
thread1.start();
thread2.start();
System.out.println("main start");
}
}
他的运行结果如下:
main start
thread1.start
thread1:currentStr1-1:null
main start
thread2.start
thread2:currentStr2-1:null
thread2:currentStr2-2:thread2-temp-end
thread2.end
thread1:currentStr1-2:thread1-temp-end
thread1.end
我们可以看到两个线程虽然共享了同一个currentStr对象,但是在自己线程中对currentStr变量的修改不会对另一个线程的currentStr对象可见,显然这样就避免了多线程共享数据线程不安全的问题。同时也能看出其实两个线程操作的是自己线程对currentStr变量的一个副本,所以不会影响别的线程的这个currentStr变量。
三。ThreadLocal的实现原理
首先我们来看一下Thread 类,会发现Thread类中有两个变量:
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
/*
* InheritableThreadLocal values pertaining to this thread. This map is
* maintained by the InheritableThreadLocal class.
*/
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
由上可以看到在Thread中维护了两个 ThreadLocal.ThreadLocalMap类型的变量 threadLocals 和 inheritableThreadLocals
那我们再来看一下,ThreadLocal类中主要有这几个方法:
public void set(T value);
public T get();
private T setInitialValue();
public void remove()
这几个方法主要定义了ThreadLocal设置,获取,设置初始值,和清除的方法。
然后还可以看到ThreadLocal类中有一个静态内部类:ThreadLocalMap
ThreadLocalMap内部其实就维护了一个Entry实体,大概跟HashMap差不多。
对于ThreadLocal有一个理解上的误区就是我们会认为每个线程的变量副本存放到ThreadLocal里面,这么理解是不对的,应该是每个线程的副本存放到了ThreadLocal.ThreadLocalMap类型的一个容器里面,这个容器跟Map类似其实是一个Entry。下面我们来看一下ThreadLocalMap的源码:
static class ThreadLocalMap {
/**
* 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;
}
}
}
这里我省去了很多内容,由上可以ThreadLocalMap是ThreadLocal的一个静态内部类,他的构成主要由Entry来保存数据,而且还是继承的WeakReference(弱引用),这里会给后面一个ThreadLocal的变量容易引起内存泄漏的问题买了一个伏笔,这里暂且不说。
好了,现在我们知道了ThreadLocal修饰的变量其实是存放在一个ThreadLocal.ThreadLocalMap类型的一个Entry实体里面的。那么解析来我们再来看一下ThreadLocal中的
public void set(T value);
public T get();
private T setInitialValue();
public void remove()
的这几个方法。
3.1ThreadLocal的set方法:
public void set(T value) {
// 1.获取当前线程
Thread t = Thread.currentThread();
// 2.以当前线程 为key去查找对应的线程变量,找到对应的value
ThreadLocalMap map = getMap(t);
// 3.如果map不为空,则直接添加本地变量,key为当前定义的ThreadLocal变量的this引用,值为添加的本地变量的值
if (map != null)
map.set(this, value);
else
// 4.如果map为空,说明是首次添加,需要先创建出对应的map,并且key为当前定义的ThreadLocal变量的引用,value作为添加的本地变量的值
createMap(t, value);
}
getMap和createMap方法如下:
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
3.2ThreadLocal的get方法:
public T get() {
// 1.获取当前线程
Thread t = Thread.currentThread();
// 2.根据当前线程或者threadLocals变量
ThreadLocalMap map = getMap(t);
// 3.如果获取到的threadLocals变量不为空,就可以直接拿到本地变量的值
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// 4.如果运行到这里,说明没有获取到threadLocals变量,则调用初始化当前线程threadLocals变量的方法
return setInitialValue();
}
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
protected T initialValue() {
return null;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
接上面,由上可以看到初始化threalLocals变量时会初始化一个null,这就是为什么上面第二块代码中第一次调用get方法是会得到一个null的原因。
3.3 ThreadLocal的setInitialValue()方法
继续粘一下setInitialValue()方法
private T setInitialValue() {
// 1.获取一个初始化的value,这个初始化的value为null
T value = initialValue();
// 2.获取当前线程
Thread t = Thread.currentThread();
// 根据当前线程获取threalLocals变量
ThreadLocalMap map = getMap(t);
// 如果能获取到threadLocals变量,则直接设置该值给这个threadLocal变量
if (map != null)
map.set(this, value);
// 如果没有获取到threadLocals变量,则初始化一个map,并把该值设置进去
else
createMap(t, value);
return value;
}
protected T initialValue() {
return null;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
3.4ThreadLocal的remove()方法
public void remove() {
// 1.获取当前线程绑定的threadLocals变量
ThreadLocalMap m = getMap(Thread.currentThread());
// 2.如果获取到的map不为空,则移除当前线程中指定的ThreadLocal实例的本地变量
if (m != null)
m.remove(this);
}
这里说一下,我们已经知道了,ThreadLocal修饰的变量每个线程都会复制一个副本存放到自己线程的Entry中,那么ThreadLocal的变量的生命周期和该线程一样,所以ThreadLocal的变量会一直存在,除非线程消亡了,ThreadLocal修饰的变量才会被回收,那么就容易会造成内存泄漏,所以在线程中对于ThreadLocal的变量,如果用完了就最好手动remove()一下。
四。ThreadLocal的实际应用
public class ThreadLocal5 {
static ThreadLocal<Map<String, String>> localMap = new ThreadLocal<Map<String, String>>(){
@Override
protected Map<String, String> initialValue() {
Map<String, String> map = new HashMap<>(2);
map.put("startTime", "2020-01-01");
map.put("endTime", "2021-12-01");
return map;
}
};
static void print(Map<String, String> map){
for (Map.Entry<String, String> entry : map.entrySet()) {
System.out.println(entry.getKey()+":"+entry.getValue());
}
}
public static void main(String[] args) {
System.out.println("main-start");
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("thread1-start...");
System.out.println("thread1-start-第一次输出完了...");
Map<String, String> map = localMap.get();
map.put("date", "2021-02-01");
map.put("startTime", "2020-07-01");
localMap.set(map);
print(localMap.get());
System.out.println("thread1-end...");
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("thread2-start...");
System.out.println("thread2-start-第一次输出完了...");
Map<String, String> map = localMap.get();
map.put("startTime", "2020-08-01");
map.put("seq", "12");
localMap.set(map);
print(localMap.get());
System.out.println("thread2-end...");
}
});
thread1.start();
thread2.start();
System.out.println("main-end");
}
}
这里是我写的一个ThreadLocal的map,并且初始化了其中的值,每个变量都会共享localMap变量。
/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/bin/java -agentlib:jdwp=transport=dt_socket,address=127.0.0.1:60397,suspend=y,server=n -javaagent:/Users/jiangtao/Library/Caches/JetBrains/IntelliJIdea2020.1/captureAgent/debugger-agent.jar -Dfile.encoding=UTF-8 -classpath /Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/charsets.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/deploy.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/ext/cldrdata.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/ext/dnsns.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/ext/jaccess.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/ext/jfxrt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/ext/localedata.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/ext/nashorn.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/ext/sunec.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/ext/sunjce_provider.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/ext/sunpkcs11.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/ext/zipfs.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/javaws.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/jce.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/jfr.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/jfxswt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/jsse.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/management-agent.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/plugin.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/resources.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/rt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/lib/ant-javafx.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/lib/dt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/lib/javafx-mx.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/lib/jconsole.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/lib/packager.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/lib/sa-jdi.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/lib/tools.jar:/Users/jiangtao/IdeaProjects/demo/threaddemo/target/classes:/Users/jiangtao/java/tools/maven_repository/org/springframework/boot/spring-boot-starter-jdbc/2.4.5/spring-boot-starter-jdbc-2.4.5.jar:/Users/jiangtao/java/tools/maven_repository/org/springframework/boot/spring-boot-starter/2.4.5/spring-boot-starter-2.4.5.jar:/Users/jiangtao/java/tools/maven_repository/org/springframework/boot/spring-boot/2.4.5/spring-boot-2.4.5.jar:/Users/jiangtao/java/tools/maven_repository/org/springframework/boot/spring-boot-autoconfigure/2.4.5/spring-boot-autoconfigure-2.4.5.jar:/Users/jiangtao/java/tools/maven_repository/org/springframework/boot/spring-boot-starter-logging/2.4.5/spring-boot-starter-logging-2.4.5.jar:/Users/jiangtao/java/tools/maven_repository/ch/qos/logback/logback-classic/1.2.3/logback-classic-1.2.3.jar:/Users/jiangtao/java/tools/maven_repository/ch/qos/logback/logback-core/1.2.3/logback-core-1.2.3.jar:/Users/jiangtao/java/tools/maven_repository/org/apache/logging/log4j/log4j-to-slf4j/2.13.3/log4j-to-slf4j-2.13.3.jar:/Users/jiangtao/java/tools/maven_repository/org/apache/logging/log4j/log4j-api/2.13.3/log4j-api-2.13.3.jar:/Users/jiangtao/java/tools/maven_repository/org/slf4j/jul-to-slf4j/1.7.30/jul-to-slf4j-1.7.30.jar:/Users/jiangtao/java/tools/maven_repository/jakarta/annotation/jakarta.annotation-api/1.3.5/jakarta.annotation-api-1.3.5.jar:/Users/jiangtao/java/tools/maven_repository/org/yaml/snakeyaml/1.27/snakeyaml-1.27.jar:/Users/jiangtao/java/tools/maven_repository/com/zaxxer/HikariCP/3.4.5/HikariCP-3.4.5.jar:/Users/jiangtao/java/tools/maven_repository/org/slf4j/slf4j-api/1.7.30/slf4j-api-1.7.30.jar:/Users/jiangtao/java/tools/maven_repository/org/springframework/spring-jdbc/5.3.6/spring-jdbc-5.3.6.jar:/Users/jiangtao/java/tools/maven_repository/org/springframework/spring-beans/5.3.6/spring-beans-5.3.6.jar:/Users/jiangtao/java/tools/maven_repository/org/springframework/spring-tx/5.3.6/spring-tx-5.3.6.jar:/Users/jiangtao/java/tools/maven_repository/org/springframework/boot/spring-boot-starter-web/2.4.5/spring-boot-starter-web-2.4.5.jar:/Users/jiangtao/java/tools/maven_repository/org/springframework/boot/spring-boot-starter-json/2.4.5/spring-boot-starter-json-2.4.5.jar:/Users/jiangtao/java/tools/maven_repository/com/fasterxml/jackson/core/jackson-databind/2.11.4/jackson-databind-2.11.4.jar:/Users/jiangtao/java/tools/maven_repository/com/fasterxml/jackson/core/jackson-annotations/2.11.4/jackson-annotations-2.11.4.jar:/Users/jiangtao/java/tools/maven_repository/com/fasterxml/jackson/core/jackson-core/2.11.4/jackson-core-2.11.4.jar:/Users/jiangtao/java/tools/maven_repository/com/fasterxml/jackson/datatype/jackson-datatype-jdk8/2.11.4/jackson-datatype-jdk8-2.11.4.jar:/Users/jiangtao/java/tools/maven_repository/com/fasterxml/jackson/datatype/jackson-datatype-jsr310/2.11.4/jackson-datatype-jsr310-2.11.4.jar:/Users/jiangtao/java/tools/maven_repository/com/fasterxml/jackson/module/jackson-module-parameter-names/2.11.4/jackson-module-parameter-names-2.11.4.jar:/Users/jiangtao/java/tools/maven_repository/org/springframework/boot/spring-boot-starter-tomcat/2.4.5/spring-boot-starter-tomcat-2.4.5.jar:/Users/jiangtao/java/tools/maven_repository/org/apache/tomcat/embed/tomcat-embed-core/9.0.45/tomcat-embed-core-9.0.45.jar:/Users/jiangtao/java/tools/maven_repository/org/glassfish/jakarta.el/3.0.3/jakarta.el-3.0.3.jar:/Users/jiangtao/java/tools/maven_repository/org/apache/tomcat/embed/tomcat-embed-websocket/9.0.45/tomcat-embed-websocket-9.0.45.jar:/Users/jiangtao/java/tools/maven_repository/org/springframework/spring-web/5.3.6/spring-web-5.3.6.jar:/Users/jiangtao/java/tools/maven_repository/org/springframework/spring-webmvc/5.3.6/spring-webmvc-5.3.6.jar:/Users/jiangtao/java/tools/maven_repository/org/springframework/spring-aop/5.3.6/spring-aop-5.3.6.jar:/Users/jiangtao/java/tools/maven_repository/org/springframework/spring-context/5.3.6/spring-context-5.3.6.jar:/Users/jiangtao/java/tools/maven_repository/org/springframework/spring-expression/5.3.6/spring-expression-5.3.6.jar:/Users/jiangtao/java/tools/maven_repository/org/mybatis/spring/boot/mybatis-spring-boot-starter/2.1.4/mybatis-spring-boot-starter-2.1.4.jar:/Users/jiangtao/java/tools/maven_repository/org/mybatis/spring/boot/mybatis-spring-boot-autoconfigure/2.1.4/mybatis-spring-boot-autoconfigure-2.1.4.jar:/Users/jiangtao/java/tools/maven_repository/org/mybatis/mybatis/3.5.6/mybatis-3.5.6.jar:/Users/jiangtao/java/tools/maven_repository/org/mybatis/mybatis-spring/2.0.6/mybatis-spring-2.0.6.jar:/Users/jiangtao/java/tools/maven_repository/org/springframework/spring-core/5.3.6/spring-core-5.3.6.jar:/Users/jiangtao/java/tools/maven_repository/org/springframework/spring-jcl/5.3.6/spring-jcl-5.3.6.jar:/Applications/IntelliJ IDEA.app/Contents/lib/idea_rt.jar com.taojiang.threaddemo.threadlocal.ThreadLocal5
Connected to the target VM, address: '127.0.0.1:60397', transport: 'socket'
main-start
thread1-start...
thread1-start-第一次输出完了...
main-end
thread2-start...
thread2-start-第一次输出完了...
date:2021-02-01
startTime:2020-08-01
startTime:2020-07-01
seq:12
endTime:2021-12-01
endTime:2021-12-01
thread2-end...
thread1-end...
Disconnected from the target VM, address: '127.0.0.1:60397', transport: 'socket'
Process finished with exit code 0
由上可知每个线程可以随便使用localMap变量,随意修改后不会影响别的线程。
五。ThreadLocal不支持继承性
同一个ThrealLocal变量在父线程中被设置了值后,在子线程中是获取不到的
代码实例如下:
public class ThreadLocal6 {
static ThreadLocal<String> localVar = new ThreadLocal<>();
public static void main(String[] args) {
System.out.println("main-start");
localVar.set("init");
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("thread1-start...");
System.out.println("thread1设置前:"+localVar.get());
localVar.set("localVar01");
System.out.println("thread1设置后:"+localVar.get());
System.out.println("thread1-end...");
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("thread2-start...");
System.out.println("thread2设置前:"+localVar.get());
localVar.set("localVar02");
System.out.println("thread2设置后:"+localVar.get());
System.out.println("thread2-end...");
}
});
thread1.start();
thread2.start();
System.out.println("main-getlocalVar:"+localVar.get());
System.out.println("main-end");
}
}
运行结果如下:
main-start
thread1-start...
thread1设置前:null
thread1设置后:localVar01
thread1-end...
thread2-start...
thread2设置前:null
thread2设置后:localVar02
thread2-end...
main-getlocalVar:init
main-end
由此可以看出子线程是拿不到父线程事先设置的ThrealLocal变量的值的。所以ThreadLocal不具有继承性。
六。InheritableThreadLocal类
上面说道ThreadLocal类不能继承,那么我们可以做个父传子吗?答案是:可以,使用InheritableThreadLocal类就可以。
具体为什么可以,源码这里不展开说了。
今天先写到这里。写的有点乱,如果有错误欢迎大家指正。
七。ThreadLocal和同步方法解决线程安全问题的区别
学习完本节我们知道了在某些场景下ThrealLocal也可以保证多线程线程安全,那么他跟同步方法有什么区别呢?
同步方法是牺牲了时间,也就是在同一时刻只能有一个线程访问共享变量,所以同步方法是时间换安全的一种方式
而ThrealLocal是每一个线程都有一个共享变量的副本,线程操作的是自己的那份拷贝,所以也解决了线程安全的问题,这种方式是以空间换安全的做法。