Java并发编程(十三)ThreadLocal

ThreadLocal

锁可以保证一个变量被使用的时候,可以将其锁住,不让其他线程修改,那么引申到一个问题,可不可给每个线程都设置一个变量,让他们各自为营,以空间去换取时间呢?当然可以,于是乎就有了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
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
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class test implements Runnable {
private static final SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd HH:m:ss");
int i=0;

public static void main(String[] args) {
ExecutorService es= Executors.newFixedThreadPool(10);
for (int i = 0; i <10 ; i++) {
es.execute(new test());
}
}
@Override
public void run() {
try {
Date t=sdf.parse("2019-12-28 15:19:"+i/60);
System.out.println(i+":"+t);
} catch (ParseException e) {
e.printStackTrace();
}
}
}


Exception in thread "pool-1-thread-6" Exception in thread "pool-1-thread-4" Exception in thread "pool-1-thread-7" Exception in thread "pool-1-thread-1" Exception in thread "pool-1-thread-2" java.lang.NumberFormatException: multiple points
at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1890)
at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
at java.lang.Double.parseDouble(Double.java:538)
at java.text.DigitList.getDouble(DigitList.java:169)
at java.text.DecimalFormat.parse(DecimalFormat.java:2089)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
at java.text.DateFormat.parse(DateFormat.java:364)
at test.run(test.java:20)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
java.lang.NumberFormatException: multiple points
at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1890)
at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
at java.lang.Double.parseDouble(Double.java:538)
at java.text.DigitList.getDouble(DigitList.java:169)
at java.text.DecimalFormat.parse(DecimalFormat.java:2089)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
at java.text.DateFormat.parse(DateFormat.java:364)
at test.run(test.java:20)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
java.lang.NumberFormatException: For input string: ""
at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
at java.lang.Long.parseLong(Long.java:601)
at java.lang.Long.parseLong(Long.java:631)
at java.text.DigitList.getLong(DigitList.java:195)
at java.text.DecimalFormat.parse(DecimalFormat.java:2084)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
at java.text.DateFormat.parse(DateFormat.java:364)
at test.run(test.java:20)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
Exception in thread "pool-1-thread-5" java.lang.NumberFormatException: For input string: ""
at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
at java.lang.Long.parseLong(Long.java:601)
at java.lang.Long.parseLong(Long.java:631)
at java.text.DigitList.getLong(DigitList.java:195)
at java.text.DecimalFormat.parse(DecimalFormat.java:2084)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
at java.text.DateFormat.parse(DateFormat.java:364)
at test.run(test.java:20)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
java.lang.NumberFormatException: For input string: ""
at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
at java.lang.Long.parseLong(Long.java:601)
at java.lang.Long.parseLong(Long.java:631)
at java.text.DigitList.getLong(DigitList.java:195)
at java.text.DecimalFormat.parse(DecimalFormat.java:2084)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:2162)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
at java.text.DateFormat.parse(DateFormat.java:364)
at test.run(test.java:20)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
java.lang.NumberFormatException: For input string: ""
at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
at java.lang.Long.parseLong(Long.java:601)
at java.lang.Long.parseLong(Long.java:631)
at java.text.DigitList.getLong(DigitList.java:195)
at java.text.DecimalFormat.parse(DecimalFormat.java:2084)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
at java.text.DateFormat.parse(DateFormat.java:364)
at test.run(test.java:20)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)0:Sat Dec 28 15:19:00 CST 2019
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)

0:Sat Dec 28 15:19:00 CST 2019
0:Sat Dec 28 15:19:00 CST 2019
0:Sat Dec 28 15:19:00 CST 2019

很明显,不可以,因为sdf.parse并不是线程安全的,所以我们除了可以使用锁去锁住它之外,还可以使用ThreadLocal去处理,为每一个线程都产生一个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
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class test implements Runnable {
static ThreadLocal<SimpleDateFormat> sdf=new ThreadLocal<SimpleDateFormat>();
int i=0;

public static void main(String[] args) {
ExecutorService es= Executors.newFixedThreadPool(10);
for (int i = 0; i <10 ; i++) {
es.execute(new test());
}
es.shutdown();
}
@Override
public void run() {
try {
if(sdf.get()==null){
sdf.set(new SimpleDateFormat("yyyy-MM-dd HH:m:ss"));
}
Date t=sdf.get().parse("2019-12-28 15:19:"+i/60);
System.out.println(i+":"+t);
} catch (ParseException e) {
e.printStackTrace();
}
}
}


0:Sat Dec 28 15:19:00 CST 2019
0:Sat Dec 28 15:19:00 CST 2019
0:Sat Dec 28 15:19:00 CST 2019
0:Sat Dec 28 15:19:00 CST 2019
0:Sat Dec 28 15:19:00 CST 2019
0:Sat Dec 28 15:19:00 CST 2019
0:Sat Dec 28 15:19:00 CST 2019
0:Sat Dec 28 15:19:00 CST 2019
0:Sat Dec 28 15:19:00 CST 2019
0:Sat Dec 28 15:19:00 CST 2019

可以看到,为每一个线程都分配了一个对象去工作,以空间换取时间,这使得我们每个线程都有了独立的局部变量,让其线程安全。但如果在应用上为每个对象都分配相同的对象实例,也是会导致线程不安全的。

ThreadLocal实现原理

set和get

ThreadLocal最主要的就是它的set和get

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}

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

可以看到我们传入的值,传入到了一个map中。把当前线程的值看做为一个key,把传入的参数看作为一个value,先获取当前线程的id,作为一个key,如果不存在则创造一个全局的map(其实就是ThreadLocal本身),将其key和value加入map中,若存在这个key,则重新修改这个值。在使用get方法取出的时候,利用了泛型,将value的值重新完整的取出。

exit和remove

当我们的线程退出的时候,自动会执行exit方法,而exit方法也包括了对ThreadLocal的清理。

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

但平常我们并不经常单独的去使用一个线程,而是使用线程池去使用线程,这就牵扯到了一个问题,那就是线程池的线程有线程复用现象,那么这样的线程就不会被销毁,那么ThreadLocal也就一直存在,当它积累到一定程度的时候,就有内存泄漏的风险,于是乎,我们便可以手动的去销毁ThreadLocalmap。

1
2
3
4
5
6
7
t.remove();
//
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}

除此之外,还可以主动的设置object=null,让jvm更快的回收。

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
88
89
90
91
92
93
94
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadLocalGC {
static volatile ThreadLocal<SimpleDateFormat> t1=new ThreadLocal<SimpleDateFormat>(){
@Override
protected void finalize() throws Throwable{
System.out.println(this.toString()+"is GC");
}
};
static volatile CountDownLatch cd=new CountDownLatch(10000);

public static void main(String[] args) throws InterruptedException {
ExecutorService es= Executors.newFixedThreadPool(10);
for (int i = 0; i <10000 ; i++) {
es.execute(new ParseDate(i));
}
cd.await();
System.out.println("任务完成");
t1=null;
System.gc();
System.out.println("首次GC成功");

t1=new ThreadLocal<SimpleDateFormat>();
cd=new CountDownLatch(10000);
for (int i = 0; i <10000 ; i++) {
es.execute(new ParseDate(i));
}
cd.await();
Thread.sleep(1000);
System.gc();
System.out.println("第二次GC成功");
es.shutdown();
}


public static class ParseDate implements Runnable{
int i=0;

public ParseDate(int i) {
this.i = i;
}

@Override
public void run() {
try {
if(t1.get()==null){
t1.set(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"){
@Override
protected void finalize() throws Throwable {
System.out.println(this.toString()+"is GC2");
}
});
System.out.println(Thread.currentThread().getId()+"create SimpleDateFormat");
}
Date t=t1.get().parse("2019-12-28 15:19:"+i/60);
} catch (ParseException e) {
e.printStackTrace();
}finally {
cd.countDown();
}
}
}
}


13create SimpleDateFormat
21create SimpleDateFormat
19create SimpleDateFormat
16create SimpleDateFormat
14create SimpleDateFormat
17create SimpleDateFormat
18create SimpleDateFormat
20create SimpleDateFormat
15create SimpleDateFormat
12create SimpleDateFormat
任务完成
首次GC成功
ThreadLocal的GC$1@74560fd0is GC
14create SimpleDateFormat
18create SimpleDateFormat
19create SimpleDateFormat
17create SimpleDateFormat
13create SimpleDateFormat
15create SimpleDateFormat
20create SimpleDateFormat
21create SimpleDateFormat
12create SimpleDateFormat
16create SimpleDateFormat
第二次GC成功

这个例子首先设置了只有10个线程的线程池,然后使用线程池去使用线程,而CountDownLatch则是必须执行10000次,因为只有十个线程,所以也只制造了10个ThreadLocal实例,在它们执行完10000次后,将ThreadLocal的设置为null,这也就使得每个线程的ThreadLocal为空了,之后马上进行一次GC回收,再次创建的时候,判定t1.get()==null,可见ThreadLocal又重新被制作了。

前面对ThreadLocal的使用的涉及到了ThreadLocalMap的entry

1
2
3
4
5
6
7
8
9
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;

Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}

可以看作为一种较为弱的HashMap。当这个ThreadLocal的t1被设置为null时,它的所有key值都将不复存在,但这并不代表线程已经被销毁了,而是指它和线程的ID 解绑了,于是ThreadLocal的entry也将被销毁,成为null,而再次去线程池里使用线程时,ThreadLocal的key值再次和线程ID绑定,只不过他的value已空,需要重新去设置。

ThreadLocal对性能的提升

最后我们最后做一个测试去查看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
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
import java.util.Random;
import java.util.concurrent.*;

public class test {
public static final int GEN_COUNT=10000000;//每个线程要执行生成随机数的次数
public static final int THREAD_COUNT=4;//线程数
static ExecutorService es= Executors.newFixedThreadPool(10);//线程池
public static Random rnd=new Random(123);//返回一个随机数

public static ThreadLocal<Random> tRnd=new ThreadLocal<Random>(){
@Override
protected Random initialValue() {
return new Random(123);//返回一个随机数
}
};
public static class RndTask implements Callable<Long>{
//java5开始,提供了Callable接口,是Runable接口的增强版。
private int mode=0;

public RndTask(int mode) {
this.mode = mode;
}

public Random getRandom(){
if(mode==0){
return rnd;//
}else if (mode==1){
return tRnd.get();//ThreadLocal
}else {
return null;
}
}

@Override
public Long call() throws Exception {
long b=System.currentTimeMillis();
for (long i = 0; i <GEN_COUNT ; i++) {
getRandom().nextInt();//返回随机数,10000000次
}
long e=System.currentTimeMillis();
System.out.println(Thread.currentThread().getName()+"花费"+(e-b)+"ms");
return e-b;// java5提供了Future接口来代表Callable接口里的call()方法的返回值,并为Future接口提供了一个FutureTask实现类,该实现类实现了Future接口,并实现了Runnable接口,所以这样可以作为Thread的target。
}
}

public static void main(String[] args) throws ExecutionException, InterruptedException {
Future<Long>[] f=new Future[THREAD_COUNT];//制作四个线程组
for (int i = 0; i <THREAD_COUNT ; i++) {
f[i]=es.submit(new RndTask(0));//提交普通的线程
}
long totaltime=0;
for (int i = 0; i <THREAD_COUNT ; i++) {
totaltime+=f[i].get();//获取返回值
}
System.out.println("多线程访问同一个Random实例"+totaltime+"ms");

//Threadlocal
for (int i = 0; i <THREAD_COUNT ; i++) {
f[i]=es.submit(new RndTask(1));//提交ThreadLocal
}
totaltime=0;
for (int i = 0; i <THREAD_COUNT ; i++) {
totaltime+=f[i].get();//获取返回值
}
System.out.println("使用ThreadLocal包装Random实例"+totaltime+"ms");
es.shutdown();
}
}





pool-1-thread-2花费1596ms
pool-1-thread-3花费1643ms
pool-1-thread-1花费1688ms
pool-1-thread-4花费1687ms
多线程访问同一个Random实例6614ms
pool-1-thread-8花费110ms
pool-1-thread-7花费121ms
pool-1-thread-5花费127ms
pool-1-thread-6花费132ms
使用ThreadLocal包装Random实例490ms

这个测试,一个使用全局的Random实例,被多个线程访问,每个线程都执行了100000次return new Random(123)的操作,总共产生了40000000次Random对象。而使用ThreadLocal去包装Random,总共就创建了4次Random对象,之后只是在不断地访问它而已,可见得,效率有着显著的提高。