1. 场景
在项目中经常用得到 SimpleDateFormat时间转化类,但是其并非线程安全的。可通过一个实例代码来说明。
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class DateFormatExample1 {
private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMdd");
// 请求总数
public static int clientTotal = 5000;
// 同时并发执行的线程数
public static int threadTotal = 200;
public static void main(String[] args) throws Exception {
ExecutorService executorService = Executors.newCachedThreadPool();
final Semaphore semaphore = new Semaphore(threadTotal);
final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
for (int i = 0; i < clientTotal ; i++) {
executorService.execute(() -> {
try {
semaphore.acquire();
update();
semaphore.release();
} catch (Exception e) {
log.error("exception", e);
}
countDownLatch.countDown();
});
}
countDownLatch.await();
executorService.shutdown();
}
private static void update() {
try {
simpleDateFormat.parse("20180208");
} catch (Exception e) {
log.error("parse exception", e);
}
}
}
2. 分析使用到的类与方法是否线程安全。
- 发布的对象是可变类且线程间共享
- 多个线程可以同时修改该对象
分析sdf的源码可知SimpleDateFormat 类内部有一个 Calendar 对象引用,它用来储存和这个 SimpleDateFormat 相关的日期信息,例如sdf.parse(dateStr),sdf.format(date) 诸如此类的方法参数传入的日期相关String, Date等等。而这个发布的对象又可以被多个线程同时修改,进而导致了时间混乱的问题。
3. 线程安全策略
通过线程安全策略的一般方法来研究这个问题的解决办法,
---> 首先,无法将Calendar对象定义成不可变对象。
---> 其次,我们来看看线程封闭的策略,如果将sdf对象封闭在线程里,也就是每个线程创建一个 sdf对象,这样虽然避免了线程安全的问题,但是存在大量的资源消耗。这样我们再看 ThreadLocal的封闭策略,这种方式确实不错,我们通过控制最多的并发线程数可以限制实例化的sdf对象,并且封闭在各自的线程之中。
---> 同步容器,这里可以使用第三方 Joda-Time包
import lombok.extern.slf4j.Slf4j;
import org.joda.time.DateTime;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
@Slf4j
public class DateFormatExample3 {
// 请求总数
public static int clientTotal = 5000;
// 同时并发执行的线程数
public static int threadTotal = 200;
private static DateTimeFormatter dateTimeFormatter = DateTimeFormat.forPattern("yyyyMMdd");
public static void main(String[] args) throws Exception {
ExecutorService executorService = Executors.newCachedThreadPool();
final Semaphore semaphore = new Semaphore(threadTotal);
final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
for (int i = 0; i < clientTotal; i++) {
final int count = i;
executorService.execute(() -> {
try {
semaphore.acquire();
update(count);
semaphore.release();
} catch (Exception e) {
log.error("exception", e);
}
countDownLatch.countDown();
});
}
countDownLatch.await();
executorService.shutdown();
}
private static void update(int i) {
log.info("{}, {}", i, DateTime.parse("20180208", dateTimeFormatter).toDate());
}
}
4. ThreadLocal 使用要注意的问题
ThreadLocal<T>的出现是一种空间换时间的思想的运用,是为了多线程环境下单线程内变量共享的问题。它的原理就是每个线程通过ThreadLocal.ThreadLocalMap,保存当前线程中所有ThreadLocal变量引用的key和值。相当于每个线程有各自的变量副本,线程内共享这个变量数据,线程间互不影响。
ThreadLocal<T>有它自己的使用场景,比如Spring中用它了解决Session、Connection等多线程并发访问问题,但不能它不能用来代替为了解决多线程安全问题的同步关键字,因为它实际上没有多线程间的变量共享,而线程安全问题是指多线程间变量共享,且共享变量可修改,进而可能会出现多线程并发修改共享变量的问题,这种需要通过同步手段解决。
ThreadLocal<T>变量一般要声名成static类型,即当前线程中只有一个T类型变量的实例,线程内可共享该实例数据且不会出问题,如将其声名成非static,则一个线程内就存储多个T类型变量的实例,有点存储空间的浪费,一般很少有这样的应用场景。另外根据实际情况,ThreadLocal变量声名时也多加上private final关键词表明它时类内私有、引用不可修改。
在线程池环境下,由于线程是一直运行且复用的,使用ThreadLocal<T>时会出现这个任务看到上个任务ThreadLocal变量值以及内存泄露等问题,解决方法就是在当前任务执行完后将ThreadLocal变量remove。
5. DateTimeFormatter (Java 8支持)
更新下,Java 8内置的DateTimeFormatter是线程安全的,放心使用。
https://www.liaoxuefeng.com/wiki/1252599548343744/1303985694703650
参考链接:
https://blog.csdn.net/zq602316498/article/details/40263083
https://www.cnblogs.com/zhuimengdeyuanyuan/archive/2017/10/25/7728009.html