一个小例子秒懂ThreadLocal使用
遇到问题
最近一个小伙伴把项目中封装的日期工具类用在多线程环境下居然出了问题,来看看怎么回事吧
日期转换的一个工具类
public class DateUtil { private static final SimpleDateFormat sdf =
new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static Date parse(String dateStr) {
Date date = null;
try {
date = sdf.parse(dateStr);
} catch (ParseException e) {
e.printStackTrace();
}
return date;
}
}
然后将这个工具类用在多线程环境下
public static void main(String[] args) { ExecutorService service = Executors.newFixedThreadPool(20);
for (int i = 0; i < 20; i++) {
service.execute(()->{
System.out.println(DateUtil.parse("2019-06-01 16:34:30"));
});
}
service.shutdown();
}
结果报异常了
这个异常就不从源码的角度分析了,写一个小Demo,理解了这个小Demo,就理解了原因
一个将数字加10的工具类
public class NumUtil { public static int addNum = 0;
public static int add10(int num) {
addNum = num;
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
return addNum + 10;
}
}
public static void main(String[] args) { ExecutorService service = Executors.newFixedThreadPool(20);
for (int i = 0; i < 20; i++) {
int num = i;
service.execute(()->{
System.out.println(num + " " + NumUtil.add10(num));
});
}
service.shutdown();
}
然后代码的一部分输出为
0 283 28
7 28
11 28
15 28
什么鬼,不是加10么,怎么都输出了28?这主要是因为线程切换的原因,线程陆续将addNum值设置为0 ,3,7但是都没有执行完(没有执行到return addNum+10这一步)就被切换了,当其中一个线程将addNum值设置为18时,线程陆续开始执行addNum+10这一步,结果都输出了28。
SimpleDateFormat的原因和这个类似,都主要是类内定义的共享变量 "public static int addNum = 0;", "private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");",在多线程使用的情况下,被共享。
那么我们如何解决这个问题呢?
解决方案
解决方案1:每次来都new新的,空间浪费比较大
public class DateUtil { public static Date parse(String dateStr) {
SimpleDateFormat sdf =
new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date date = null;
try {
date = sdf.parse(dateStr);
} catch (ParseException e) {
e.printStackTrace();
}
return date;
}
}
解决方案2:方法用synchronized修饰,并发上不来
public class DateUtil { private static final SimpleDateFormat sdf =
new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static synchronized Date parse(String dateStr) {
Date date = null;
try {
date = sdf.parse(dateStr);
} catch (ParseException e) {
e.printStackTrace();
}
return date;
}
}
解决方案3:用jdk1.8中的日期格式类DateFormatter,DateTimeFormatter,又不想改项目中用到这个工具类的代码 解决方案
4:用ThreadLocal,一个线程一个SimpleDateFormat对象
public class DateUtil { private static ThreadLocal<DateFormat> threadLocal = ThreadLocal.withInitial(
()-> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
public static Date parse(String dateStr) {
Date date = null;
try {
date = threadLocal.get().parse(dateStr);
} catch (ParseException e) {
e.printStackTrace();
}
return date;
}
}
上面的加10的工具类可以改成如下形式(主要为了演示ThreadLocal的使用)
public class NumUtil { private static ThreadLocal<Integer> addNumThreadLocal = new ThreadLocal<>();
public static int add10(int num) {
addNumThreadLocal.set(num);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
return addNumThreadLocal.get() + 10;
}
}
现在2个工具类都能正常使用了,这是为啥呢?
原理分析
当多个线程同时读写同一共享变量时存在并发问题,如果不共享不就没有并发问题了,一个线程存一个自己的变量,类比原来好几个人玩同一个球,现在一个人一个球,就没有问题了,如何把变量存在线程上呢?其实Thread类内部已经有一个Map容器用来存变量了。它的大概结构如下所示
ThreadLocalMap是一个Map,key是ThreadLocal,value是Object
看下面这篇,原理讲的详细 一点: https://www.jianshu.com/p/98b68c97df9b
以上是 一个小例子秒懂ThreadLocal使用 的全部内容, 来源链接: utcz.com/z/516755.html