ThreadLocal的使用
线程不安全的解决方案:
面对线程不安全的问题共有以下3中解决方案:
1.CPU抢占执行
2.加锁synchronized\lock
3.私有变量-------ThreadLocal线程级别的私有变量
首先提出下面的问题,通过代码理解不同的方式解决线程不安全问题的区别
问题:1000个任务的时间格式化
1.通过线程池完成:
public class App {
// 时间格式化对象
private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("mm:ss");
public static void main(String[] args) throws InterruptedException {
// 创建线程池执⾏任务
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(10, 10, 60,
TimeUnit.SECONDS, new LinkedBlockingQueue<>(1000));
for (int i = 0; i < 1000; i++) {
int finalI = i;
// 执⾏任务
threadPool.execute(new Runnable() {
@Override
public void run() {
// 得到时间对象
Date date = new Date(finalI * 1000);
// 执⾏时间格式化
formatAndPrint(date);
}
});
}
// 线程池执⾏完任务之后关闭
threadPool.shutdown();
}
private static void formatAndPrint(Date date) {
// 执⾏格式化
String result = simpleDateFormat.format(date);
// 打印最终结果
System.out.println("时间:" + result);
}
}
结果如下:
使用SimpleDataFormat的parse()方法,内部有一个Calendar对象,调用SimpleDataFormat的parse()方法会先调用Calendar.clear(),然后调用Calendar.add(),如果一个线程先调用了add()然后另一个线程又调用了clear(),这时候parse()方法解析的时间就出错了。
1… 线程 1 执⾏了 calendar.setTime(date) ⽅法,将⽤户输⼊的时间转换成了后⾯格式化时所需要的时
间;
2. 线程 1 暂停执⾏,线程 2 得到 CPU 时间⽚开始执⾏;
3. 线程 2 执⾏了 calendar.setTime(date) ⽅法,对时间进⾏了修改;
4. 线程 2 暂停执⾏,线程 1 得出 CPU 时间⽚继续执⾏,因为线程 1 和线程 2 使⽤的是同⼀对象,⽽时
间已经被线程 2 修改了,所以此时当线程 1 继续执⾏的时候就会出现线程安全的问题了。
1.解决方案A:加锁,
可以解决线程不安全的问题,但需要排队执行,
private static void formatAndPrint(Date date) {
// 执⾏格式化
String result = simpleDateFormat.format(date);
// 打印最终结果
synchronized (Main.class){
System.out.println("时间:" + result);
}
}
2.解决方案B:ThreadLocal
1.认识ThreadLocal
多线程访问同一个共享变量的时候容易出现并发问题,特别是多个线程对一个变量进行写入的时候,为了保证线程安全,一般使用者在访问共享变量的时候需要进行额外的同步措施才能保证线程安全性。ThreadLocal是除了加锁这种同步方式之外的一种保证一种规避多线程访问出现线程不安全的方法,当我们在创建一个变量后,如果每个线程对其进行访问的时候访问的都是线程自己的变量这样就不会存在线程不安全问题。
ThreadLocal 提供了线程本地的实例。它与普通变量的区别在于,每个使用该变量的线程都会初始化一个完全独立的实例副本。
当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。如下图所示
ThreadLocal的使用方法:
1.set:设置线程独立变量副本,没有set操作容易引起脏数据
2.get:用于获取线程独立变量副本,没有get操作没有意义
3.remove:用于移除线程独立变量副本,没有remove容易引起内存泄露
代码示例:
private static ThreadLocal<String> threadLocal =
new ThreadLocal<>();
public static void main(String[] args) {
// 设置私有变量
Runnable runnable = new Runnable() {
@Override
public void run() {
String name = Thread.currentThread().getName();
System.out.println(name+"存入值"+name);
threadLocal.set(name);
print(name);
}
};
new Thread(runnable,"1号").start();
new Thread(runnable,"2号").start();
}
private static void print(String name){
try{
String result = threadLocal.get();
System.out.println(name+"取出值"+result);
}finally {
threadLocal.remove();
}
}
2.ThreadLocal扩展用法
1.initialValue
private static ThreadLocal<String> threadLocal =
new ThreadLocal(){
@Override
protected Object initialValue() {
System.out.println("执行initialValue");
return "默认值";
}
};
public static void main(String[] args) {
// 设置私有变量
Runnable runnable = new Runnable() {
@Override
public void run() {
print();
}
};
new Thread(runnable,"1号").start();
new Thread(runnable,"2号").start();
}
private static void print(){
try{
String result = threadLocal.get();
System.out.println(result);
}finally {
threadLocal.remove();
}
}
当使用了threadLocal.set方法后,初始化方法便不会执行了
在上述代码的基础上添加了set方法
public static void main(String[] args) {
// 设置私有变量
Runnable runnable = new Runnable() {
@Override
public void run() {
String name = Thread.currentThread().getName();
threadLocal.set(name);
print();
}
};
new Thread(runnable,"1号").start();
new Thread(runnable,"2号").start();
}
为什么执⾏了 ThreadLocal 的 set ⽅法之后,就不会执⾏初始化 initialValue ⽅法了?这要从 ThreadLocal.get() ⽅法的源码实现说起了,因为 initialValue ⽅法在初始化 ThreadLocal 的时候并不会⽴即执⾏,⽽是在调⽤了 get ⽅法时才会执⾏
2.withInitial
public class Main{
// 创建了一个 ThreadLocal
private static ThreadLocal<String> threadLocal =
ThreadLocal.withInitial(new Supplier<String>() {
@Override
public String get() {
System.out.println("执行withInitial");
return "默认值";
}
});
public static void main(String[] args) {
// 设置私有变量
Runnable runnable = new Runnable() {
@Override
public void run() {
String name = Thread.currentThread().getName();
print(name);
}
};
new Thread(runnable,"1号").start();
new Thread(runnable,"2号").start();
}
private static void print(String name){
String result = threadLocal.get();
System.out.println(name+"获得"+result);
}
}
在执行get时才会进行初始化,withInitial的使用有更简洁的方法
private static ThreadLocal<String> threadLocal = ThreadLocal.
withInitial(() -> "默认值");
对比一下
private static ThreadLocal<String> threadLocal =
ThreadLocal.withInitial(new Supplier<String>() {
@Override
public String get() {
System.out.println("执行withInitial");
return "默认值";
}
});
3.使用ThreadLocal解决1000个时间格式化问题
public class Main{
// 创建了一个 ThreadLocal
private static ThreadLocal<SimpleDateFormat> threadLocal =
ThreadLocal.withInitial(()->new SimpleDateFormat("mm:ss"));
public static void main(String[] args) {
// 设置私有变量
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(10,10,60,
TimeUnit.SECONDS,new LinkedBlockingDeque<>(1000));
for (int i = 0; i <1000 ; i++) {
int finalI = i;
threadPool.execute(new Runnable() {
@Override
public void run() {
Date date = new Date(finalI*1000);
formatAndPrint(date);
}
});
}
threadPool.shutdown();
// threadPool.shutdown();
}
private static void formatAndPrint(Date date){
String result = threadLocal.get().format(date);
System.out.println("时间:"+result);
}
}
3.ThreadLocal的特性
1.不可继承性
子线程不能读取到父线程的值;
在创建ThreadLocal时通过InheritableThreadLocal构建可继承的局部线程变量;但仍然不能实现并列线程之间的数据传输
private static ThreadLocal<String> threadLocal =new ThreadLocal<>();
public static void main(String[] args) {
// 设置私有变量
String data = "java";
System.out.println("主线程存入数据"+data);
threadLocal.set(data);
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("子线程读取值"+threadLocal.get());
}
});
t1.start();
}
使用InheritableThreadLocal可解决子线程继承父线程的问题;⽆论是 ThreadLocal 还是 InheritableThreadLocal 本质都是线程本地变量,都不能跨线程进⾏数据共享也是正常的。
private static ThreadLocal<String> threadLocal =new InheritableThreadLocal<>();
2.造成脏数据
在一个线程中读取到了不属于自己的数据;
单个线程使用ThreadLocal不会出现脏读,每个线程都使用的是自己的变量值和ThreadLocal;
在线程池中使用ThreadLocal就会出现脏数据,线程池复用线程导致线程中的静态属性也被复用,从而导致某些方法不能被执行,于是出现了脏数据的问题;
1.因此需要避免使用静态属性,
2.使用remove避免数据重合;
private static ThreadLocal<String> threadLocal =new InheritableThreadLocal<>();
public static void main(String[] args) {
// 设置私有变量
ExecutorService executorService = Executors.newFixedThreadPool(1);
for (int i = 0; i <2 ; i++) {
MyThread myThread = new MyThread();
executorService.execute(myThread);
}
executorService.shutdown();
}
private static class MyThread extends Thread{
private static boolean flag = true;
@Override
public void run() {
if (flag){
threadLocal.set(this.getName()+"session info");
flag=false;
}
System.out.println(this.getName()+":"+threadLocal.get());
}
}
使用remove之后,
private static ThreadLocal<String> threadLocal =new InheritableThreadLocal<>();
public static void main(String[] args) {
// 设置私有变量
ExecutorService executorService = Executors.newFixedThreadPool(1);
for (int i = 0; i <2 ; i++) {
MyThread myThread = new MyThread();
executorService.execute(myThread);
}
executorService.shutdown();
}
private static class MyThread extends Thread{
private static boolean flag = true;
@Override
public void run() {
if (flag){
threadLocal.set(this.getName()+"session info");
flag=false;
}
System.out.println(this.getName()+":"+threadLocal.get());
threadLocal.remove();
}
}
3.造成内存溢出
原因:打开数据并未关闭
内存溢出定义:当一个线程执行完之后,不会释放这个线程所占用内存,或释放内存不及时的情况,都叫做内存溢出,线程不用了但相关内存还没有释放。
分析发生oom-out of memory的原因?
线程池是长生命周期;线程在执行完任务后生命终止(线程相关的资源就会释放);
首先了解一下强弱引用:
ThreadLcoal–>Thread->ThreadLocalMap->Entry[]->Entry-key,value(弱引用),
在释放掉threadLocal的强引用后,map中的value却没有回收,而此value永远不会被访问了 所以存在oom,最好的方法是使用threadLocal的remove方法;
ThreadLocal引用如下所示
实线表示强引用,虚线表示弱引用;
每个thread中都存在一个map(ThreadLocalMap), map的类型是ThreadLocal.ThreadLocalMap. Map中的key为一个threadlocal实例. 这个Map的确使用了弱引用,不过弱引用只是针对key. 每个key都弱引用指向threadlocal. 当把threadlocal实例置为null以后,没有任何强引用指向threadlocal实例,所以threadlocal将会被gc回收. 但是,我们的value却不能回收,因为存在一条从current thread连接过来的强引用. 只有当前thread结束以后, current thread就不会存在栈中,强引用断开, Current Thread, Map, value将全部被GC回收.
结论:就是只要这个线程对象被gc回收,就不会出现内存泄露,但在threadLocal设为null和线程结束这段时间不会被回收的,就发生了我们认为的内存泄露