Java并发编程 | 第七篇:ThreadLocal

释放双眼,带上耳机,听听看~!

ThreadLocal介绍

ThreadLocal为每个使用变量的线程提供独立的变量副本,所以每一个线程可以独立修改自己的副本,从而隔离了多个线程对数据的访问冲突。
ThreadLocal 适用于每个线程需要自己独立的实例且该实例需要在多个方法中被使用,也即变量在线程间隔离而在方法或类间共享的场景。

注意:跟多线程并发问题没关系,跟多线程并发问题没关系,跟多线程并发问题没关系

ThreadLocal 适用于如下两种场景

  • 每个线程需要有自己单独的实例
  • 实例需要在多个方法中共享,但不希望被多线程共享

ThreadLocal和run方法的局部变量什么区别

  • ThreadLocal可以跨方法共享变量
  • run局部变量只能在单个方法

ThreadLocal简单实现


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
1public class TestNum {
2   // 通过匿名内部类的initialValue()方法,指定初始化值
3   private static ThreadLocal<Integer> seqNum = new ThreadLocal<Integer>() {
4
5       public Integer initialValue() {
6           return 0;
7       }
8   };
9
10  //获得下一个序列值
11  public int getNextNum() {
12      seqNum.set(seqNum.get() - 1);
13      return seqNum.get();
14  }
15    //ThreadLocal为每一个线程提供了单独的副本
16  public static void main(String[] args) {
17       TestNum sn=new TestNum();
18      
19       TestClient t1=new TestClient(sn);
20       TestClient t2=new TestClient(sn);
21       TestClient t3=new TestClient(sn);
22      
23       t1.start();
24       t2.start();
25       t3.start();
26  }
27 
28  private static class TestClient extends Thread{
29      private TestNum sn;
30     
31      public TestClient(TestNum sn){
32          this.sn=sn;
33      }
34     
35     
36      @Override
37      public void run() {
38          for(int i=0;i<3;i++){
39              //每个线程打出3个序列值
40              System.out.println("thread["+Thread.currentThread().getName()
41                      +"]-->sn["+sn.getNextNum()+"]");
42          }
43      }
44  }
45}
46
47

我们发现每个TestClient线程产生的序号虽然都共享同一TestNum实例,但它们并没有发现相互干扰,因为ThreadLocal为每一个线程提供了单独的副本。

ThreadLocal的实现原理

我们需要关注ThreadLocal的set()方法和get()方法
下面的set()源码


1
2
3
4
5
6
7
8
9
10
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    }
9
10

可以看出,先获取当前线程对象,然后通过getMap()拿到线程的ThreadLocalMap,并将值设置进去,ThreadLocalMap的key就当前对象,value就我们要的值。
线程隔离原理就在这个ThreadLocalMap,ThreadLocalMap是ThreadLocal类的一个静态内部类,每个线程都有独立的ThreadLocalMap副本,里面存储的值只能对当前线程读取和修改。ThreadLocal类通过操作每一个线程特有的ThreadLocalMap副本,从而实现了变量访问不同线程的隔离

在看get()方法


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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    }
14
15

先取当前线程的ThreadLocalMap对象,通过将自己作为key获取内部数据

了解源码后,你会发现这些变量是维护在Thread类内部的,也就ThreadLocalMap,这也就说只要外部的Thread线程不退出,对象的引用将一直存在
再看Thread类的exit()方法,里面包括清除ThreadLocalMap


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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    }
15
16

如果我们使用线程池,当前线程很可能总是存在,那么也会导致对象一直保存ThreadLocalMap,
为了避免内存泄漏,我们需要使用ThrealLocal.remove()将变量移除。我们有时候可以为加速垃圾回收,会特意写obj=null类似的代码,obj指向的对象会更容易被垃圾回收器发现,然后加速回收

ThreadLocal对性能帮助

工作线程的内部逻辑有两种模式:
1、多线程共享一个Random(mode=0)
2、多线程各分配一个Random(mode=1)


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
1package ThreadLocal;
2
3import java.util.Random;
4import java.util.concurrent.Callable;
5import java.util.concurrent.ExecutionException;
6import java.util.concurrent.ExecutorService;
7import java.util.concurrent.Executors;
8import java.util.concurrent.Future;
9
10public class tRnd {
11  // 每个线程产生的随机数的数量
12  public static final int GEN_COUNT = 10000000;
13  // 参与工作的线程数量
14  public static final int THREAD_COUNT = 4;
15  static ExecutorService es = Executors.newFixedThreadPool(THREAD_COUNT);
16  // 被多线程共享的Random实例,用于产生随机数
17  public static Random rnd = new Random(123);
18
19  public static ThreadLocal<Random> tRnd = new ThreadLocal<Random>() {
20      @Override
21      protected Random initialValue() {
22          // TODO Auto-generated method stub
23          return new Random(123);
24      }
25
26  };
27
28  public static class RndTask implements Callable<Long> {
29      private int mode = 0;
30
31      public RndTask(int mode) {
32          this.mode = mode;
33      }
34
35      public Random getRandom() {
36          if (mode == 0) {
37              return rnd;
38          } else if (mode == 1) {
39              return tRnd.get();
40          } else {
41              return null;
42          }
43      }
44
45      @Override
46      public Long call() throws Exception {
47          // TODO Auto-generated method stub
48          long b=System.currentTimeMillis();
49          for(int i=0;i<GEN_COUNT;i++){
50              getRandom().nextInt();
51          }
52          long e=System.currentTimeMillis();
53          System.out.println(Thread.currentThread().getName()+" spend "+(e-b)+"ms");
54         
55          return e-b;
56      }
57     
58
59  }
60  public static void main(String[] args) throws InterruptedException, ExecutionException {
61      //Future表示异步计算的结果,它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。
62      Future<Long>[] futs=new Future[THREAD_COUNT];
63      for(int i=0;i<THREAD_COUNT;i++){
64          futs[i]=es.submit(new RndTask(0));
65      }
66      long totaltime=0;
67      for(int i=0;i<THREAD_COUNT;i++){
68          totaltime+=futs[i].get();
69      }
70      System.out.println("多线程访问同一Random实例:"+totaltime+"ms");
71     
72      for(int i=0;i<THREAD_COUNT;i++){
73          futs[i]=es.submit(new RndTask(1));
74      }
75       totaltime=0;
76      for(int i=0;i<THREAD_COUNT;i++){
77          totaltime+=futs[i].get();
78      }
79      System.out.println("使用ThreadLocal包装Random实例:"+totaltime+"ms");
80     
81      es.shutdown();
82  }
83
84}
85
86
87

防止内存泄漏

Entry虽然是弱引用,但它是 ThreadLocal 类型的弱引用(也即上文所述它是对 键 的弱引用),而非具体实例的的弱引用,所以无法避免具体实例相关的内存泄漏。
对于已经不再被使用且已被回收的 ThreadLocal 对象,它在每个线程内对应的实例由于被线程的 ThreadLocalMap 的 Entry 强引用,无法被回收,可能会造成内存泄漏。
针对该问题,ThreadLocalMap 的 set 方法中,通过 replaceStaleEntry 方法将所有键为 null 的 Entry 的值设置为 null,从而使得该值可被回收。另外,会在 rehash 方法中通过 expungeStaleEntry 方法将键和值为 null 的 Entry 设置为 null 从而使得该 Entry 可被回收。通过这种方式,ThreadLocal 可防止内存泄漏。


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
1private void set(ThreadLocal<?> key, Object value) {
2  Entry[] tab = table;
3  int len = tab.length;
4  int i = key.threadLocalHashCode & (len-1);
5  for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
6    ThreadLocal<?> k = e.get();
7    if (k == key) {
8      e.value = value;
9      return;
10    }
11    if (k == null) {
12      replaceStaleEntry(key, value, i);
13      return;
14    }
15  }
16  tab[i] = new Entry(key, value);
17  int sz = ++size;
18  if (!cleanSomeSlots(i, sz) && sz >= threshold)
19    rehash();
20}
21
22
23

参考:《Java高并发程序设计》
http://blog.csdn.net/lufeng20/article/details/24314381

给TA打赏
共{{data.count}}人
人已打赏
安全技术

Bootstrap 4 Flex(弹性)布局

2021-12-21 16:36:11

安全技术

从零搭建自己的SpringBoot后台框架(二十三)

2022-1-12 12:36:11

个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索