ThreadLocal知识小结
Posted 赵jc
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ThreadLocal知识小结相关的知识,希望对你有一定的参考价值。
什么是ThreadLocal ?
问题引入:
线程安全的解决方案有两种加锁和创建私有变量,但加锁后任务会排队执行,消耗大量时间,每次任务会创建新的私有变量会消耗大量的资源,那么有没有一种更好的方法呢,肯定是有的啦,ThreadLocal是线程的本地变量,每一个线程会创建一个私有变量。
SimpleDateFormat 是线程不安全的类,一般不要定义为 static 变量,如果定义为 static,必须加锁,或者使用 DateUtils 工具类。
ThreadLocal 使⽤
ThreadLocal 核⼼⽅法总共有以下三个:
- set(T):将内容存储到ThreadLocal。没有 set 操作的 ThreadLocal 容易引起脏数据。
- get:从线程中取私有变量。没有 get 操作的 ThreadLocal 对象没有意义。
- remove:从线程中移除私有变量。没有 remove 操作容易引起内存泄漏。
代码演示:
/**
* ThreadLocal使用
*/
private static ThreadLocal<String> threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
//线程执行任务
Runnable runnable = new Runnable() {
@Override
public void run() {
String threadName = Thread.currentThread().getName();
System.out.println(threadName + "存入值" + threadName);
//在ThreadLocal中设置值
threadLocal.set(threadName);
//执行方法,打印线程中设置的值
print(threadName);
}
};
//创建并启动线程1
new Thread(runnable, "MyThread-1").start();
//创建并启动线程2
new Thread(runnable, "MyThread-2").start();
}
/**
* 打印线程中ThreadLocal值
*/
private static void print(String threadName) {
try{
//得到ThreadLocal中得值
String result = threadLocal.get();
//打印结果
System.out.println(threadName + "取出值" + result);
}finally {
//移除ThreadLocal中得值
threadLocal.remove();
}
}
ThreadLocal有两种初始化方法initialValue和
initialValue
⽅法(线程级别的)需要new一个变量
private static ThreadLocal<Integer> threadLocal =
new ThreadLocal() {
@Override
protected Integer initialValue() {
int num = new Random().nextInt(10);
System.out.println("执行了 initialValue 生成了:" + num);
return num;
}
};
public static void main(String[] args) {
// 创建线程池
ThreadPoolExecutor executor = new ThreadPoolExecutor(
1, 1,
0, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1000));
executor.submit(new Runnable() {
@Override
public void run() {
// get ThreadLocal
int result = threadLocal.get();
System.out.println(Thread.currentThread().getName() +
" 得到结果1:" + result);
}
});
executor.submit(new Runnable() {
@Override
public void run() {
// get ThreadLocal
int result = threadLocal.get();
System.out.println(Thread.currentThread().getName() +
" 得到结果2:" + result);
}
});
}
withInitial
⽅法,静态方法不需要new对象
// 创建并初始化 ThreadLocal
private static ThreadLocal<String> threadLocal =
ThreadLocal.withInitial(new Supplier<String>() {
@Override
public String get() {
System.out.println("执行了 withInitial 方法");
return Thread.currentThread().getName() + " Java";
}
});
/**
// 创建并初始化 ThreadLocal lambada表达式写法
private static ThreadLocal<String> threadLocal =
ThreadLocal.withInitial(() -> "Java");
*/
public static void main(String[] args) {
Runnable runnable = new Runnable() {
@Override
public void run() {
// 获取 ThreadLoal
String result = threadLocal.get();
System.out.println(Thread.currentThread().getName() +
" 获取到的内容:" + result);
}
};
Thread t1 = new Thread(runnable, "线程1");
t1.start();
Thread t2 = new Thread(runnable, "线程2");
t2.start();
}
注意
:如果执行了ThreadLocal的set方法,之后就不会执行initialValue方法了,这是由于ThreadLocal.get()方法的源码的设计了,它是懒加载的方式,initialValue方法在初始化ThreadLocal时并不会立刻执行,而是先调用get()方法之后再执行相应的代码,如果ThreadLocal有值的话直接返回,如果没有的话才会调用initialValue方法进行初始化
ThreadLocal 的使用场景
- 解决线程安全的问题
- 实现线程级别的数据传递(实现一定程度上的解耦)
传统数据传递方式有两种,调用一个类在return的方式和将参数传递过来,这样会增加代码之间的耦合度
private static ThreadLocal<User> userThreadLocal
= new ThreadLocal();
/**
* 实体类
*/
static class User {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
public static void main(String[] args) {
Storage1 storage1 = new Storage1();
Storage2 storage2 = new Storage2();
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
// 用户登录
User user = new User();
user.setName("张三");
userThreadLocal.set(user);
// 打印用户的信息
storage1.printUserName();
storage2.printUserName();
}
});
thread.start();
}
/**
* 仓储类
*/
static class Storage1 {
public void printUserName() {
User user = userThreadLocal.get();
System.out.println("用户名" + user.getName());
}
}
/**
* 类 2
*/
static class Storage2 {
public void printUserName() {
User user = userThreadLocal.get();
System.out.println("用户名" + user.getName());
}
}
ThreadLocal 注意事项
不可继承性
我们可以使用InheritableThreadLocal来解决不可继承性的问题,但前提两个线程必须是父子线程的关系(或者从属进程的关系),不能实现并列线程之间的数据传输(数据设置和获取)为什么会出现这种情况?因为⽆论是 ThreadLocal 还是 InheritableThreadLocal 本质都是线程本地变量,所以不能跨线程进⾏数据共享也是正常的。
//可以拿到对应的值
// 创建 ThreadLocal
private static ThreadLocal threadLocal =
new InheritableThreadLocal();
public static void main(String[] args) {
// 在主线程里面设置值
threadLocal.set("Java");
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("获取值:" +
threadLocal.get());
}
});
t1.start();
}
//不嫩拿到级别相同线程的值
// 创建 InheritableThreadLocal
private static ThreadLocal threadLocal =
new InheritableThreadLocal();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
// 在主线程里面设置值
threadLocal.set("Java");
System.out.println("线程1 执行值");
}
});
t1.start();
// 线程 1 执行完
t1.join();
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("线程2得到取:" + threadLocal.get());
}
});
t2.start();
}
产生脏读问题
在使⽤线程池的时候,因为线程池会复⽤线程,那么和线程绑定的静态属性也会被复⽤(如:ThreadLocal),所以如果下⼀个线程不执⾏ set 操作或者上⼀个线程不执⾏ remove 操作,那么下⼀个 线程就可以读取到旧值
static ThreadLocal<String> threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
MyThreadLocal myThreadLocal = new MyThreadLocal();
myThreadLocal.show();
}
}, "线程1");
t1.start();
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
MyThreadLocal myThreadLocal = new MyThreadLocal();
myThreadLocal.show();
}
}, "线程2");
t2.start();
}
static class MyThreadLocal {
private static boolean flag = false;
public void show() {
String tname = Thread.currentThread().getName();
if (!flag) {
// 第一次执行
threadLocal.set(tname);
System.out.println(tname + " 设置了:" + tname);
flag = true;
}
System.out.println(tname + "得到了:" + threadLocal.get());
}
}
脏读的解决方案
避免使用静态变量
使用完之后执行remove()操作
内存溢出(配合线程池使用)
当一个线程使用完资源之后,没有释放资源,或者说释放资源不及时就是内存溢出。原因:线程池是长周期声明的,并且ThreadLocal的value(1mb)是强引用不会释放资源。
ThreadLocal实现原理
Java引用类型4种类型
- 强引用Strong Reference: 最为常见,如Object object = newObject(),这样的变量的生命的定义就会产生对该对象的强引用,在Java内存回收时,即使发生OOM,也不会回收该对象
- 软引用Soft Reference:它的引用力度仅次于"强引用",如果内存够用,那么垃圾回收不会考虑回收此引用,在将要发生OOM时才会回收此引用
- 弱引用Weak Reference: 不管内存够不够用,下一次回收都会将此引用的对象回收掉
- 虚引用Phantorn Reference:是一种极弱的引用关系,创建既回收,它可以触发一个垃圾回收的回调
从源码开始分析 set/get ⽅法:
set和get都会先调用getMap(),getMap()会返回当前线程的threadlocals(其实是一个ThreadLocalMap(存储的是所有线程的本地变量)),ThreadLocalMap中有一个Entry[]数组,其中Entry的key是当前ThreadLocal的值(是弱引用),保存的值是value(是强引用),下面根根据图片来理解一下
ThreadLocalMap解决hash冲突的方法?
开放寻址法(因为一般数据量比较小)
以上是关于ThreadLocal知识小结的主要内容,如果未能解决你的问题,请参考以下文章
Android 开发也要掌握的Java知识 -ThreadLocal