• ThreadLocal


      除了控制资源的访问外,我们还可以通过增加资源来保证所有对象的线程安全。比如,让100个人填写个人信息表,如果只有一支笔,大家就得挨个填,对于管理人员来说,必须保证大家不会去哄抢仅存的一支笔,否则,谁也填不完。当然我们还可以从另外一个角度出发,就是准备100支笔,人手一支,那么所有人都可以各自为营,很快就能完成表格的填写工作。

      如果说锁是一种思路,那么ThreadLocal就是第二种思路。

    ThreadLocal的简单使用

      ThreadLocal是线程的局部变量,也就是说,只有当前线程才能访问。既然是只有当前线程才能访问的数据,自然就是线程安全的,下面是一个简单的示例,了解ThreadLocal的基本用法:

     1 public class ThreadLocalDemo {
     2     static ThreadLocal<SimpleDateFormat> threadLocal = new ThreadLocal<SimpleDateFormat>();
     3 
     4     public static class ParseDate implements Runnable{
     5         int i = 0;
     6         public ParseDate(int i){
     7             this.i = i;
     8         }
     9 
    10         @Override
    11         public void run() {
    12             try {
    13                 if (threadLocal.get() == null){
    14                     threadLocal.set(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
    15                 }
    16                 Date t = threadLocal.get().parse("2018-10-16 09:13:"+ i%60);
    17                 System.out.println(i + ":" + t);
    18             }catch (ParseException e) {
    19                 e.printStackTrace();
    20             }
    21         }
    22     }
    23     //测试
    24     public static void main(String[] args){
    25         ExecutorService es = Executors.newFixedThreadPool(10);
    26         for (int i = 0;i < 1000;i++){
    27             es.execute(new ParseDate(i));
    28         }
    29     }
    30 }

    上述代码第13,14行,如果当前线程不持有SimpleDateformat对象实例。那么就新建一个并把它设置到当前线程中,如果持有,则直接使用。

      从上面的例子可以看出,为每一个线程人手分配一个对象的工作并不是由ThreadLocal来完成的,而是需要在应用层面保证的。如果在应用上为不同的线程分配了相同的对象实例,那么ThreadLocal也不能保证线程安全。ThreadLocal只是起到了容器的作用。

    ThreadLocal的实现原理

    ThreadLocal又是如何保证这些对象只被当前线程所访问呢?下面来看看ThreadLocal的内部实现。

    set():

    1 public void set(T value) {
    2         Thread t = Thread.currentThread();
    3         ThreadLocalMap map = getMap(t);
    4         if (map != null)
    5             map.set(this, value);
    6         else
    7             createMap(t, value);
    8     }

    在set时,首先获得了当前线程对象,然后通过getMap()拿到当前线程的ThreadLocalMap,并将值设置到ThreadLocalMap中。而ThreadLocalMap可以理解为一个Map(虽然不是,但是可以把它简单的理解成HashMap),但是它是定义在Thread内部的成员。

    ThreadLocal.ThreadLocalMap threadLocals = null;

    上述代码是在Thread类中定义的。可以看出设置到ThreadLocal中的数据,就是写入到了threadLocals这个Map中。其中,key为ThreadLocal当前对象,value就是我们需要的值。而threadLocals本身就保存了当前自己所在线程所有的“局部变量”,也就是一个ThreadL变量的集合。

    get():

     1 public T get() {
     2         Thread t = Thread.currentThread();
     3         ThreadLocalMap map = getMap(t);
     4         if (map != null) {
     5             ThreadLocalMap.Entry e = map.getEntry(this);
     6             if (e != null) {
     7                 @SuppressWarnings("unchecked")
     8                 T result = (T)e.value;
     9                 return result;
    10             }
    11         }
    12         return setInitialValue();
    13     }

    首先,get()方法也是先取得当前线程的ThreadLocalMap对象,然后,通过将自己作为key取得内部的实际数据。否则通过setInitialValue()方法,返回null。

    在了解了ThreadLocal的set()和get()方法后,就会想到一个问题。那就是这些变量是维护在Thread类内部的,这也就意味着只要线程不退出,对象的引用将一直存在。

    线程退出

    当线程退出时,Thread类会进行一些清理工作,其中就包括清理ThreadLocalMap,下面方法就是线程退出的源码:

     1 private void exit() {
     2         if (group != null) {
     3             group.threadTerminated(this);
     4             group = null;
     5         }
     6         /* Aggressively null out all reference fields: see bug 4006245 */
     7         target = null;
     8         /* Speed the release of some of these resources */
     9         threadLocals = null;
    10         inheritableThreadLocals = null;
    11         inheritedAccessControlContext = null;
    12         blocker = null;
    13         uncaughtExceptionHandler = null;
    14     }

    可以看出在线程退出时,会将threadLocals设置为null,这样会让GC在下一次回收时,回收这个引用。

    注意:

      如果我们使用线程池,那就意味着当前线程未必会退出(比如固定大小的线程池,线程总是存在),如果这样,将一些大的对象设置到了ThreadLocal中(实际上是保存在ThreadLocalMap中),可能会导致内存泄露的可能(意思就是:你设置了大的对象到ThreadLocal中,但是不清理它,在你使用几次后,这个对象就不再有用了,但是它却无法被回收),此时,如果你希望及时回收对象,最好使用ThreadLocal.remove()方法将这个变量移除。还有一种方法是将ThreadLocal的变量手动设置为null(比如:threadLocal = null),那么这个ThreadLocal对应的局部变量会更加容易被垃圾回收器发现,从而加速回收。

    ThreadLocal的回收机制

      要了解ThreadLocal的回收机制,需要更进一步了解ThreadLocal.ThreadLocalMap的实现。之前说过,ThreadLocalMap类似于HashMap,其实,更准确的说是,ThreadLocalMap更加类似于WeakHashMap。

      ThreadLocalMap的实现使用了弱引用。弱引用是比强引用弱得多的引用。Java虚拟机在垃圾回收时,若发现弱引用,就会立即回收。ThreadLocalMap是由一系列的Entry构成的,下面看一下Entry源码:

    1 static class Entry extends WeakReference<ThreadLocal<?>> {
    2             /** The value associated with this ThreadLocal. */
    3             Object value;
    4 
    5             Entry(ThreadLocal<?> k, Object v) {
    6                 super(k);
    7                 value = v;
    8             }
    9         }

    可以看出,每一个Entry都是WeakReference<ThreadLocal>。这里的参数k就是Map的key,v就是Map的value。其中k就是ThreadLocal的实例,作为弱引用使用(super(k)就是调用了WeakReference的构造函数),因此,虽然这里使用ThreadLocal作为Map的key,但是实际上它并不是真的持有ThreadLocal的引用。而当ThreadLocal的外部强引用被回收时,ThreadLocalMap中的key就会变为null。当系统进行ThreadLocalMap清理时(比如将新的变量加入表中,就会自动进行一次清理),就会将这些垃圾数据回收。下面看个例子:

     1 public class ThreadLocalDemo_GC {
     2     static volatile ThreadLocal<SimpleDateFormat> threadLocal = new ThreadLocal<SimpleDateFormat>(){
     3         @Override
     4         protected void finalize() throws Throwable{
     5             System.out.println(this.toString() + " is GC!");
     6         }
     7     };
     8 
     9     static volatile CountDownLatch cd = new CountDownLatch(100);
    10     public static class ParseDate implements Runnable{
    11         int i = 0;
    12         public ParseDate(int i){
    13             this.i = i;
    14         }
    15 
    16         @Override
    17         public void run() {
    18             try {
    19                 if (threadLocal.get() == null){
    20                     threadLocal.set(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"){
    21                         @Override
    22                         protected void finalize() throws Throwable {
    23                             System.out.println(this.toString() + " is GC");
    24                         }
    25                     });
    26                     System.out.println(Thread.currentThread().getId() + ": create SimpleDateFormat");
    27                 }
    28                 Date date = threadLocal.get().parse("2018-10-16 11:27:" + i%60);
    29             } catch (ParseException e) {
    30                 e.printStackTrace();
    31             }finally {
    32                 cd.countDown();
    33             }
    34         }
    35     }
    36     //测试
    37     public static void main(String[] args) throws InterruptedException {
    38         ExecutorService es = Executors.newFixedThreadPool(10);
    39         for (int i =0 ;i < 100;i++){
    40             es.execute(new ParseDate(i));
    41         }
    42         cd.await();
    43         System.out.println("first mission complete!");
    44         threadLocal = null;
    45         System.gc();
    46         System.out.println("first GC complete!");
    47         //在设置ThreadLocal的时候,会清除ThreadLocalMap中无效对象
    48         threadLocal = new ThreadLocal<SimpleDateFormat>();
    49         cd = new CountDownLatch(100);
    50         for (int i = 0;i < 100;i++){
    51             es.execute(new ParseDate(i));
    52         }
    53         cd.await();
    54         System.out.println("second mission complete!");
    55         threadLocal = null;
    56         System.gc();
    57         System.out.println("second GC complete!");
    58         es.shutdown();
    59     }
    60 }

    输出结果:

    19: create SimpleDateFormat
    20: create SimpleDateFormat
    16: create SimpleDateFormat
    11: create SimpleDateFormat
    17: create SimpleDateFormat
    13: create SimpleDateFormat
    15: create SimpleDateFormat
    14: create SimpleDateFormat
    18: create SimpleDateFormat
    12: create SimpleDateFormat
    first mission complete!
    first GC complete!
    CurrentJava.ThreadLocalDemo_GC$1@46147084 is GC!
    11: create SimpleDateFormat
    second mission complete!
    second GC complete!

    在上述代码的第44行,将threadLocal设置为null,第一次GC的时候并没有将threadLocal回收,但是当我们再次使用threadLocal时(代码第48行),将threadLocal中无效的对象就被清理了。当我们第二次将threadLocal设置true的时候,从输出结果中可以看出,threadLocal并没有被清除。所以并不是将threadLocal设置为null,就一定会被垃圾回收器回收,只是增加了回收的可能而已,但也是非常必要的。

    ThreadLocal 的性能

     为每一个线程分配一个独立的对象对系统性能也许是有帮助的。当然,这不是一定的。这取决于共享对象的内部逻辑。如果共享对象对于竞争的处理容易引起性能损失,这样我们就可以考虑使用ThreadLocal为每个线程分配单独的对象。下面举一个典型的案例:多线程下产生随机数

     1 public class RandomDemo {
     2     public static final int GEN_COUNT = 10000000;//每个线程要产生的随机数
     3     public static final int THREAD_COUNT = 4;//线程数
     4     static ExecutorService es = Executors.newFixedThreadPool(THREAD_COUNT);//线程数量为4的固定线程池
     5     public static Random random = new Random(123);//共享的Random实例用于产生随机数
     6 
     7     //ThreadLocal 封装了Random
     8     public static ThreadLocal<Random> tRnd = new ThreadLocal<Random>(){
     9         @Override
    10         protected Random initialValue() {
    11             return new Random(123);
    12         }
    13     };
    14 
    15     public static class RndTask implements Callable<Long>{
    16         private int mode = 0;
    17         public RndTask(int mode){
    18             this.mode = mode;
    19         }
    20         //mode=0:共享一个Random实例;mode=1:每个线程分配一个实例
    21         public Random getRandom(){
    22             if (mode == 0){
    23                 return random;
    24             }else if (mode == 1){
    25                 return tRnd.get();
    26             }else {
    27                 return null;
    28             }
    29         }
    30         //每个线程产生100000个随机数,完成工作后,记录并返回所消耗的时间
    31         @Override
    32         public Long call() {
    33             long b = System.currentTimeMillis();
    34             for (long i = 0;i < GEN_COUNT;i++){
    35                 getRandom().nextInt();
    36             }
    37             long e = System.currentTimeMillis();
    38             System.out.println(Thread.currentThread().getName() + " spend " + (e - b) + "ms");
    39             return e-b;
    40         }
    41     }
    42     //测试
    43     public static void main(String[] args) throws ExecutionException, InterruptedException {
    44         Future<Long>[] futs = new Future[THREAD_COUNT];
    45         for (int i = 0;i < THREAD_COUNT;i++){
    46             futs[i] = es.submit(new RndTask(0));
    47         }
    48         long totalTime = 0;
    49         for (int i = 0;i < THREAD_COUNT;i++){
    50             totalTime += futs[i].get();
    51         }
    52         System.out.println("多线程访问同一个Random实例:"+ totalTime + "ms");
    53         //ThreadLocal情况
    54         for (int i = 0;i < THREAD_COUNT;i++){
    55             futs[i] = es.submit(new RndTask(1));
    56         }
    57         totalTime = 0;
    58         for (int i = 0;i < THREAD_COUNT;i++){
    59             totalTime += futs[i].get();
    60         }
    61         System.out.println("使用ThreadLocal包装Random实例:" + totalTime + "ms");
    62         es.shutdown();
    63     }
    64 }

     输出结果:

    pool-1-thread-2 spend 978ms
    pool-1-thread-1 spend 983ms
    pool-1-thread-3 spend 808ms
    pool-1-thread-4 spend 1216ms
    多线程访问同一个Random实例:3985ms
    pool-1-thread-4 spend 109ms
    pool-1-thread-2 spend 111ms
    pool-1-thread-3 spend 115ms
    pool-1-thread-1 spend 130ms
    使用ThreadLocal包装Random实例:465ms

    可以看出:多线程在共享一个Random实例的情况下,总共耗时3985ms,而在ThreadLocal模式下,仅仅耗时465ms。注意:不是所有的并发情况下都适合ThreadLocal,只是在锁资源竞争激烈的情况下,ThreadLocal才有可能适合。

    最后

      ThreadLocal的key就是线程本身的弱引用,一个ThreadLocalMap只能存储一种特定的数据类型。如果想让ThreadLocalMap里存储不同数据类型的数据,你如一个String,一个Integer,一个Date,那么就只能多定义几个ThreadLocal,ThreadLocal支持泛型"public class ThreadLocal<T>"。

    参考:《Java高并发程序设计》 葛一鸣 郭超 编著:

    作者:Joe
    努力了的才叫梦想,不努力的就是空想,努力并且坚持下去,毕竟这是我相信的力量
  • 相关阅读:
    使用迭代器模式批量获得数据(C#实现)
    如何从技术上预防抢票软件刷屏
    如何用Tesseract做日文OCR(c#实现)
    我的.net开发百宝箱
    程序员必备基础:Git 命令全方位学习
    Java 异常处理的十个建议
    50道Java集合经典面试题(收藏版)
    记一次接口性能优化实践总结:优化接口性能的八个建议
    100道MySQL数据库经典面试题解析(收藏版)
    800+Java后端经典面试题,希望你找到自己理想的Offer呀~
  • 原文地址:https://www.cnblogs.com/Joe-Go/p/9797036.html
Copyright © 2020-2023  润新知