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

2021-06-15  本文已影响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不一定会进行一次彻底的扫描,但是也是有几率的)。

上一篇下一篇

猜你喜欢

热点阅读