第五部分 并发~~第19章 同步和协作工具类

77 阅读4分钟

19.5 理解ThreadLocal

本节,我们来探讨一个特殊的概念:线程本地变量。在Java中的实现是类ThreadLocal, 它是什么?有什么用?实现原理是什么?让我们接下来逐步探讨。

19.5.1 基本概念和用法

线程本地变量是说,每个线程都有同一个变量的独有拷贝。这个概念听上去比较难以理解,我们先直接来看类ThreadLocal的用法。ThreadLocal是一个泛型类,接受一个类型参数T, 它只有一个空的构造方法,有两个主要的public方法:

    public T get();
    public void set(T value)

set就是设置值,get就是获取值,如果没有值,返回null, 看上去,ThreadLocal就是一个单一对象的容器,比如:

    public static void main(String[] args){
        ThreadLocal<Integer> local = new ThreadLocal<>();
        local.set(100);
        System.out.println(local.get());
    }

输出为100。那ThreadLocal有什么特殊的呢?特殊发生在有多个线程的时候,看个例子:

    public class ThreadLocalBasic{
        static ThreadLocal<Integer> local = new ThreadLocal<>();
        public static void main(String[] args) throws InterruptedException{
            Thread child = new Tread(){
                @Override
                public void run(){
                    System.out.println("child thred initial: " + local.get());
                    local.set(200);
                    System.out.println("child thread final: " + local.get());
                }
            };
            local.set(100);
            child.start();
            child.join();
            System.out.println("main thread final: " + local.get());
        }
    }

local是一个静态变量,main方法创建了一个子线程child, main和child都访问了local, 程序的输出为:

    child thread initial: null
    child thread final: 200
    main thread final: 100

这说明,main线程对local变量的设置对child线程不起作用,child线程对local变量的改变也不会影响main线程,它们访问的虽然是同一个变量local, 但每个线程都有自己的独立的值,这就是线程本地变量的含义。

除了get/set, ThreadLocal还有两个方法:

    protected T initialValue();
    public void remove()

initialValue用于提供初始值,这是一个受保护方法,可以通过匿名内部类的方式提供,当调用get方法时,如果之前没有设置过,会调用该方法获取初始值,默认实现是返回null。remove删掉当前线程对应的值,如果删掉后,再次调用get, 会再调用initialValue获取初始值。看个简单的例子:

    public class ThreadLocalInit{
        static ThreadLocal<Integer> local = new ThreadLocal<Integer>(){
            @Override
            protected Integer initialValue(){
                return 100;
            }
        };
        public static void main(String[] args){
            System.out.println(local.get());
            local.set(200);
            local.remove();
            System.out.println(local.get());
        }
    }

输出值都是100。

19.5.2 使用场景

ThreadLocal有什么用呢?我们来看三个例子:日期处理、随机数和上下文信息。

1.日期处理

ThreadLocal是实现线程安全的一种方案,比如对于DateFromat/SimpleDateFormat, 我们在介绍日期和时间操作的时候,提到它们是非线程安全的,实现安全的一种方式是使用锁,另一种方式是每次都创建一个新的对象,更好的方式就是使用ThreadLocal, 每个线程使用自己的DateFormat,就不存在安全问题了,在线程的整个使用过程中,只需要创建一次,又避免了频繁创建的开销,示例代码如下:

    public class ThreadLocalDateFormat{
        static ThreadLocal<DateFormat> sdf = new ThreadLocal<DateFormat>(){
            @Override
            protected DateFormat initialValue(){
                return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            }
        };
        
        public static String date2String(Date date){
            return sdf.get().format(date);
        }
        
        public static Date string2Date(String str) throws ParseException{
            return sdf.get().parse(str);
        }
        
    }

需要说明的是,ThreadLocal对象一般都定义为static, 以便于引用。

2.随机数

即使对象是线程安全的,使用ThreadLocal也可以减少竞争,比如,我们在介绍Random类的时候提到,Random是线程安全的,但如果并发访问竞争激烈的话,性能会下降,所以Java并发包提供了类ThreadLocalRandom,它是Random的子类,利用了ThreadLocal,它并没有public的构造方法,通过静态方法获取对象,比如:

    public static void main(String[] args){
        ThreadLocalRandom rnd = ThreadLocalRandom.current();
        System.out.println(rnd.nextInt());
    }

current方法的实现为:

    public static ThreadLocalRandom current(){
        return localRandom.get();
    }

localRandom就是一个ThreadLocal变量:

    private static final TreadLocal<ThreadLocalRandom> localRandom = 
        new ThreadLocal<ThreadLocalRandom>(){
            protected ThreadLocalRandom initialValue(){
                return new ThreadLocalRandom();
            }
        }

3.上下文信息

ThreadLocal的典型用途是提供上下文信息,比如在一个Web服务器中,一个线程执行用户的请求,在执行过程中,很多代码都会访问一些共同的信息,比如请求信息、用户身份信息、数据库连接、当前事务等,它们是线程执行过程中的全局信息,如果作为参数在不同代码间传递,代码会很烦琐,这时,使用ThreadLocal就很方便,所以它被用于各种框架如Spring中。我们看个简单的示例,如代码清单19-6所示。

代码清单19-6 使用ThreadLocal保存上下文信息

    public class RequestContext{
        public static class Request{ //...
        };
        private static ThreadLocal<String> localUserId = new ThreadLocal<>();
        private static ThreadLocal<Request> localRequest = new Thread<>();
        public static String getCurrentUserId(){
            return localUserId.get();
        }
        public static void setCurrentUserId(String userId){
            localUserId.set(userId);
        }
        pubic static Request getCurrentRequest(){
            return localRequest.get();
        }
        public static void setCurrentRequest(Request request){
            localRequest.set(request);
        }   
    }

在首次获取到信息时,调用set方法如setCurrentRequest/setCurrentUserId进行设置,然后就可以在代码的任意其他地方调用get相关方法进行获取了。

19.5.3 基本实现原理

ThreadLocal是怎么实现的呢?为什么对同一个对象的get/set,每个线程都能有自己独立的值呢?我们直接来看代码(基于Java7)。set方法的代码为:

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

它调用了getMap,getMap的代码为:

    ThreadLocalMap getMap(Thread t){
        return t.threadLocals;
    }

返回线程的实例变量threadLocals, 它的初始值为null, 在null时,set调用createMap初始化,代码为:

    void createMap(Thread t, T firstValue){
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

从以上代码可以看出,每个线程都有一个Map, 类型为ThreadLocalMap, 调用set实际上是在线程自己的Map里设置了一个条目,键为当前的ThreadLocal对象,值为value。ThreadLocalMap是一个内部类,它是专门用于ThreadLocal的,与一般的Map不同,它的键类型为WeakReference<ThreadLocal>。我们没有提过WeakReference, 它与Java的垃圾回收机制有关,使用它,便于回收内存,具体我们就不再探讨了。

get方法的代码为:

    public T get(){
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if(map != null){
            ThreadLocalMap.Entry e = map.getEntity(this);
            if(e != null)
                return (T)e.value;
        }
        return setInitialValue();
    }

通过线程访问到Map, 以ThreadLocal对象为键从Map中获取到条目,取其value, 如果Map中没有,则调用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;
    }

initialValue()就是之前提到的提供初始值的方法,默认实现就是返回null。

remove方法的代码也很直接,如下所示:

    public void remove(){
        ThreadLocalMap m = getMap(Thread.currentThread());
        if(m != null)
            m.remove(this);
    }

简单总结下,每个线程都有一个Map,对于每个ThreadLocal对象,调用其get/set实际上就是以ThreadLocal对象为键读写当前线程的Map, 这样,就实现了每个线程都有自己的独立副本的效果。

本章介绍了Java并发包中的一些同步协作工具:

  1. 在读多写少的场景中使用ReentrantReadWriteLock替代ReentrantLock, 以提高性能。
  2. 使用Semaphore限制对资源的并发访问数。
  3. 使用CountDownLatch实现不同角色线程间的同步。
  4. 使用CyclicBarrier实现同一角色线程间的协调一致。

关于ThreadLocal,本章介绍了它的基本概念、用法用途和实现原理,简单总结来说:

  1. ThreadLocal使得每个线程对同一个变量有自己的独立副本,是实现线程安全、减少竞争的一种方案。
  2. ThreadLocal经常用于存储上下文信息,避免在不同代码间来回传递,简化代码。
  3. 每个线程都有一个Map,调用ThreadLocal对象的get/set实际就是以ThreadLocal对象为键读写当前线程的该Map。

至此,关于并发就介绍完了,下一章,让我们一起回顾总结一下。