美文网首页
《实战高并发程序设计》读书笔记-ThreadLocal

《实战高并发程序设计》读书笔记-ThreadLocal

作者: 乙腾 | 来源:发表于2021-06-15 06:05 被阅读0次

    除了控制资源的访问外,我们还可以通过增加资源来保证所有对象的线程安全。

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

    ThreadLocal的简单使用

    从ThreadLocal的名字上可以看到,这是一个线程的局部变量。也就是说,只有当前线程可以访问。既然是只有当前线程可以访问的数据,自然是线程安全的。
    下面来看一个简单的示例:

    01 private static final  SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    02 public static class ParseDate implements Runnable{
    03     int i=0;
    04     public ParseDate(int i){this.i=i;}
    05     public void run() {
    06         try {
    07             Date t=sdf.parse("2015-03-29 19:29:"+i%60);
    08             System.out.println(i+":"+t);
    09         } catch (ParseException e) {
    10             e.printStackTrace();
    11         }
    12     }
    13 }
    14 public static void main(String[] args) {
    15     ExecutorService es=Executors.newFixedThreadPool(10);
    16     for(int i=0;i<1000;i++){
    17         es.execute(new ParseDate(i));
    18     }
    19 }
    

    上述代码执行会得到一些异常

    Exception in thread "pool-1-thread-26" java.lang.NumberFormatException: For input string: ""
    Exception in thread "pool-1-thread-17" java.lang.NumberFormatException: multiple points
    

      出现这些问题的原因,是SimipleDateFormat.parse()方法并不是线程安全的。因此,在线程池中共享这个对象必然导致错误。
      一种可行的方案是在sdf.parse()前后加锁,这也是我们一般的处理思路。这里我们不这么做,我们使用ThreadLocal为每一个线程都产生一个SimpleDateformat对象实例:

    01 static ThreadLocal<SimpleDateFormat> tl=new ThreadLocal<SimpleDateFormat>();
    02 public static class ParseDate implements Runnable{
    03     int i=0;
    04     public ParseDate(int i){this.i=i;}
    05     public void run() {
    06         try {
    07             if(tl.get()==null){
    08                 tl.set(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
    09             }
    10             Date t=tl.get().parse("2015-03-29 19:29:"+i%60);
    11             System.out.println(i+":"+t);
    12         } catch (ParseException e) {
    13             e.printStackTrace();
    14         }
    15     }
    16 }
    

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

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

      注意:为每一个线程分配不同的对象,需要在应用层面保证。ThreadLocal只是起到了简单的容器作用。

    notice:

      应用层分配对象实例指的是第8行,为当前线程分配一个属于该线程独有的变量,如果分配的是同一个对象(比如:成员变量),依旧无法保证线程安全,那么ThreadLocal也不能保证线程安全,ThreadLocal只是为当前线程分配一个只属于他的变量,<font color=red>ThreadLocal保证放入其中的这些对象只被当前线程所访问</font>。

    ThreadLocal的实现原理

    那ThreadLocal又是如何保证这些对象只被当前线程所访问呢?下面让我们一起深入ThreadLocal的内部实现。
    我们需要关注的,自然是ThreadLocal的set()方法和get()方法。

    ThreadLocalMap

    但是说set(),get()之前先来说一下ThreadLocalMap,ThreadLocal存储数据的容器是ThreadLocalMap,他是在Thread中定义的成员变量

    ThreadLocal.ThreadLocalMap threadLocals = null;
    

    其中,key为ThreadLocal当前对象,value就是我们需要的值。而threadLocals本身就保存了当前自己所在线程的所有“局部变量”,也就是一个ThreadLocal变量的集合。

    set()

    public void set(T value) {
        Thread t = Thread.currentThread();// 先获取当前线程
        ThreadLocalMap map = getMap(t);  //获取当前线程的ThreadLocalMap
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }
    

    在set时,首先获得当前线程对象,然后通过getMap()拿到线程的ThreadLocalMap,并将值设入ThreadLocalMap中。

    get()

    在进行get()操作时,自然就是将这个Map中的数据拿出来:

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

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

    ThreadLocalMap的问题

    在了解了ThreadLocal的内部实现后,我们自然会引出一个问题。那就是<font color=red>这些变量是维护在Thread类内部的(ThreadLocalMap定义所在类),这也意味着只要线程不退出,对象的引用将一直存在</font>。
    当线程退出时,Thread类会进行一些清理工作,其中就包括清理ThreadLocalMap,注意下述代码:

    /**
    * 在线程退出前,由系统回调,进行资源清理
    */
    private void exit() {
        if (group != null) {
            group.threadTerminated(this);
            group = null;
        }
        target = null;
        /* 注意这里,jdk加速资源清理 */
        threadLocals = null;
        inheritableThreadLocals = null;
        inheritedAccessControlContext = null;
        blocker = null;
        uncaughtExceptionHandler = null;
    }
    

      因此,如果我们使用线程池,那就意味着当前线程未必会退出(比如固定大小的线程池,线程总是存在)。如果这样,将一些大大的对象设置到ThreadLocal中(它实际保存在线程持有的threadLocals Map内),可能会使系统出现内存泄露的可能(这里我的意思是:你设置了对象到ThreadLocal中,但是不清理它,在你使用几次后,这个对象也不再有用了,但是它却无法被回收)。

      此时,如果你希望及时回收对象,最好使用ThreadLocal.remove()方法将这个变量移除。就像我们习惯性地关闭数据库连接一样。如果你确实不需要这个对象了,那么就应该告诉虚拟机,请把它回收掉,防止内存泄露。

      另外一种有趣的情况是JDK也可能允许你像释放普通变量一样释放ThreadLocal。比如,<font color=red>我们有时候为了加速垃圾回收,会特意写出类似obj=null之类的代码。如果这么做,obj所指向的对象就会更容易地被垃圾回收器发现,从而加速回收</font>。

      同理,如果对于ThreadLocal的变量,我们也手动将其设置为null,比如tl=null,那么这个ThreadLocal对应的所有线程的局部变量都有可能被回收。这里面的奥秘是什么呢?先来看一个简单的例子:

    01 public class ThreadLocalDemo_Gc {
    02  static volatile ThreadLocal<SimpleDateFormat> tl = new ThreadLocal<SimpleDateFormat>() {
    03         protected void finalize() throws Throwable {//注意这里重载的是ThreadLocal
    04             System.out.println(this.toString() + " is gc");
    05         }
    06     };
    07     static volatile CountDownLatch cd = new CountDownLatch(10000);
    08     public static class ParseDate implements Runnable {
    09         int i = 0;
    10         public ParseDate(int i) {
    11             this.i = i;
    12         }
    13         public void run() {
    14             try {
    15                 if (tl.get() == null) {
    16                     tl.set(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss") {
    17                         protected void finalize() throws Throwable { //这里重载的是SimpleDateFormat
    18                             System.out.println(this.toString() + " is gc");
    19                         }
    20                     });
    21             System.out.println(Thread.currentThread().getId() + ":create SimpleDateFormat");
    22                 }
    23                 Date t = tl.get().parse("2015-03-29 19:29:" + i % 60);
    24             } catch (ParseException e) {
    25                 e.printStackTrace();
    26             } finally {
    27                 cd.countDown();
    28             }
    29         }
    30     }
    31
    32     public static void main(String[] args) throws InterruptedException {
    33         ExecutorService es = Executors.newFixedThreadPool(10);
    34         for (int i = 0; i < 10000; i++) {
    35             es.execute(new ParseDate(i));
    36         }
    37         cd.await();
    38         System.out.println("mission complete!!");
    39         tl = null;
    40         System.gc();
    41         System.out.println("first GC complete!!");
    42         //在设置ThreadLocal的时候,会清除ThreadLocalMap中的无效对象
    43         tl = new ThreadLocal<SimpleDateFormat>();
    44         cd = new CountDownLatch(10000);
    45         for (int i = 0; i < 10000; i++) {
    46             es.execute(new ParseDate(i));
    47         }
    48         cd.await();
    49         Thread.sleep(1000);
    50         System.gc();
    51         System.out.println("second GC complete!!");
    52     }
    53 }
    

      上述案例是为了跟踪ThreadLocal对象以及内部SimpleDateFormat对象的垃圾回收。为此,我们在第3行和第17行,重载了finalize()方法。这样,我们在对象被回收时,就可以看到它们的踪迹。
      在主函数main中,先后进行了两次任务提交,每次10000个任务。在第一次任务提交后,代码第39行,我们将tl设置为null,接着进行一次GC。接着,我们进行第2次任务提交,完成后,在第50行再进行一次GC。

      如果你执行上述代码,则最有可能的一种输出如下:

    10:create SimpleDateFormat
    11:create SimpleDateFormat
    13:create SimpleDateFormat
    17:create SimpleDateFormat
    14:create SimpleDateFormat
    8:create SimpleDateFormat
    16:create SimpleDateFormat
    15:create SimpleDateFormat
    12:create SimpleDateFormat
    9:create SimpleDateFormat
    mission complete!!
    first GC complete!!
    geym.conc.ch4.tl.ThreadLocalDemo_Gc$1@15f157b is gc  //第一次gc 只有ThreadLocal被回收了
    9:create SimpleDateFormat
    8:create SimpleDateFormat
    16:create SimpleDateFormat
    13:create SimpleDateFormat
    15:create SimpleDateFormat
    10:create SimpleDateFormat
    11:create SimpleDateFormat
    14:create SimpleDateFormat
    17:create SimpleDateFormat
    12:create SimpleDateFormat
    second GC complete!!
    geym.conc.ch4.tl.ThreadLocalDemo_Gc$ParseDate$1@4f76f1a0 is gc  //第二次gc 第一次的所有SimpleDateFormat实例被回收
    geym.conc.ch4.tl.ThreadLocalDemo_Gc$ParseDate$1@4f76f1a0 is gc
    geym.conc.ch4.tl.ThreadLocalDemo_Gc$ParseDate$1@4f76f1a0 is gc
    geym.conc.ch4.tl.ThreadLocalDemo_Gc$ParseDate$1@4f76f1a0 is gc
    geym.conc.ch4.tl.ThreadLocalDemo_Gc$ParseDate$1@4f76f1a0 is gc
    geym.conc.ch4.tl.ThreadLocalDemo_Gc$ParseDate$1@4f76f1a0 is gc
    geym.conc.ch4.tl.ThreadLocalDemo_Gc$ParseDate$1@4f76f1a0 is gc
    geym.conc.ch4.tl.ThreadLocalDemo_Gc$ParseDate$1@4f76f1a0 is gc
    geym.conc.ch4.tl.ThreadLocalDemo_Gc$ParseDate$1@4f76f1a0 is gc
    geym.conc.ch4.tl.ThreadLocalDemo_Gc$ParseDate$1@4f76f1a0 is g
    

      注意这些输出所代表的含义。首先,线程池中10个线程都各自创建了一个SimpleDateFormat对象实例。接着进行第一次GC,可以看到ThreadLocal对象被回收了(这里使用了匿名类,所以类名看起来有点怪,这个类就是第2行创建的tl对象)。接着提交了第2次任务,这次一样也创建了10个SimpleDateFormat对象。然后,进行第2次GC。可以看到,在第2次GC后,第一次创建的10个SimpleDateFormat子类实例全部被回收。可以看到,虽然我们没有手工remove()这些对象,但是系统依然有可能回收它们(注意,这段代码是在JDK 7中输出的,在JDK 8中,你也许得不到类似的输出,大家可以比较两个JDK版本之间线程持有ThreadLocal变量的不同)。
      要了解这里的回收机制,我们需要更进一步了解Thread.ThreadLocalMap的实现。之前我们说过,ThreadLocalMap是一个类似HashMap的东西。更精确地说,它更加类似<font color=red>WeakHashMap</font>。
      ThreadLocalMap的实现使用了弱引用。弱引用是比强引用弱得多的引用。Java虚拟机在垃圾回收时,如果发现弱引用,就会立即回收。ThreadLocalMap内部由一系列Entry构成,每一个Entry都是WeakReference<ThreadLocal>:

    static class Entry extends WeakReference<ThreadLocal> {
        /** The value associated with this ThreadLocal. */
        Object value;
        Entry(ThreadLocal k, Object v) {
            super(k);
            value = v;
        }
    }
    

      这里的参数k就是Map的key,v就是Map的value。<font color=red>其中k也就是ThreadLocal实例,作为弱引用使用(super(k)就是调用了WeakReference的构造函数)。因此,虽然这里使用ThreadLocal作为Map的key,但是实际上,它并不真的持有ThreadLocal的引用。而当ThreadLocal的外部强引用被回收时,ThreadLocalMap中的key就会变成null。当系统进行ThreadLocalMap清理时(比如将新的变量加入表中,就会自动进行一次清理,虽然JDK不一定会进行一次彻底的扫描,但显然在我们这个案例中,它奏效了),就会自然将这些垃圾数据回收</font>。

    这里解释一下上面ThreadLocal实例回收:

      首先ThreadLocalMap内部由一系列Entry构成,每一个Entry都是WeakReference<ThreadLocal>,key为ThreadLocal实例,但是其为弱引用,一旦发生gc,所有虚引用都会被回收,那么此时key都被回收了,但是value没有被回收,此时ThreadLocalMap中的key就会变成null。将新的变量加入表中,就会自动进行一次清理(虽然JDK不一定会进行一次彻底的扫描,但是也是有几率的)。

    总结:

    使用:

      ThreadLocal虽然可以定义为成员变量,但是通过应用层合理的赋予局部变量,保证该局部变量只有该线程可以访问。

    并发安全的原理:

      ThreadLocal通过Thread类中的ThreadLocalMap容器保存所有局部变量,key为ThreadLocal,value为局部变量,所以每个线程只可以访问自己这条线程的数据,正因为他的key为当前线程,所以保证了每个线程只能访问自己的局部变量,因为他获取不到其他线程的线程id。

      但是ThreadLocal的并发安全是通过应用层实现的,即value必须是局部变量,不能是共享变量。

    回收:

      ThreadLocalMap内部由一系列Entry构成,每一个Entry都是WeakReference<ThreadLocal>,key为ThreadLocal实例,但是其为弱引用,一旦发生gc,所有虚引用都会被回收,那么此时key都被回收了,但是value没有被回收,此时ThreadLocalMap中的key就会变成null。将新的变量加入表中,就会自动进行一次清理(虽然JDK不一定会进行一次彻底的扫描,但是也是有几率的)。

    相关文章

      网友评论

          本文标题:《实战高并发程序设计》读书笔记-ThreadLocal

          本文链接:https://www.haomeiwen.com/subject/xbbaeltx.html