什么是ThreadLocal
多线程访问同一个共享变量的时候容易出现并发问题,特别是多个线程对一个变量进行写入的时候,为了保证线程安全,一般在访问共享变量的时候需要进行额外的同步措施才能保证线程安全性。
ThreadLocal是除了加锁这种同步方式之外的另一种保证多线程访问变量时的线程安全的方法 ;如果每个线程对变量的访问都是基于线程自己的变量 这样就不会存在线程不安全问题。
在Java的多线程编程中,为保证多个线程对共享变量的安全访问,通常会使用synchronized来保证同一时刻只有一个线程对共享变量进行操作 。这种情况下其实还可以将变量放到ThreadLocal类型的对象中 ,使变量在每个线程中都有独立拷贝,在一个线程中对变量的任何操作都不会影响到其它线程的变量。在很多情况下,ThreadLocal比直接使用synchronized同步机制解决线程安全问题更简单,更方便,同时还能保证程序的性能。
ThreadLocal的实现原理
Java中的ThreadLocal是用哈希表 HashMap 实现的,每个线程里都有一个ThreadLocalMap 属性,里面就以Map的形式存储了多个ThreadLocal对象。当在线程中调用ThreadLocal操作方法时,都会通过当前Thread线程对象拿到线程里的ThreadLocalMap,再通过ThreadLocal对象从ThreadLocalMap中锁定数据实体(ThreadLocalMap.Entry)。
如何使用ThreadLocal
public class ThreadLocalDemo {
private static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>() {
@Override
protected Integer initialValue() {
System.out.println("当前的线程id:" + Thread.currentThread().getId());
return 10;
}
};
public static void main(String[] args) {
System.out.println("~~~~~~~~~~~~主线程~~~~~~~~~~~~~");
System.out.println("在主线程中获取threadLocal的值:" + threadLocal.get());
threadLocal.set(100);
System.out.println("在主线程中再次获取threadLocal的值:" + threadLocal.get());
System.out.println("~~~~~~~~~~~~新线程~~~~~~~~~~~~~");
new Thread(() -> System.out.println("在新的线程中获取threadLocal的值:" + threadLocal.get())).start();
}
}
所以JAVA中ThreadLocal的用途是什么?
典型场景1:每个线程需要一个独享的对象(通常是工具类,典型需要使用的类有SimpleDateFormat和Random)
典型场景2:每个线程内需要保存全局变量(例如在拦截器中获取用户信息),可以让不同方法直接使用,避免参数传递的麻烦。
典型场景1:每个线程需要一个独享的对象
每个Thread内有自己的实例副本,不共享;
举例:SimpleDateFormat。(当多个线程共用这样一个SimpleDateFormat,但是这个类是不安全的)
2个线程分别用自己的SimpleDateFormat,这没问题; 后来延伸出10个,那就有10个线程和10个SimpleDateFormat,这虽然写法不优雅,但勉强可以接受 但是当需求变成了1000,那么必然要用线程池,消耗内存太多; 但是每一个SimpleDateFormat我们都需要创建一遍,那么太耗费new对象了,改成static共用的,所有线程都共用一个simpleDateFormat对象,但这是线程不安全的,容易出现时间一致的情况,在调用的时候,可加锁来解决,但还是不优雅; 用ThreadLocal来解决该问题,给每个线程分配一个simpledateformat,由此可见这个threadlocal是线程安全的;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadLocalNormalUsage05 {
public static ExecutorService threadPool = Executors.newFixedThreadPool(10);
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000; i++) {
int finalI = i;
threadPool.submit(new Runnable() {
@Override
public void run() {
String date = new ThreadLocalNormalUsage05().date(finalI);
System.out.println(date);
}
});
}
threadPool.shutdown();
}
public String date(int seconds) {
Date date = new Date(1000 * seconds);
SimpleDateFormat dateFormat = ThreadSafeFormatter.dateFormatThreadLocal2.get();
return dateFormat.format(date);
}
}
class ThreadSafeFormatter {
public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>() {
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
}
};
public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal2 = ThreadLocal
.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
}
典型场景2:当前用户信息需要被线程内所有方法共享
一个比较繁琐的解决方案是把user作为参数层层传递,从service-1()传到service-2(),以此类推,但是这样做会导致代码冗余且不易维护。 进阶点就是userMap来保存,但是当多线程同时工作时,需要保证线程安全,需要用synchronized,或者concurrenthashmap,但无论用什么,都会对性能有所影响
每个线程内需要保存全局变量,可以让不同方法直接使用,避免参数传递的麻烦
用ThreadLocal保存一些业务内存(用户权限信息,从用户系统获取到的用户名、userId等) 这些信息在同一个线程内相同,但是不同的线程使用的业务内容是不相同的 在线程生命周期内,都通过这个静态ThreadLocal实例的get()方法取得自己set过的那个对象,避免了将这个对象作为参数传递的麻烦
public class ThreadLocalNormalUsage06 {
public static void main(String[] args) {
new Service1().process("");
}
}
class Service1 {
public void process(String name) {
User user = new User("超哥");
UserContextHolder.holder.set(user);
new Service2().process();
}
}
class Service2 {
public void process() {
User user = UserContextHolder.holder.get();
ThreadSafeFormatter.dateFormatThreadLocal.get();
System.out.println("Service2拿到用户名:" + user.name);
new Service3().process();
}
}
class Service3 {
public void process() {
User user = UserContextHolder.holder.get();
System.out.println("Service3拿到用户名:" + user.name);
UserContextHolder.holder.remove();
}
}
class UserContextHolder {
public static ThreadLocal<User> holder = new ThreadLocal<>();
}
class User {
String name;
public User(String name) {
this.name = name;
}
}
注意点:
强调的是同一个请求内(同一个线程内)不同方法见的共享; 不需重写initialValue()方法,但是必须手动调用set()方法
ThreadLocal方法
-
initialValue 。 在ThreadLocal第一次get的时候把对象给初始化出来,对象的初始化时机可以由我们控制。 -
set 。如果需要保存到ThreadLocal里面的对象的生成时机不由我们随意控制。例如拦截器生成的用户信息,用ThreadLocal.set直接放到ThreadLocal当中。
|