关于
线程池为学习 JUC 的第二章,持续更新,直到完结 。如果您感兴趣,可以前往本人的 JUC 标签中。如果您有问题或者本人这里写的不对、不足,欢迎评论留言,感谢!!
第二章 ThreadLocal1、两大使用场景
小伙伴们看完 两大使用场景 后或许有些疑惑,请阅读下面的“ 3 、重要方法”内容,可能会对您有所帮助。
1、线程需要一个独享的对象(例如工具类,典型需要使用的类有 SimpleDateFormat 和 Random)。
1)并发使用静态工具类是有很大风险的,此时可以使用 ThreadLocal 为每个线程都制作一个独享的对象;
2)使用线程池加上 ThreadLocal 可以节省资源,不需要创建更多的对象浪费资源。
示例:
public class ThreadLocalNormal { public static ExecutorService service = Executors.newFixedThreadPool(5); public String dateFormat(int seconds){
//每个线程只会触发一次下面的 initialValue() SimpleDateFormat format = ThreadSafeFormatter.threadLocal.get(); return format.format(new Date(seconds* 1000)); } public static void main(String[] args) { //使用此方法就不需要创建 1000 个 SimpleDateFormat 对象了,有多少个线程,就创建多少个 SimpleDateFormat 对象就够了 for (int i = 0; i < 1000; i++) { int finalI = i; service.submit(() -> { System.out.println(new ThreadLocalNormal().dateFormat(finalI)); }); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } } service.shutdown(); } } class ThreadSafeFormatter { public static ThreadLocal<SimpleDateFormat> threadLocal = new ThreadLocal<SimpleDateFormat>(){ @Override //这个函数是初始化作用 protected SimpleDateFormat initialValue() { return new SimpleDateFormat("yy-mm-dd hh:mm:ss"); } }; }
2、线程内需要保存全局变量(例如在拦截器中获取用户信息),可以让不同方法直接使用,避免参数传递的麻烦。
1)例如用 ThreadLocal 保存一些业务内容(用户权限信息、从用户系统获取到的用户名、user ID 等),就不需要把一个信息从 service1 传递到 service2 在传递 service3 .......
2)每个线程使用 ThreadLocal 保存的内容是不共享的,自己独有的。
示例1:
/** * 避免传递参数的麻烦 */ public class ThreadLocalNormal3 { public static void main(String[] args) { ExecutorService executorService = Executors.newFixedThreadPool(2); for (int i = 0; i < 2; i++) { int j = i; executorService.execute(() -> { Service1 service1 = new Service1(); service1.process("张三"+j,"test"+j); }); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } } executorService.shutdown(); } } class Service1 { public void process(String name,String password){ User user = new User(name,password); UserContextHolder.holder.set(user); new Service2(); } } class Service2 { public Service2(){ System.out.println(Thread.currentThread().getName()+"---我是Service2:" + UserContextHolder.holder.get()); new Service3(); } } class Service3 { public Service3(){ System.out.println(Thread.currentThread().getName()+"---我是Service3:" + UserContextHolder.holder.get()); } } class UserContextHolder { public static ThreadLocal<User> holder = new ThreadLocal<>(); } class User{ public String name; public String password; public User(String name, String password) { this.name = name; this.password = password; } @Override public String toString() { return "User{" + "name='" + name + '\'' + ", password='" + password + '\'' + '}'; } }
运行结果:
示例二:
public class ThreadLocalNormal3 { public static ThreadLocal local = new ThreadLocal(); public static void main(String[] args) throws InterruptedException { local.set("我是主线程"); new Thread(() -> { local.set("我是线程一"); System.out.println(local.get()); },"线程一").start(); Thread.sleep(1000); System.out.println(local.get()); } }
运行结果:
2、使用 ThreadLocal 的好处
1、达到线程安全
1)例如上面的场景一,我们可以把共享的静态对象变成每个线程自己独享的对象。
2、不需要加锁,提高执行效率
1)既然是每个线程自己所独享的资源,线程就是安全的,不需要加锁。
3、更高效地利用内存、节省开销
1)如上面场景一代码以及内容所示。
4、免去传参的繁琐:无论是场景一的工具类,还是场景二的用户信息,都可以在任何地方直接通过 ThreadLocal 拿到,再也不需要每次都传递同样的参数。ThreadLocal 使得代码耦合度耕地,更优雅
3、重要方法
1、搞清楚 Thread、ThreadLocal、ThreadLocalMap 三者之间的关系:
如上图所示:
1)每个 Thread 对象中持有一个 ThreadLocalMap 成员变量;
2)TreadLocalMap 是以 ThreadLocal 为 key 以独享的资源为 value;
3)因为一个 Thread 可以有多个不同的独享资源 ,所以一个 ThreadLocalMap 中是以 ThreadLocal 为 key 来标识每一个资源;
4)在线程中,可以使用 ThreadLocal.get() 来获取相应的资源。
2、T initialValue() :初始化
1)该方法会返回当前线程对应的“初始值”(如果没有重写该方法,则初始值为 null),这是一个延迟加载的方法,只有在第一次调用 get 的时候,才会触发(除非.......在 3)叙述.....);
2)当前线程第一次使用 get 方法时,就会调用此方法,除非线程先前调用了 set 方法,在这种情况下,就不会调用该线程所对应自己的 initialValue 方法;
(以上两点正是对应了以上写的两种场景示例代码)
3)通常,每个线程最多调用一次此方法,但如果已经调用了 remove() 后,在调用 get() ,则可以再次调用此方法;
4)如果不重写本方法,这个方法会返回 null。一般使用匿名内部类的方法来重写 initialValue() ,以便在后续使用中可以初始化副本对象。
3、void set(T t) :为这个线程设置一个新值,与setinitialValue() 很类似
源码:
public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); //如果 map 不为空(该 map 可以存储多个不同的 ThreadLocal),则更改目标 ThreadLocal 所对应的 value(this 为当前的 ThreadLocal) //注意:这个 map 以及 map 中的 key 和 value 都是保存在线程中的 if (map != null) map.set(this, value); //如果为空,则存入 else createMap(t, value); }
4、T get() :得到 ThreadLocalMap 中对应的 ThreadLocal 的值 。如果是首次调用 get(),则会调用 initialize 来得到这个值
1)get 方法是先取出当前线程的 ThreadLocalMap,然后调用 map.getEntry 方法,把本 ThreadLocal 的引用作为参数传入,取出 map 中属于本 ThreadLocal 的 value。
源码:
5、void remove() :删除 ThreadLocalMap 里面所对应的 key 以及 value(下面代码中,删除所对应的 key 为 local 对象)
1)remove() 在上面没有做演示,在这里简单的做两个示例:
示例一:
public class ThreadLocalNormal3 { public static ThreadLocal local = new ThreadLocal(); public static void main(String[] args) throws InterruptedException { IsNull(); } public static void IsNull() throws InterruptedException { local.set("我是主线程"); local.remove(); Thread.sleep(1000); System.out.println("IsNull 方法结果:"+local.get()); } }
示例二:
public class ThreadLocalNormal3 { public static ThreadLocal local = new ThreadLocal(); public static void main(String[] args) throws InterruptedException { IsNull(); } public static void IsNull() throws InterruptedException { local.set("我是主线程"); local.remove(); local.set("重新 set"); Thread.sleep(1000); System.out.println("IsNull 方法结果:"+local.get()); } }
源码:
6、两种场景殊途同归
- setInitialValue 和直接 set 最后都是利用 map.set() 来设置;
- 也就是说,最后都会对应到 ThreadLocalMap 的一个 Entry,只不过是起点和入口不一样
7、演示:一个线程,一个 ThreadLocalMap,两个 ThreadLocal
public class ThreadLocalDemo { //一个主线程 public static void main(String[] args) throws InterruptedException { //设置两个ThreadLocal ThreadLocal local1 = new ThreadLocal(); ThreadLocal local2 = new ThreadLocal(); //分别为两个 ThreadLocal 设置 value local1.set("我是 local1"); local2.set("我是 local2"); Thread.sleep(1000); //底层会自动的将这两个 ThreadLocal 以及相应的 value 放入主线程的 ThreadLocalMap 中 //输出 ThreadLocalMap 中的这两个 ThreadLocal 对应的 value System.out.println(local1.get()); //我是 local1 System.out.println(local2.get()); //我是 local2 } }
4、ThreadLocalMap 类
1、ThreadLocalMap 类是每个线程 Thread 类里面的变量,里面最重要的是一个键值对数组 Entry[] table。可以认为是一个 Map,键值对:
- 键:这个 ThreadLocal;
- 值:实际需要的成员变量,比如 user 或者 simpleDateFormat 对象。
2、ThreadLocalMap 这里采用的是线性探测法,也就是说,如果发生冲突,就继续找下一个空位置,而不是用链表拉链
5、其他
1、注意空指针异常,原理如代码中的注释:
在进行 get 之前,必须先 set,否则可能报空指针异常,以下代码报空指针异常
public class ThreadLocalNPE { ThreadLocal<Long> longThreadLocal = new ThreadLocal<>(); public void set() { longThreadLocal.set(Thread.currentThread().getId()); } //上面定义的是 ThreadLocal<Long>,这里如果返回值是 long,那么 get 到的值要做拆箱,如果拿 null 去拆箱,会报空指针异常 public long get(){ return longThreadLocal.get(); } public static void main(String[] args) { ThreadLocalNPE threadLocalNPE = new ThreadLocalNPE(); System.out.println(threadLocalNPE.get()); } }
2、共享对象问题
1)如果在每个线程中 ThreadLocal.set() 进去的东西本来就是多线程共享的同一个对象,比如 static 对象,那么多个线程的 ThreadLocal.get() 取得的还是这个共享对象本身,还是有并发访问问题;
2)如果可以不适用 ThreadLocal 就解决问题,那么不要强行使用;
-
- 例如在任务数很少的时候,在局部变量中可以新建对象就可以解决问题,那么就不需要使用到 ThreadLocal。
3)优先使用框架的支持,而不是自己创造。
-
- 例如在 Spring 中,如果可以使用 RequestContextHolder,那么就不需要自己维护 ThreadLocal,因为自己可能会忘记调用 remove() 等,造成内存泄漏。