本文主要讲解:
- ThreadLocal的用法
- 多线程竞争同一个变量
- 同一个线程无需显式调用
- ThreadLocal的原理
- 数据结构
- set、get 和 remove方法
- ThreadLocal的问题
- 线程不安全的场景
- 内存溢出问题
- 延伸:强弱虚软引用
本文源码地址:E01_ThreadLocal, 欢迎star我的Github
正文开始
ThreadLocal的用法
- 多线程竞争同一个变量
当我们使用线程池来复用线程的时候,对于同一个变量的竞争使用,一般会导致线程安全问题,因此建议放入到ThreadLocal中使用。
public class RightThreadLocalDemo {
public static ExecutorService tpool = Executors.newFixedThreadPool(10);
@Test//一千个线程
public void ThreadLocalDemo_right() throws InterruptedException {
for (int i = 0; i < 1000; i++) {
int fi = i;
tpool.submit(() -> {
String date = new RightThreadLocalDemo().date(fi);
System.out.println(Thread.currentThread().getName() + ": " + date);
});
}
Thread.sleep(100);
tpool.shutdown();
}
public String date(int second) {
Date date = new Date(1000 * second);
SimpleDateFormat df = datefmt1.get();
//此处证明了ThreadLocal即使是static 对象,其在线程中也不仅仅是一个,而是以副本的形式存在于线程中
System.out.println(Thread.currentThread().getName()+"========"+System.identityHashCode(df));
return df.format(date);
}
//此处是新建 ThreadLocal
public static ThreadLocal<SimpleDateFormat> datefmt1 =
ThreadLocal.withInitial(() -> new SimpleDateFormat("mm:ss"));
}
- 注意到一个问题:为什么ThreadLocal可以写在很多地方,比如写在不同的类中,用的时候确在另一个类或者方法里面,但是依旧是线程安全的?
- 因为ThreadLocal是属于线程的,使用了 Thread.currentThread() 来获取当前线程。
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
- 同一个线程无需显式调用
当一个线程需要经历很多类的很多方法,一般是将需要的对象当作参数,每一步都带下去,此时可以使用ThreadLocal,就像web里面的session一下,做到随取随用。
public class RightThreadLocalDemo2 {
static ThreadLocal<User> tl = new ThreadLocal<>();
@Test
public void deal(String nn){
new Service1().service(nn);
}
public static ExecutorService tpool = Executors.newFixedThreadPool(10);
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 100; i++) {
int fg = i;
tpool.submit(()->{
new Service1().service("ljfirst---"+fg);
});
}
//Thread.sleep(100);
tpool.shutdown();
}
}
class Service1{
public void service(String n){
User u = new User(n);
RightThreadLocalDemo2.tl.set(u);
new Service2().service();
}
}
class Service2{
public void service(){
User u = RightThreadLocalDemo2.tl.get();
System.out.println(Thread.currentThread().getName()+" ---Service2().service(): "+u.name);
new Service3().service();
}
}
class Service3{
public void service(){
User u = RightThreadLocalDemo2.tl.get();
System.out.println(Thread.currentThread().getName()+" ---Service3().service(): "+u.name);
new Service4().service();
}
}
class Service4{
public void service(){
User u = RightThreadLocalDemo2.tl.get();
System.out.println(Thread.currentThread().getName()+" ---Service4().service(): "+u.name);
//不用的时候记得remove
RightThreadLocalDemo2.tl.remove();
}
}
class User{
String name ;
public User(String name){
this.name = name;
}
}
ThreadLocal的原理
- 数据结构
图片来源:用了三年 ThreadLocal 今天才弄明白其中的道理
- set、get 和 remove方法
- set方法
public void set(T value) {
// 获取当前线程
Thread t = Thread.currentThread();
// 从 Thread 中获取 ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
- get方法
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
- remove方法
在这里插入代码片public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
上述提及的三个方法,只是表象,需要看具体的实现,还是进ThreadLocalMap中看。源码解析来自于:ThreadLocal有没有内存泄漏?源码给你安排得明明白白
ThreadLocal的问题
- 线程不安全的场景
public class threadlocal复用污染 {
public static ExecutorService tpool = Executors.newFixedThreadPool(10);
@Test
public void test() {
Thread t = new Thread(()->{
Tools.tl.set("bbb");
System.out.println(Thread.currentThread().getName() + ":======" + Tools.tl.get());
//这句话不加会导致后续的线程复用时,threadlocal也被复用,因此造成线程不安全
Tools.tl.remove();
});
tpool.submit(t);
Thread t1 = new Thread(()->{
Tools.dd();
System.out.println(Thread.currentThread().getName() + ":======" + Tools.tl.get());
/*
ddd.tl.remove():这句话不加会导致后续的线程复用时,threadlocal也被复用,因此造成线程不安全
现象:所有的pool-1-thread-1 只会出现一次pool-1-thread-1:======bbb,
pool-1-thread-2没有清除threadlocal,因此会出现多次 pool-1-thread-2aaaa
*/
});
tpool.submit(t1);
for (int i = 0; i < 100; i++) {
tpool.submit(() -> {
System.out.println(Thread.currentThread().getName() + Tools.tl.get());
});
}
tpool.shutdown();
}
}
class Tools {
static ThreadLocal<String> tl = new ThreadLocal<>();
public static void dd() {
tl.set("aaaa");
}
}
- 内存溢出问题
public class ThreadLocal内存泄漏 {
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Task().calc(10);
//通过下方的Evaluate Expression 可以看出在80前后回收了 线程里面的内容
//但是仅仅回收了map 的 key(当前的ThreadLocal),并不是回收Value。
//因此存在内存溢出的问题。
if (i == 80) {
System.gc();
}
}
}
static class Task {
ThreadLocal<Integer> value;
public int calc(int i) {
value = new ThreadLocal();
value.set((value.get() == null ? 0 : value.get()) + i);
return value.get();
}
}
}
- 延伸:强弱虚软引用
- 强引用:
- 类似于Student s = new Student();这里的s就是一个强引用,当线程、对象消失,或者手动s=null;就在恰当的时间,被GC掉。
- 弱引用:
- 当一个对象同时被强、弱引用指向时,它不会被回收,但是当强引用消失,那么弱引用就会在恰当的时间,被GC掉,有种狐假虎威的感觉。
- 下方代码的Entry,在线程销毁,或者线程池销毁的时候,将被GC掉。
- 弱引用的特点是不管内存是否足够,只要发生GC,都会被回收
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
- 虚引用:
- 在NIO中,就运用了虚引用管理堆外内存
- 软引用:
- 软引用就是把对象用SoftReference包裹一下,当我们需要从软引用对象获得包裹的对象,只要get一下就可以了。
- 当内存不足,会触发JVM的GC,如果GC后,内存还是不足,就会把软引用的包裹的对象给干掉,也就是只有在内存不足,JVM才会回收该对象
SoftReference<Student>studentSoftReference=new SoftReference<Student>(new Student());
Student student = studentSoftReference.get();
System.out.println(student);
强软弱虚的验证代码:强软弱虚 参考博客:强软弱虚引用