SimpleDateFormat在多线程下不安全?
最近公司统一了下开发规范,其中有一条就是使用SimpleDateFormat时候不要设置成类的静态成员变量,被各个方法引。而是,建议改成方法内部变量,或者借助下ThreadLocal。
作为菜鸟的我,当时听到时,内心绝对的是无数个问号,当然了,听会的时候还是要强行装淡定嘛,假装我懂了。emm,,大家不要学我哦,所谓,子曰:知之为知之,不知为不知,是知也!
To 孔子老师:我错了,所以我来认真学习了,写下这笔记可好?
问题复现
废话别说了,赶紧重现下,别想糊弄到屏幕面前的各位大佬 !!!
下面的代码主要是开启10个线程,每个线程都去调用下格式化日期的方法,会发现结果10次调用的结果中,会出现重复日期,呃呃呃。
public class DateUtil {
private static SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");
//工具类:私有构造
private DateUtil() {
}
public static String format(Integer second) {
return DATE_FORMAT.format(second * 1000);
}
//测试下
public static void main(String[] args) {
//启动10个固定大小的线程池,调用10次的格式化方法
ThreadFactory factory = new ThreadFactoryBuilder().setDaemon(true).setNameFormat("DateUtil-%s").build();
ExecutorService executorService = Executors.newFixedThreadPool(10, factory);
//计数器:当值被减到0的时候,await方法将不会被阻塞
CountDownLatch countDownLatch = new CountDownLatch(10);
//用来存放10次调用格式化后的字符串结果(利用set的不可重复性)
Set<String> dateStrSet = Sets.newConcurrentHashSet();
for (int i = 1; i <= 10; i++) {
final int second = i;
executorService.submit(new Runnable() {
@Override
public void run() {
try {
String dateStr = DateUtil.format(second);
dateStrSet.add(dateStr);
System.out.println(Thread.currentThread().getName() + ":======>" + dateStr);
} catch (Exception e) {
e.printStackTrace();
}
//每执行完一次调用,计数器减1
countDownLatch.countDown();
}
});
}
//计数器未完成,则线程将阻塞在这里
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(" set集合的大小:======>" + dateStrSet.size());
}
}
}
运行结果:
DateUtil-2:======>1970-01-01 08:00:03 000
DateUtil-3:======>1970-01-01 08:00:04 000
DateUtil-1:======>1970-01-01 08:00:03 000
DateUtil-0:======>1970-01-01 08:00:03 000
DateUtil-5:======>1970-01-01 08:00:07 000
DateUtil-6:======>1970-01-01 08:00:07 000
DateUtil-4:======>1970-01-01 08:00:07 000
DateUtil-7:======>1970-01-01 08:00:08 000
DateUtil-8:======>1970-01-01 08:00:09 000
DateUtil-9:======>1970-01-01 08:00:10 000
set集合的大小:======>6
天呐,我这里只是10个线程啊,这重复率有点高吧~
理想的结果应该是10个不重复的日期字符串的。
分析原因
主要原因:
格式化日期,是借助将Date里存储的时间戳(毫秒级)set到
java.util.Calendar 中,而这个Calendar在SimpleDateFormat 中是一个类的成员变量。多线程调用同一个SimpleDateFormat对象的format方法的时候,使用的是同一个Calendar对象 。 讲到这里,应该都懂了吧 。 并发情况下,如果操作同一个SimpleDateForamt的Calendar变量,会引发线程不安全的问题。
源码段1 : format 方法
SimpleDateFormat.java#format方法
Calendar 是SimpleDateFormat 父类DateFormat里的一个成员变量
源码段2 : calendar在哪里被设值的?
SimpleDateFormat.java#构造方法
比如:线程A 把Calendar 改成了 2020-07-20 18:51:00 ,这时候,并发来个线程B ,又把Calendar 改成 2020-07-20 18:51:01 ,,最终线程A调用format返回的值将会是线程B改了之后的值 。这样就会出现,线程A 和线程B 调用format 方法返回值是一样的呢。
解决办法
思路: 每个线程能有着独立的Calendar
(1)内部类成员变量改成内部方法变量
意思就是,在你的工具类里面,每次format的时候,就new SimpleDateFormat ,避免多个线程之间共用SimpleDateFormat对象的问题啦。
public class DateUtil {
//工具类:私有构造
private DateUtil() {
}
public static String format(Integer second) {
SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");
return DATE_FORMAT.format(second * 1000);
}
public static void main(String[] args) {
ThreadFactory factory = new ThreadFactoryBuilder().setDaemon(true).setNameFormat("DateUtil-%s").build();
ExecutorService executorService = Executors.newFixedThreadPool(10, factory);
Set<String> dateStrSet = Sets.newConcurrentHashSet();
CountDownLatch countDownLatch = new CountDownLatch(1000);
for (int i = 1; i <= 1000; i++) {
final int second = i;
executorService.submit(new Runnable() {
@Override
public void run() {
try {
String dateStr = DateUtil.format(second);
dateStrSet.add(dateStr);
System.out.println(Thread.currentThread().getName() + ":======>" + dateStr);
} catch (Exception e) {
e.printStackTrace();
}
countDownLatch.countDown();
}
});
}
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(" set集合的大小:======>" + dateStrSet.size());
}
}
运行结果:
。。。这里省略了很多日期的打印,可直接看最后的set集合大小
DateUtil-8:======>1970-01-01 08:16:38 000
DateUtil-8:======>1970-01-01 08:16:39 000
DateUtil-8:======>1970-01-01 08:16:40 000
DateUtil-3:======>1970-01-01 08:15:36 000
DateUtil-4:======>1970-01-01 08:15:31 000
DateUtil-9:======>1970-01-01 08:15:30 000
DateUtil-7:======>1970-01-01 08:15:29 000
DateUtil-5:======>1970-01-01 08:15:27 000
DateUtil-0:======>1970-01-01 08:15:26 000
DateUtil-2:======>1970-01-01 08:15:25 000
DateUtil-6:======>1970-01-01 08:15:23 000
DateUtil-1:======>1970-01-01 08:15:38 000
set集合的大小:======>1000
这里看出,起码循环调用了format方法1000次,最终产生的格式后的字符产放在了Set集合中,Set的最终大小也是1000个,说明这种方法是可以解决SimpleDateFormat的线程不安全问题的。
(2)ThreadLocal 线程隔离
上面的方法,是能够解决问题的。但是可能追求高质量代码的大佬们,看着肯定就不舒服,就知道天天new ,非要给垃圾回收增加负担是吧!!!
行吧,再支一招呗。使用ThreadLocal< SimpleDateFormat > 让每个线程有着独立的SimpleDateFormat , 这样不就等同于每个线程有着独立的Calendar 啦 ~
import com.google.common.collect.Sets;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import java.text.SimpleDateFormat;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
public class DateUtil {
private static String DATE_FORMAT_DEFAULT = "yyyy-MM-dd HH:mm:ss SSS";
private static ThreadLocal<SimpleDateFormat> DATE_FORMAT_LOCAL = new ThreadLocal<SimpleDateFormat>() {
//给每个线程创建一个SimpleDateFormat对象并存储到ThreadLocalMap中
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat(DATE_FORMAT_DEFAULT);
}
};
//工具类:私有构造
private DateUtil() {
}
public static String format(Integer second) {
return DATE_FORMAT_LOCAL.get().format(second * 1000);
}
//测试下
public static void main(String[] args) {
ThreadFactory factory = new ThreadFactoryBuilder().setDaemon(true).setNameFormat("DateUtil-%s").build();
ExecutorService executorService = Executors.newFixedThreadPool(10, factory);
CountDownLatch countDownLatch = new CountDownLatch(1000);
Set<String> dateStrSet = Sets.newConcurrentHashSet();
for (int i = 1; i <= 1000; i++) {
final int second = i;
executorService.submit(new Runnable() {
@Override
public void run() {
try {
String dateStr = DateUtil.format(second);
dateStrSet.add(dateStr);
System.out.println(Thread.currentThread().getName() + ":======>" + dateStr);
} catch (Exception e) {
e.printStackTrace();
}
//每执行完一次调用,计数器减1
countDownLatch.countDown();
}
});
}
//计数器为完成,则线程将阻塞在这里
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(" set集合的大小:======>" + dateStrSet.size());
}
}
运行结果:
。。。这里省略了很多日期的打印,可直接看最后的set集合大小
DateUtil-4:======>1970-01-01 08:16:40 000
DateUtil-6:======>1970-01-01 08:16:15 000
DateUtil-2:======>1970-01-01 08:16:14 000
DateUtil-9:======>1970-01-01 08:16:13 000
DateUtil-0:======>1970-01-01 08:13:27 000
DateUtil-8:======>1970-01-01 08:13:49 000
DateUtil-1:======>1970-01-01 08:13:29 000
DateUtil-3:======>1970-01-01 08:13:18 000
DateUtil-7:======>1970-01-01 08:13:14 000
DateUtil-5:======>1970-01-01 08:16:27 000
set集合的大小:======>1000
这里也能看出来,使用ThreadLocal 也可以解决SimpleDateFormat的线程不安全的问题。
我现在能猜出,屏幕之前的大佬们肯定又要考我了。
“你不知道ThreadLocal有内存泄漏的问题么?确定这样可以?”
好了,到这里又引发了另一个热点问题:ThreadLocal你了解多少 !
这里就要来点弱引用的定义:
弱引用:这里讨论ThreadLocalMap中的Entry类的重点,如果一个对象只具有弱引用,那么这个对象就会被垃圾回收器回收掉(被弱引用所引用的对象只能生存到下一次GC之前,当发生GC时候,无论当前内存是否足够,弱引用所引用的对象都会被回收掉)。弱引用也是和一个引用队列联合使用,如果弱引用的对象被垃圾回收期回收掉,JVM会将这个引用加入到与之关联的引用队列中。若引用的对象可以通过弱引用的get方法得到,当引用的对象被回收掉之后,再调用get方法就会返回null。
再来段代码:
import java.lang.ref.WeakReference;
import java.util.concurrent.TimeUnit;
public class TestWeakReference {
public static void main(String[] args) {
WeakReference<Long> weakReference = new WeakReference<Long>(new Long(1));
System.out.println("触发gc之前:" + weakReference.get());
System.gc();
while (weakReference.get() != null) {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("触发gc之后:" + weakReference.get());
}
}
}
运行结果:
从这结果可以看出,weakReference 构造的时候,是通过new Long()的方式和该Long类型的对象建立了引用,但这个Long类型的对象除了这个弱引用使用了,其它都没有应用,手动触发垃圾回收,当然就把这个孤零零的Long对象给清掉了呀~
改下代码:
Long longKey = new Long(1);
WeakReference<Long> weakReference = new WeakReference<Long>(longKey);
运行效果:
无限的【触发gc之后:1】输出到了控制台,我等了好久,我希望它停下来啊,估计是永远等不了。
为啥?因为 Long longKey 是个强引用,并且在main函数未结束的时候,绝对不会回收这个强引用的。只有当longKey被回收了,那么longKey只剩下弱引用在使用,这时候,弱引用也会被回收!
记住什么情况下才会回收弱引用:
- 如果一个对象只具有弱引用
- 垃圾回收启动了
再回归到上面考察题的正确答案:上述的 DATE_FORMAT_LOCAL被定义的是个static变量呀,正常来说,ThreadLocalMap引用的这个变量是个static类型的呀,啥时候会被回收?JVM在垃圾回收的时候应该不会回收吧!所以别担心了。
本文地址:https://blog.csdn.net/u014240299/article/details/107468618
上一篇: 集合框架——Collection体系集合