ThreadLocal实现原理

从了解了多线程后,我们一直在学习如何实现线程安全?可以通过加锁(ReentrantLock、synchronized、)或者采用具有原子性操作的类型定义共享变量。我们创建的变量是可以被任何一个线程访问并修改的。如果想实现每一个线程都有自己的专属本地变量该如何解决呢?

JDK 中自带的ThreadLocal类正是为了解决这样的问题。ThreadLocal被称为线程局部变量,用于在线程中保存数据。由于在ThreadLocal中保存的数据仅属于当前线程,所以该变量对其他线程而言是隔离的,也就是说该变量是当前线程独有的变量。

下面看一个情景:两个线程分别要进行角色分配

public class Demo01 {
	protected static String role;

	public static void main(String[] args) throws InterruptedException {

		Thread t1=new Thread(new Runnable() {
			@Override
			public void run() {
				role="苏妲己";
				show();
				Sample.dosth();
			}
		},"线程t1");

		Thread t2=new Thread(new Runnable() {
			@Override
			public void run() {
				role="姜子牙";
				show();
				Sample.dosth();
			}
		},"线程t2");
		
		t1.start();
		t2.start();
		t1.join();
		t2.join();
		
		System.out.println("THE END!");
	}
	
	// 线程1或线程2
	public static void show() {
		System.out.println("show:"+Thread.currentThread().getName()+"分配角色:"+role);

	}
}
class Sample {
	// 线程1或线程2
	public static void dosth() {
		System.out.println("dosth:"+Thread.currentThread().getName()+"分配角色:"+Demo01.role);

	}
}

按照我们所想的本应该是线程t1分配的角色为苏妲己,t2分配的角色为姜子牙,但是变量role为一个共享变量,线程t1、t2均可访问,在t1访问赋值后t2可能就再次赋值了,对于2个线程来说role是共享的,不独属于任何一个线程,结果就是后执行的线程会覆盖前一个线程的数据。

 为了避免上述情况的发生,我们引入对ThreadLocal类的使用,修改代码如下

public class Demo01 {
    protected static ThreadLocal<String> roleThreadLocal=new ThreadLocal<>();
    public static void main(String[] args) throws InterruptedException {
		Thread t1=new Thread(new Runnable() {
			@Override
			public void run() {
				roleThreadLocal.set("苏妲己");
				show();
				Sample.dosth();
			}
		},"线程t1");
		Thread t2=new Thread(new Runnable() {
			@Override
			public void run() {
				roleThreadLocal.set("姜子牙");
				show();
				Sample.dosth();
			}
		},"线程t2");
		t1.start();
		t2.start();
		t1.join();
		t2.join();
		System.out.println("THE END!");
	}
	public static void show() {
		System.out.println("show:"+Thread.currentThread().getName()+"分配角色:"+roleThreadLocal.get());

	}
}
class Sample {
	// 线程1或线程2
	public static void dosth() {
		System.out.println("dosth:"+Thread.currentThread().getName()+"分配角色:"+Demo01.roleThreadLocal.get());
	}
}

运行结果如下:

通过使用ThreadLocal让线程之间有了隔离,每个线程都有自己的“私域”,不会被别的线程共享,下面我们通过读ThreadLocal的部分源代码让我们更深入的了解一下它的实现原理

public class Thread implements Runnable {
    //......
    //与此线程有关的ThreadLocal值。由ThreadLocal类维护
    ThreadLocal.ThreadLocalMap threadLocals = null;

    //与此线程有关的InheritableThreadLocal值。由InheritableThreadLocal类维护
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
    //......
}

从上面Thread类 源代码可以看出Thread 类中有一个 threadLocals 和 一个 inheritableThreadLocals 变量,它们都是 ThreadLocalMap 类型的变量,ThreadLocalMap为ThreadLocal的静态内部类,我们可以把 ThreadLocalMap 理解为ThreadLocal 类实现的定制化的 HashMap。默认情况下这两个变量都是 null,只有当前线程调用 ThreadLocal 类的 setget方法时才创建它们,实际上调用这两个方法的时候,我们调用的是ThreadLocalMap类对应的 get()set()方法

1、存储数据至当前线程的ThreadLocalMap

public void set(T value) {
    //获取当前请求的线程
    Thread t = Thread.currentThread();
    //取出 Thread 类内部的 threadLocals 变量(ThreadLocalMap)
    ThreadLocalMap map = getMap(t);
    if (map != null)
        // 将需要存储的值放入到这个哈希表中
        map.set(this, value);
    else
        createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

2、从当前线程的ThreadLocalMap中获取数据

public T get() {
    // 获取当前线程的ThreadLocalMap
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);

    // 使用ThreadLocal对象做key,获取数据(Entry类型)
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

我们发现最终的变量是放在了当前线程的 ThreadLocalMap 中,并不是存在 ThreadLocal 上,ThreadLocal 可以理解为只是ThreadLocalMap的封装,传递了变量值。 ThrealLocal 类中可以通过Thread.currentThread()获取到当前线程对象后,直接通过getMap(Thread t)可以访问到该线程的ThreadLocalMap对象。

ThreadLocal与线程Threa及ThreadLocalMap的关系如下图所示:

image.png

 每个Thread中都具备一个ThreadLocalMap,而ThreadLocalMap可以存储以ThreadLocal为 key ,Object 对象为 value 的键值对。ThreadLocalMap部分源码:

static class ThreadLocalMap {
        
        static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
        //数组初始大小
        private static final int INITIAL_CAPACITY = 16;
        //数组
        private Entry[] table;
    }
}

ThreaLocalMap用于存值的数组内部基于一个个Entry对象所实现,它的数组初始容量为16,与我们所了解的HashMap这一点极为相似,下面进入ThreaLocalMap看一下set方法

private void set(ThreadLocal<?> key, Object value) {
            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();
                if (k == key) {
                    e.value = value;
                    return;
                }
                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

两个参数:ThreadLocal类型数据作为key,要存入的数据

1、把用于存储数据的table数组复制,存储长度

2、取key的hashcode与数组长度-1做与运算获取key在哈希表中的位置【同key的hashcode%(数组长度)】扰动函数

3、获取所计算出来的下标在map中的元素值,判断是否为null

        若不为null,则说明当前位置已有元素,获取它的key,继续判断它的key与我传进来的key是否相等,相同则直接覆盖它原来的value并返回。

        若为null,则说明当前位置没有元素,直接添加,同时size++。

上面我们看了向ThreadLocal中存数据和取数据。还有一点要注意:

使用完成后从当前线程的ThreadLocalMap中删除数据,在线程池的线程复用场景中,线程执行完毕时一定要调用remove(),避免在线程被重新放入线程池中时被本地变量的旧状态仍然被保存。

     ThreadLocalMap m = getMap(Thread.currentThread());
     // 使用当前ThreadLocal对象做key,删除数据
     if (m != null)
         m.remove(this);
}

下面思考一下:对于父线程往ThreadLocal中存储的数据,子线程能否获取到呢?

答案是否,因为线程在ThreadLocal.set()存储数据时,实际是往当前线程中的ThreadLocalMap中存,每个线程都有自己的ThreadLocalMap,通过ThreadLocal.get()实际上是使用同一个键ThreadLocal在不同的ThreadLocalMap取值。

但是在实际工作中,有可能需要在父子线程中共享数据的,在这种情况下使用ThreadLocal是行不通的。main方法是在主线程中执行的,相当于父线程。在main方法中开启了另外一个线程,相当于子线程。两个线程对象,各自拥有不同的ThreadLocalMap。应该使用InheritableThreadLocal,它是JDK自带的类,继承了ThreadLocal类。

 

小结:

  • 每个Thread中都具备一个ThreadLocalMap,而ThreadLocalMap可以存储以ThreadLocal为 key ,Object 对象为 value 的键值对.
  • ThreadLocal.get()实际上是使用同一个键ThreadLocal在不同的ThreadLocalMap取值
  • 对于父子线程想要共享使用InheritableThreadLocal

最后特别注意的地方是:一定要在finally代码块中,调用remove()方法清理没用的数据。如果业务代码出现异常,也能及时清理没用的数据。remove()方法中会把Entry中的key和value都设置成null,这样就能被GC及时回收,无需触发额外的清理机制,所以它能解决内存泄露问题。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值