前言
前面的文章里, 我们学习了有关锁的使用, 锁的机制是保证同一时刻只能有一个线程访问临界区的资源, 也就是通过控制资源的手段来保证线程安全, 这固然是一种有效的手段, 但程序的运行效率也因此大大降低. 那么, 有没有更好的方式呢? 答案是有的, 既然锁是严格控制资源的方式来保证线程安全, 那我们可以反其道而行之, 增加更多资源, 保证每个线程都能得到所需对象, 各自为营, 互不影响, 从而达到线程安全的目的, 而 ThreadLocal 便是采用这样的思路.
ThreadLocal 实例
ThreadLocal 翻译成中文的话大概可以说是: 线程局部变量, 也就是只有当前线程能够访问. 它的设计作用是为每一个使用该变量的线程都提供一个变量值的副本, 每个线程都是改变自己的副本并且不会和其他线程的副本冲突, 这样一来, 从线程的角度来看, 就好像每个线程都拥有了该变量.
下面是一个简单的实例:
- public class ThreadLocalDemo {
- static ThreadLocal<Integer> local = new ThreadLocal<Integer>(){
- @Override
- protected Integer initialValue() {
- return 0;
- }
- };
- public static class MyRunnable implements Runnable{
- @Override
- public void run() {
- for (int i = 0;i<3;i++){
- try {
- Thread.sleep(1000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- int value = local.get();
- System.out.println(Thread.currentThread().getName() + ":" + value);
- local.set(value + 1);
- }
- }
- }
- public static void main(String[] args) {
- MyRunnable runnable = new MyRunnable();
- Thread t1 = new Thread(runnable);
- Thread t2 = new Thread(runnable);
- t1.start();
- t2.start();
- }
- }
上面的代码不难理解, 首先是定义了一个名为 local 的 ThreadLocal 变量, 并初识变量的值为 0, 然后是定义了一个实现 Runnable 接口的内部类, 在其 run 方法中对 local 的值做读取和加 1 的操作, 最后是 main 方法中开启两个线程来运行内部类实例.
以上就是代码的大概逻辑, 运行 main 函数后, 程序的输出结果如下:
- Thread-0:0
- Thread-1:0
- Thread-1:1
- Thread-0:1
- Thread-1:2
- Thread-0:2
从结果可以看出, 虽然两个线程都共用一个 Runnable 实例, 但两个线程中所展示的 ThreadLocal 的数据值并不会相互影响, 也就是说这种情况下的 local 变量保存的数据相当于是线程安全的, 只能被当前线程访问.
ThreadLocal 实现原理
那么 ThreadLocal 内部是怎么保证对象是线程私有的呢? 毫无疑问, 答案需要从源码中查找. 回顾前面的代码, 可以发现其中调用了 ThreadLocal 的两个方法 set 和 get, 我们就从这两个方法入手.
先看 set() 的源码:
- public void set(T value) {
- Thread t = Thread.currentThread();
- // 获取线程的 ThreadLocalMap, 返回 map
- ThreadLocalMap map = getMap(t);
- if (map != null)
- map.set(this, value);
- else
- //map 为空, 创建
- createMap(t, value);
- }
- ThreadLocalMap getMap(Thread t) {
- return t.threadLocals;
- }
- void createMap(Thread t, T firstValue) {
- t.threadLocals = new ThreadLocalMap(this, firstValue);
- }
set 的代码逻辑比较简单, 主要是把值设置到当前线程的一个 ThreadLocalMap 对象中, 而 ThreadLocalMap 可以理解成一个 Map, 它是定义在 Thread 类中内部的成员, 初始化是为 null,
ThreadLocal.ThreadLocalMap threadLocals = null;
不过, 与常见的 Map 实现类, 如 HashMap 之类的不同的是, ThreadLocalMap 中的 Entry 是继承于 WeakReference 类的, 保持了对 "键" 的弱引用和对 "值" 的强引用, 这是类的源码:
- static class ThreadLocalMap {
- static class Entry extends WeakReference<ThreadLocal<?>> {
- /** The value associated with this ThreadLocal. */
- Object value;
- Entry(ThreadLocal<?> k, Object v) {
- super(k);
- value = v;
- }
- }
- // 省略剩下的源码
- ....................
- }
从源码中中可以看出, Entry 构造函数中的参数 k 就是 ThreadLocal 实例, 调用 super(k) 表明对 k 是弱引用, 使用弱引用的原因在于, 当没有强引用指向 ThreadLocal 实例时, 它可被回收, 从而避免内存泄露, 那么为何需要防止内存泄露呢? 原因下面会说到.
接着说 set 方法的逻辑, 当调用 set 方法时, 其实是将数据写入 threadLocals 这个 Map 对象中, 这个 Map 的 key 为 ThreadLocal 当前对象, value 就是我们存入的值. 而 threadLocals 本身能保存多个 ThreadLocal 对象, 相当于一个 ThreadLocal 集合.
接着看 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;
- }
- }
- // 设置初识值到 ThreadLocal 中并返回
- return setInitialValue();
- }
- private T setInitialValue() {
- T value = initialValue();
- Thread t = Thread.currentThread();
- ThreadLocalMap map = getMap(t);
- if (map != null)
- map.set(this, value);
- else
- createMap(t, value);
- return value;
- }
get 方法的逻辑也是比较简单的, 就是直接获取当前线程的 ThreadLocalMap 对象, 如果该对象不为空就返回它的 value 值, 否则就把初始值设置到 ThreadLocal 中并返回.
看到这, 我们大概就能明白为什么 ThreadLocal 能实现线程私有的原理了, 其实就是每个线程都维护着一个 ThreadLocal 的容器, 这个容器就是 ThreadLocalMap, 可以保存多个 ThreadLocal 对象. 而调用 ThreadLocal 的 set 或 get 方法其实就是对当前线程的 ThreadLocal 变量操作, 与其他线程是分开的, 所以才能保证线程私有, 也就不存在线程安全的问题了.
然而, 该方案虽然能保证线程私有, 但却会占用大量的内存, 因为每个线程都维护着一个 Map, 当访问某个 ThreadLocal 变量后, 线程会在自己的 Map 内维护该 ThreadLocal 变量与具体实现的映射, 如果这些映射一直存在, 就表明 ThreadLocal 存在引用的情况, 那么系统 GC 就无法回收这些变量, 可能会造成内存泄露.
针对这种情况, 上面所说的 ThreadLocalMap 中 Entry 的弱引用就起作用了.
TheadLocal 与同步机制的区别
最后, 总结一下 ThreadLocal 和同步机制之间的区别吧.
实现机制:
同步机制采用了 "以时间换空间" 的方式, 控制资源保证同一时刻只能有一个线程访问.
ThreadLocal 采用了 "以空间换时间" 的方式, 为每一个线程都提供一份变量的副本, 从而实现同时访问而互不影响, 但因为每个线程都维护着一份副本, 对内存空间的占用会增加.
数据共享:
同步机制是对公共资源做控制访问的方式来保证线程安全, 但资源仍是共享状态, 可用于线程间的通信;
ThreadLocal 是每个线程都有自己的资源 (变量) 副本, 互相之间不影响, 也就不存在共享的说法了.
来源: https://www.cnblogs.com/yeya/p/10212501.html