场景
Java中ExecutorService线程池的使用(Runnable和Callable多线程实现):
https://blog.csdn.net/BADAO_LIUMANG_QIZHI/article/details/126242904
Java中创建线程的方式以及线程池创建的方式、推荐使用ThreadPoolExecutor以及示例:
https://blog.csdn.net/BADAO_LIUMANG_QIZHI/article/details/130068794
Java中使用CountDownLatch实现并发流程控制:
https://blog.csdn.net/BADAO_LIUMANG_QIZHI/article/details/126255413
以下会用到如上概念。
Java开发手册中对于SimpleDateFormat的使用的要求是:
【强制】SimpleDateFormat 是线程不安全的类,一般不要定义为 static 变量,如果定义为 static,
必须加锁,或者使用 DateUtils 工具类。
正例:
注意线程安全,使用 DateUtils。亦推荐如下处理:
private static final ThreadLocal<DateFormat> df = new ThreadLocal<DateFormat>() { @Override protected DateFormat initialValue() { return new SimpleDateFormat("yyyy-MM-dd"); } };
说明:如果是 JDK8 的应用,可以使用 Instant 代替 Date,LocalDateTime 代替 Calendar,
DateTimeFormatter 代替 SimpleDateFormat,官方给出的解释:
simple beautiful strong immutable thread-safe。
注:
博客:
https://blog.csdn.net/badao_liumang_qizhi
为了验证以上结论,首先需要了解下时区的概念。
时区是地球上的区域使用同一个时间定义。
以前,人们通过观察太阳的位置(时角)决定时间,这就使得不同经度的地方的时间有所不同(地方时)。
1863 年,首次使用时区的概念。时区通过设立一个区域的标准时间部分地解决了这个问题。
世界各个国家位于地球不同位置上,因此不同国家,特别是东西跨度大的国家日出、日落时间必定有所偏差。
这些偏差就是所谓的时差。现今全球共分为 24 个时区。
由于实用上常常 1 个国家,或 1 个省份同时跨着 2个或更多时区,为了照顾到行政上的方便,常将 1 个国家或 1 个省份划在一起。
所以时区并不严格按南北直线来划分,而是按自然条件来划分。
例如,中国幅员宽广,差不多跨 5 个时区,但为了使用方便简单,实际上在只用东八时区的标准时即北京时间为准。
由于不同的时区的时间是不一样的,甚至同一个国家的不同城市时间都可能不一样,所以,
在 Java 中想要获取时间的时候,要重点关注一下时区问题。
默认情况下,如果不指明,在创建日期的时候,会使用当前计算机所在的时区作为默认时区,
这也是为什么我们通过只要使用new Date()就可以获取中国的当前时间的原因。
Java中输出不同时区的时间
Java中可以通过SimpleDateFormat实现获取不同时区的时间
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); sdf.setTimeZone(TimeZone.getTimeZone("America/New_York")); System.out.println(sdf.format(Calendar.getInstance().getTime()));
以上输出纽约的时间比中国北京时间早了12个小时
验证如果将SimpleDateFormat声明为static会如何
新建测试类TestStaticSDF
import com.google.common.util.concurrent.ThreadFactoryBuilder; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Collections; import java.util.HashSet; import java.util.Set; import java.util.concurrent.*; public class TestStaticSDF { //定义全局SimpleDateFormat private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); //使用guava的ThreadFactoryBuilder定义一个线程池 private static ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat("demo-pool-%d").build(); private static ExecutorService pool = new ThreadPoolExecutor(5, 200, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(1024), threadFactory, new ThreadPoolExecutor.AbortPolicy()); //定义一个CountDownLatch,保证所有子线程执行完之后主线程再执行 private static CountDownLatch countDownLatch = new CountDownLatch(100); public static void main(String[] args) throws InterruptedException { //定义一个线程安全的HashSet Set<String> dates = Collections.synchronizedSet(new HashSet<String>()); for (int i = 0; i < 100; i++) { //获取当前时间 Calendar calendar = Calendar.getInstance(); int finalI = i; pool.execute(()->{ //时间增加 calendar.add(Calendar.DATE,finalI); String dateString = simpleDateFormat.format(calendar.getTime()); dates.add(dateString); countDownLatch.countDown(); }); } //堵塞,直到countDown数量为0 countDownLatch.await(); System.out.println(dates.size());//60 } }
就是循环100次,每次循环的时候都在当前时间基础上增加一个天数,然后把所有日期放在一个线程安全的、带有去重功能的Set中,
然后输出set的元素个数。
这里要注意:
在Java中,Collections类提供了许多线程安全的方法来处理集合类,其中一个重要的方法就是synchronizedSet()。
这是一个可以将任何Set集合转换为线程安全的Set集合的方法。
实际结果却是小于100的值,原因就是因为 SimpleDateFormat 作为一个非线程安全的类,
被当做了共享变量在多个线程中进行使用,这就出现了线程安全问题。
查看format的源码,方法在执行过程中,会使用一个成员变量calendar 来保存时间。
由于我们在声明 SimpleDateFormat 的时候,使用的是 static 定义的。
那么这 个 SimpleDateFormat 就 是 一 个 共 享 变 量, 随 之,SimpleDateFormat 中 的calendar 也就可以被多个线程访问到。
假设线程 1 刚刚执行完calendar.setTime把时间设置成 2018-11-11,还没等执行完,线程 2 又执行了calendar.setTime把时间改成了 2018-12-12。
这时候线程 1 继续往下执行,拿到的calendar.getTime得到的时间就是线程 2 改过之后的。
除了 format 方法以外,SimpleDateFormat 的 parse 方法也有同样的问题。所以,不要把 SimpleDateFormat 作为一个共享变量使用。
Java中多线程环境下使用static的SimpleDateFormat的解决方式
第一种方式,将SimpleDateFormat声明为局部变量,就不会被多个线程同时访问到了,避开线程安全问题
public class TestStaticSDFSolve { //使用guava的ThreadFactoryBuilder定义一个线程池 private static ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat("demo-pool-%d").build(); private static ExecutorService pool = new ThreadPoolExecutor(5, 200, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(1024), threadFactory, new ThreadPoolExecutor.AbortPolicy()); //定义一个CountDownLatch,保证所有子线程执行完之后主线程再执行 private static CountDownLatch countDownLatch = new CountDownLatch(100); public static void main(String[] args) throws InterruptedException { //定义一个线程安全的HashSet Set<String> dates = Collections.synchronizedSet(new HashSet<String>()); for (int i = 0; i < 100; i++) { //获取当前时间 Calendar calendar = Calendar.getInstance(); int finalI = i; pool.execute(()->{ //SimpleDateFormat 声明成局部变量 SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); //时间增加 calendar.add(Calendar.DATE,finalI); String dateString = simpleDateFormat.format(calendar.getTime()); dates.add(dateString); countDownLatch.countDown(); }); } //堵塞,直到countDown数量为0 countDownLatch.await(); System.out.println(dates.size());//100 } }
第二种方式,对于共享变量加锁,通过加锁,使多个线程排队顺序执行,避免了并发导致的线程安全问题
public class TestStaticSDFSolve2 { private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); //使用guava的ThreadFactoryBuilder定义一个线程池 private static ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat("demo-pool-%d").build(); private static ExecutorService pool = new ThreadPoolExecutor(5, 200, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(1024), threadFactory, new ThreadPoolExecutor.AbortPolicy()); //定义一个CountDownLatch,保证所有子线程执行完之后主线程再执行 private static CountDownLatch countDownLatch = new CountDownLatch(100); public static void main(String[] args) throws InterruptedException { //定义一个线程安全的HashSet Set<String> dates = Collections.synchronizedSet(new HashSet<String>()); for (int i = 0; i < 100; i++) { //获取当前时间 Calendar calendar = Calendar.getInstance(); int finalI = i; pool.execute(()->{ synchronized (simpleDateFormat){ //时间增加 calendar.add(Calendar.DATE,finalI); String dateString = simpleDateFormat.format(calendar.getTime()); dates.add(dateString); countDownLatch.countDown(); } }); } //堵塞,直到countDown数量为0 countDownLatch.await(); System.out.println(dates.size());//100 } }
第三种方式,就是使用ThreadLocal。可以确保每个线程都可以得到单独的一个SimpleDateFormat的对象
public class TestStaticSDFSolve3 { private static ThreadLocal<SimpleDateFormat> simpleDateFormatThreadLocal = ThreadLocal .withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")); //使用guava的ThreadFactoryBuilder定义一个线程池 private static ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat("demo-pool-%d").build(); private static ExecutorService pool = new ThreadPoolExecutor(5, 200, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(1024), threadFactory, new ThreadPoolExecutor.AbortPolicy()); //定义一个CountDownLatch,保证所有子线程执行完之后主线程再执行 private static CountDownLatch countDownLatch = new CountDownLatch(100); public static void main(String[] args) throws InterruptedException { //定义一个线程安全的HashSet Set<String> dates = Collections.synchronizedSet(new HashSet<String>()); for (int i = 0; i < 100; i++) { //获取当前时间 Calendar calendar = Calendar.getInstance(); int finalI = i; pool.execute(()->{ //时间增加 calendar.add(Calendar.DATE,finalI); String dateString = simpleDateFormatThreadLocal.get().format(calendar.getTime()); dates.add(dateString); countDownLatch.countDown(); }); } //堵塞,直到countDown数量为0 countDownLatch.await(); System.out.println(dates.size());//100 } }
第四种方式,如果是java8应用,可以使用DateTimeFormatter代替SimpleDateFormat,这是一个线程安全的格式化工具类public class
TestStaticSDFSolve4 { private static DateTimeFormatter format = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); //使用guava的ThreadFactoryBuilder定义一个线程池 private static ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat("demo-pool-%d").build(); private static ExecutorService pool = new ThreadPoolExecutor(5, 200, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(1024), threadFactory, new ThreadPoolExecutor.AbortPolicy()); //定义一个CountDownLatch,保证所有子线程执行完之后主线程再执行 private static CountDownLatch countDownLatch = new CountDownLatch(100); public static void main(String[] args) throws InterruptedException { //定义一个线程安全的HashSet Set<String> dates = Collections.synchronizedSet(new HashSet<String>()); for (int i = 0; i < 100; i++) { //获取当前时间 LocalDateTime now = LocalDateTime.now(); int finalI = i; pool.execute(()->{ //时间增加 LocalDateTime localDateTime = now.plusDays(finalI); String dateString = localDateTime.format(format); dates.add(dateString); countDownLatch.countDown(); }); } //堵塞,直到countDown数量为0 countDownLatch.await(); System.out.println(dates.size());//100 } }
标签:线程,private,SimpleDateFormat,static,new,calendar,多线程 From: https://www.cnblogs.com/badaoliumangqizhi/p/17458754.html