为什么使用ThreadLocal?
初识ThreadLocal时,第一句话就是说其是为了解决线程安全问题的,这句话可以说是正确的但又不正确,为什么说它不正确呢,它会误导我们初学者,我们对线程安全的第一印象往往就是其帮助解决了资源共享的问题,比如sychronized、volatile,多个线程在同一时刻只能有一个访问共享资源,这样来解决共享资源混乱的问题。而ThreadLocal简单理解就是我们在每个线程中都使用了单独的资源(变量等),互不影响。借用一位老哥的图展示一下(我比较懒~):

只看图比较抽象,我们用实践来证明下:
我们在类中定义一个volitaile修饰的同步Integer变量 sync_num和threadLocal包含的线程本地Integer变量threadLocal_num,起三个线程,在线程中进行三次+1操作。
/**
* ThreadLocal初步理解
*/
public class ThreadLocalTest {
private volatile static Integer sync_num =0;//同步变量
//threadLocal局部变量
private static ThreadLocal<Integer> threadLocal_num = ThreadLocal.withInitial(()->new Integer(0));
//lamda表达式方便了我们的使用,其代替的是initialValue方法,如下为原理方法
private ThreadLocal<Integer> threadLocal_num2 = new ThreadLocal<Integer>(){
//该方法提供初始化值,当我们第一次调用get方法时执行,若使用set方法设置了则不会执行该方法
@Override
protected Integer initialValue() {
return new Integer(0);
}
};
public static void main(String[] args) {
for(int i=0;i<3;i++){
new Thread(()->{
Integer integer = threadLocal_num.get();
for (int j=0 ;j<3;j++){
System.out.print(++sync_num+"* ");
System.out.println(++integer+"% ");
}
}).start();
}
}
}
执行结果如下:
1* 1%
2* 2%
3* 3%
4* 1%
5* 2%
6* 3%
7* 1%
8* 2%
9* 3%
可以看到带*号的整数同步变量每次都加一所以加到了9,而线程本地整数变量每次都是自己加到3。
那回到标题上来,我们为啥要使用ThreadLocal呢?第一点,我们都知道同步虽然可以保证数据的准确性,但会导致线程的阻塞,特别是并发量较大的情况下,性能会降低,而线程本地变量就没有这个问题了,缺点是每个线程会增加内存的使用,就是用空间换时间。举个例子,我们都使用过SimpleDateFormat这个类进行时间转换,如:
String dateFormat ="yyyy-MM-dd HH:mm:ss:SSS";
SimpleDateFormat simpleDateFormat = new SimpleDateFormat(dateFormat);
String dateString = simpleDateFormat.format(new Date());
首先,它是线程不安全的,dateFormat使用的内部数据结构可能会被并发的访问所破坏,如果在不同线程里使用同一个SimpleDateFormat变量极有可能会造成生成Date格式的混乱,第一个解决方案就是使用同步,但这样每个线程在其被加锁之后都需要等待,效率低,开销大;其次,如果我有的线程需要使用不同dateFormat来获取不同格式的dateString呢?现在想想是不是ThreadLocal可以完美解决这两个问题,代码如下:
private ThreadLocal<SimpleDateFormat> format = ThreadLocal.withInitial(()->{
String dateFormat ="yyyy-MM-dd HH:mm:ss:SSS";
return new SimpleDateFormat(dateFormat);
});
new Thread(new Runnable() {
@Override
public void run() {
//在线程自己内部设置具体的格式与其他线程不相关
SimpleDateFormat simpleDateFormat = format.get();
String dateFormat ="yyyy-MM-dd ";
simpleDateFormat.applyPattern(dateFormat);
}
}).start();
同样,在多个线程中生成随机数也存在类似的问题。只是java.util.Random类是线程安全的。但是如果多个线程需要等待一个共享的随机数生成器,还是会很低效。Java7中提供了一个便利类,只需要如下调用即可:
int random = ThreadLocalRandom.current().nextInt(upperBound);
上面说本地ThreadLocal比sychornized同步快,口说无凭,我们实地测试一下上面所说的ThreadLocal比synchornized效率高的论点:
测试代码如下:
private volatile static long totalTime =0;
public static void main(String[] args) {
//创建一个核心数为100的线程池
ExecutorService executorService = Executors.newFixedThreadPool(100);
for(int i=0;i<100;i++){
executorService.execute( new Runnable() {
@Override
public void run() {
Integer integer = threadLocal_num.get();
long start_time = System.nanoTime();
for (int j=0 ;j<3;j++){
System.out.print(++sync_num+"* ");
// System.out.println(++integer+"% ");
}
System.out.println("线程"+Thread.currentThread().getName()+"使用时间:"+(System.nanoTime()-start_time));
totalTime= totalTime+System.nanoTime()-start_time;
System.out.println("总时间"+ totalTime);
}
});
}
}
我们创建一个线程池来模拟下高并发场景,将代码运行一遍打印出时间(这里totalTime是同步的,不然计时可就不准了哈哈)
同步变量sync_num执行结果如下:
1* 2* 3* 线程pool-1-thread-1使用时间:721942
总时间1386282
4* 5* 6* 线程pool-1-thread-2使用时间:757783
总时间2223427
7* 8* 9* 线程pool-1-thread-3使用时间:193285
总时间2491595
10* 11* 12* 线程pool-1-thread-4使用时间:207366
总时间2775124
13* 14* 15* 线程pool-1-thread-5使用时间:183045
总时间3034332
16* 17* 18* 线程pool-1-thread-6使用时间:225927
总时间3338341
19* 20* 21* 线程pool-1-thread-7使用时间:219527
总时间3653871
22* 23* 24* 线程pool-1-thread-8使用时间:1804215
总时间5829937
25* 26* 27* 线程pool-1-thread-9使用时间:200326
总时间6108346
.....
28* 29* 30* 线程pool-1-thread-99使用时间:215687
总时间613659414
线程本地变量执行结果:
.....
1% 2% 3% 线程pool-1-thread-64使用时间:13067913
总时间264541078
1% 2% 3% 线程pool-1-thread-65使用时间:14975171
总时间279849059
1% 2% 3% 线程pool-1-thread-48使用时间:815384
总时间280720125
1% 2% 3% 线程pool-1-thread-50使用时间:500495
总时间281532309
1% 2% 3% 线程pool-1-thread-51使用时间:555537
总时间282350254
1% 2% 3% 线程pool-1-thread-49使用时间:496015
总时间283067076
1% 2% 3% 线程pool-1-thread-45使用时间:719381
总时间288843890
1% 2% 3% 线程pool-1-thread-43使用时间:437133
总时间289501190
1% 2% 3% 线程pool-1-thread-47使用时间:485774
总时间290230172
1% 2% 3% 线程pool-1-thread-46使用时间:648979
总时间291238842
结果显而易见吧,差了一倍。
ThreadLoca使用注意点
- 使用完ThreadLoca最好使用remove方法移除不然有可能内存溢出
- 最好不要将ThreadLocal设置成静态的
网友评论