zoukankan      html  css  js  c++  java
  • ThreadLocal使用和原理简析

     1. 解决共享资源冲突

      对于并发工作,需要某种方式来防止两个任务同时访问相同的资源,至少在关键阶段不能出现这种冲突情况。

       方法之一就是当资源被一个任务使用时,在其上加锁。第一个访问某项资源的任务必须锁定这项资源,使其他任务在其被解锁之前,就无法访问它了,而在其被解锁之时,另一个任务就可以锁定并使用它,以此类推。Java中的synchronized、ReentrantLock就属于这种方式,关于这部分,前面有专门撰文详述:

      第二种方式是根除对变量的共享。线程本地存储是一种自动化机制,可以为使用相同变量的每个不同的线程都创建不同的存储。因此,如果你有5个线程都要使用变量x所表示的对象,那线程本地存储就会生成5个用于x的不同的存储块。它使得你可以将状态与线程关联起来。创建和管理线程本地存储可以由java.lang.ThreadLocal类来实现。本文我们就来学习ThreadLocal的用法,并且从源码层面探究一下其实现原理。

       按照官方的说法:

    • ThreadLocal可以让线程拥有自己独享的变量,就是说多个线程共享同一个ThreadLocal对象,但是每个线程都可以通过ThreadLocal的get方法获取或set方法设置属于该线程的变量副本,变量只属于该线程并且其他线程无关;
    • ThreadLocal对象一般作为类的private static属性,可以用来为线程设置一些状态信息比如userId或者transactionId;

    2. ThreadLocal使用

      到这里,如果之前没有使用过ThreadLocal,可能对于ThreadLocal的作用依然不清楚,我们通过官方的一个示例熟悉一下吧(如果之前使用过ThreadLocal的可以直接跳过此处):

    public class ThreadLocalId {
      
      private final static AtomicInteger nextId = new AtomicInteger(0);
      
      private final static ThreadLocal<Integer> threadId = new ThreadLocal<Integer>() {
        @Override
    // 重写ThreadLocal中的方法,用于如果没有通过set设置值时,第1次通过get来获取值时会调用这个方法生成初始值,可以重写该方法来指定初始值生成规则
    protected Integer initialValue() { return nextId.getAndIncrement(); } }; public static Integer get() { return threadId.get(); } public static void main(String[] args) { for(int i = 0; i < 5; i++) { new Thread(new Runnable() { @Override public void run() { int id = ThreadLocalId.get(); System.out.println("thread:" + Thread.currentThread().getName() + ",threadId-->" + id); threadId.set(id); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("thread:" + Thread.currentThread().getName() + ",threadId-->" + threadId.get()); } }).start(); } } } // 输出结果 thread:Thread-0,threadId-->0 thread:Thread-1,threadId-->1 thread:Thread-2,threadId-->2 thread:Thread-3,threadId-->3 thread:Thread-4,threadId-->4 thread:Thread-4,threadId-->4 thread:Thread-2,threadId-->2 thread:Thread-1,threadId-->1 thread:Thread-0,threadId-->0 thread:Thread-3,threadId-->3

      如上,开启5个线程,每个线程都调用同一个ThreadLocal对象threadId的get方法获取一个id值作为线程标识,并通过set方法保存到ThreadLocal中,然后再通过get方法来获取,从输出结果上我们可以看,每个线程虽然都是操作的同一个ThreadLocal对象,但是它们获取到的值并没有被其它线程覆盖,都是自己set进去的值。这就是ThreadLocal的作用:提供线程本地变量,这样一来就不会受到其他线程的影响了,从而可以保证线程安全。那ThreadLocal又是如何做到的呢?我们来看一下源码:

     3. 线程本地存储

      我们先从其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;
            }
        }
        return setInitialValue();
    }
    
    ThreadLocalMap getMap(Thread t) {
       // ThreadLocalMap作为Thread的一个属性
    return t.threadLocals; }

      get方法的主要逻辑如下:

    • 先获取当前线程t;
    • 然后获取ThreadLocalMap;
    • 如果map不为空,则以当前对象(ThreadLocal对象)为key获取value;
    • 如果map为空,则执行初始化操作;

      在第2步中,从哪里获取map?我们可以看到是从当前线程直接获取的(ThreadLocal作为Thread类的一个属性),也就是说这个map是属于当前线程的,而我们想要保存的值是存在这个map中的,这就是ThreadLocal的魔法所在,通过线程本地保存的方式来实现线程之间状态的互不干扰。

      好,接下来我们看看如果是第一次调用get时ThreadLocalMap如果还没有的话是如何初始化ThreadLocalMap并生成初始值的:

    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;
    }
    
    // 通过这个方法生成初始值,可以重写该方法来指定生成初始值规则
    protected T initialValue() { return null; }
    // new一个ThreadLocalMap
    void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); }

      setInitialValue的主要逻辑如下:

    • 首先通过initialValue方法生成初始值;
    • 然后获取ThreadLocalMap;
    • 如果map不为空,则将第1步生成的值set进去,以当前对象(ThreadLocal对象)为key;
    • 如果map为空,则new一个ThreadLocalMap出来;
    • 返回生成的初始值;

       现在我们知道了ThreadLocal的get方法是从一个保存在线程本地(就是Thread.currentThread()获取到的Thread实例对象中)的一个叫threadLocals的ThreadLocalMap的数据结构中,这是一个定制化的hash类型的map,专门用于保存线程本地变量。现在我们再看一下ThreadLocal是如何保存元素的:

    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

      逻辑比较简单:

    • 获取当前线程;
    • 从当前线程获取ThreadLocalMap;
    • 如果map不为空,则以当前ThreadLocal对象为key保存到ThreadLocalMap中;
    • 如果map为空,则初始化ThreadLocalMap;

       好了,现在我们知道ThreadLocal是通过将变量保存在线程本地来实现线程之间相互隔离的,可以结合下图一起理解。

    4. 总结

    •  Java中通过java.lang.ThreadLocal类来实现创建和管理线程本地存储;
    • 线程本地存储是指Thread类中的threadLocals,类型为ThreadLocalMap,其类定义在ThreadLocal中;
    • 每个线程通过同一个ThreadLocal进行set和get变量时,实际上是以这个ThreadLocal为key进行存储和获取数据,线程之间共享的是ThreadLocal这个对象,但是数据是保存在线程各自本地;

      ThreadLocalMap是保存数据的关键,它的内部原理又是怎样的呢?ThreadLocal又存在什么问题呢?请看下文。。。

  • 相关阅读:
    CentOS下NFS服务器安装及配置实例
    appserver安装常见的问题
    java程序链接到sql server数据库
    用C# ASP.net将数据库中的数据表导出到Excel中
    看 《精通CSS》 的读书笔记
    CSS 布局练习
    CSS 学习&测试记录
    兼容 IE6 下Tbody 的 innerHTML 只读无法赋值的问题
    IE6 中 select 隐藏不了的问题
    setInterval、ajax 并用引发的内存漏泄
  • 原文地址:https://www.cnblogs.com/volcano-liu/p/10681399.html
Copyright © 2011-2022 走看看