当前位置: 首页 > 图灵资讯 > 技术篇> 高并发场景下,6种解决SimpleDateFormat类的线程安全问题方法

高并发场景下,6种解决SimpleDateFormat类的线程安全问题方法

来源:图灵教育
时间:2023-06-30 16:27:00

摘要:解决SimpledateFormat在高并发场景下的线程安全问题的方法有很多。在这里,我们将列出几种常见的方法供参考。

本文分享了华为云社区SimpledateFormat线程不安全问题分析的错误,作者: 冰 河 。

解决Simpledateformat在高并发场景下的线程安全问题的方法有很多。在这里,我们将列出几种常见的方法供参考,您也可以在评论区提供更多的解决方案。

1.局部变量法

最简单的方法是将Simpledateformat对象定义为局部变量,如下所示,将Simpledateformat对象定义为parse(String)以上方法,可以解决问题。

package io.binghe.concurrent.lab06;import java.text.ParseException;import java.text.SimpleDateFormat;import java.util.concurrent.CountDownLatch;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;import java.util.concurrent.Semaphore;/** * @author binghe * @version 1.0.0 * @description 解决SimpledateFormat线程安全问题的局部变量法 */public class Simpledateformattest02 { //总执行次数 private static final int EXECUTE_COUNT = 1000; //同时运行的线程数量 private static final int THREAD_COUNT = 20; public static void main(String[] args) throws InterruptedException { final Semaphore semaphore = new Semaphore(THREAD_COUNT); final CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_COUNT); ExecutorService executorService = Executors.newCachedThreadPool(); for (int i = 0; i < EXECUTE_COUNT; i++){ executorService.execute(() -> { try { semaphore.acquire(); try { SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd"); simpleDateFormat.parse("2020-01-01"); } catch (ParseException e) { System.out.println(“线程:” + Thread.currentThread().getName() + " 格式化日期失败”); e.printStackTrace(); System.exit(1); }catch (NumberFormatException e){ System.out.println(“线程:” + Thread.currentThread().getName() + " 格式化日期失败”); e.printStackTrace(); System.exit(1); } semaphore.release(); } catch (InterruptedException e) { System.out.println("信号量错误"); e.printStackTrace(); System.exit(1); } countDownLatch.countDown(); }); } countDownLatch.await(); executorService.shutdown(); System.out.println(“所有线程格式化日期成功”); }}

运行修改后的程序,输出结果如下所示。

所有线程格式日期成功

至于为什么在高并发场景下使用局部变量可以解决线程的安全问题,我们将在[JVM主题]JVM内存模式的相关内容中进行深入分析。这里没有太多的介绍。

当然,这种方法会在高并发下创建大量的Simpledateformat对象,影响程序的性能。因此,在实际生产环境中不推荐这种方法。

2.synchronized锁

将Simpledateformat对象定义为全球静态变量。此时,所有线程共享Simpledateformat对象。此时,在调用格式化时间的方法时,可以同步Simpledateformat对象,代码如下所示。

package io.binghe.concurrent.lab06;import java.text.ParseException;import java.text.SimpleDateFormat;import java.util.concurrent.CountDownLatch;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;import java.util.concurrent.Semaphore;/** * @author binghe * @version 1.0.0 * @description SimpledateFormat类的线程安全问题通过Synchronized锁解决 */public class Simpledateformattest03 { //执行总次数 private static final int EXECUTE_COUNT = 1000; //同时运行的线程数量 private static final int THREAD_COUNT = 20; //SimpleDateFormat对象 private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd"); public static void main(String[] args) throws InterruptedException { final Semaphore semaphore = new Semaphore(THREAD_COUNT); final CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_COUNT); ExecutorService executorService = Executors.newCachedThreadPool(); for (int i = 0; i < EXECUTE_COUNT; i++){ executorService.execute(() -> { try { semaphore.acquire(); try { synchronized (simpleDateFormat){ simpleDateFormat.parse("2020-01-01"); } } catch (ParseException e) { System.out.println(“线程:” + Thread.currentThread().getName() + " 格式化日期失败”); e.printStackTrace(); System.exit(1); }catch (NumberFormatException e){ System.out.println(“线程:” + Thread.currentThread().getName() + " 格式化日期失败”); e.printStackTrace(); System.exit(1); } semaphore.release(); } catch (InterruptedException e) { System.out.println(“信号量错误”); e.printStackTrace(); System.exit(1); } countDownLatch.countDown(); }); } countDownLatch.await(); executorService.shutdown(); System.out.println(“所有线程格式化日期成功”); }}

此时,解决问题的关键代码如下。

synchronized (simpleDateFormat){simpleDateFormat.parse("2020-01-01");}

输出结果如下所示。

所有线程格式日期成功

需要注意的是,虽然这种方法可以解决SimpledateFormat类的线程安全问题,但在程序执行过程中,SimpledateFormat类对象增加了synchronized锁,导致同时只有一个线程执行parse(String)方法。此时会影响程序的执行性能,不建议在要求高并发的生产环境下使用。

3.Lock锁模式

Lock锁的实现原理与synchronized锁相同,都是通过JVM的锁机制在高并发下保证程序的线程安全。Lock锁解决问题的代码如下所示。

package io.binghe.concurrent.lab06;import java.text.ParseException;import java.text.SimpleDateFormat;import java.util.concurrent.CountDownLatch;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;import java.util.concurrent.Semaphore;import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock;/** * @author binghe * @version 1.0.0 * @description SimpledateFormat线程安全问题通过Lock锁解决 */public class Simpledateformattest04 { //执行总次数 private static final int EXECUTE_COUNT = 1000; /////同时运行的线程数 private static final int THREAD_COUNT = 20; ///Simpledateformat对象 private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd"); ///Lock对象 private static Lock lock = new ReentrantLock(); public static void main(String[] args) throws InterruptedException { final Semaphore semaphore = new Semaphore(THREAD_COUNT); final CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_COUNT); ExecutorService executorService = Executors.newCachedThreadPool(); for (int i = 0; i < EXECUTE_COUNT; i++){ executorService.execute(() -> { try { semaphore.acquire(); try { lock.lock(); simpleDateFormat.parse("2020-01-01"); } catch (ParseException e) { System.out.println("线程:"" + Thread.currentThread().getName() + " 格式化日期失败”); e.printStackTrace(); System.exit(1); }catch (NumberFormatException e){ System.out.println("线程:"" + Thread.currentThread().getName() + " 格式化日期失败”); e.printStackTrace(); System.exit(1); }finally { lock.unlock(); } semaphore.release(); } catch (InterruptedException e) { System.out.println(“信号量错误”); e.printStackTrace(); System.exit(1); } countDownLatch.countDown(); }); } countDownLatch.await(); executorService.shutdown(); System.out.println(“所有线程格式化日期成功”); }}

通过代码可以知道,首先,Lock类型的全球静态变量被定义为加锁和释放锁的句柄。然后是simpleDateFormat.parse(String)在代码通过lock之前.lock()加锁。这里需要注意的是,为了防止程序抛出异常导致锁无法释放,必须将释放锁的操作放入finally代码块,如下所示。

finally {lock.unlock();}

输出结果如下所示。

所有线程格式日期成功

这种方法也会影响高并发场景的性能,不建议在高并发生产环境中使用。

4.Threadlocal方式

使用Threadlocal存储Simpledateformat对象的副本,可以有效避免多线程引起的线程安全问题。使用Threadlocal解决线程安全问题的代码如下所示。

package io.binghe.concurrent.lab06;import java.text.DateFormat;import java.text.ParseException;import java.text.SimpleDateFormat;import java.util.concurrent.CountDownLatch;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;import java.util.concurrent.Semaphore;/** * @author binghe * @version 1.0.0 * @description SimpledateFormat线程安全问题通过Threadlocal解决 */public class Simpledateformattest { //执行总次数 private static final int EXECUTE_COUNT = 1000; /////同时运行的线程数 private static final int THREAD_COUNT = 20; private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>(){ @Override protected DateFormat initialValue() { return new SimpleDateFormat("yyyy-MM-dd"); } }; public static void main(String[] args) throws InterruptedException { final Semaphore semaphore = new Semaphore(THREAD_COUNT); final CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_COUNT); ExecutorService executorService = Executors.newCachedThreadPool(); for (int i = 0; i < EXECUTE_COUNT; i++){ executorService.execute(() -> { try { semaphore.acquire(); try { threadLocal.get().parse("2020-01-01"); } catch (ParseException e) { System.out.println("线程:"" + Thread.currentThread().getName() + " 格式化日期失败”); e.printStackTrace(); System.exit(1); }catch (NumberFormatException e){ System.out.println("线程:"" + Thread.currentThread().getName() + " 格式化日期失败”); e.printStackTrace(); System.exit(1); } semaphore.release(); } catch (InterruptedException e) { System.out.println(“信号量错误”); e.printStackTrace(); System.exit(1); } countDownLatch.countDown(); }); } countDownLatch.await(); executorService.shutdown(); System.out.println(“所有线程格式化日期成功”); }}

通过代码可以知道,每个线程使用的Simpledateformat副本保存在Threadlocal中,每个线程在使用时不会相互干扰,从而解决了线程安全问题。

输出结果如下所示。

所有线程格式日期成功

该方法运行效率较高,建议在高并发业务场景的生产环境中使用。

此外,ThreadLocal还可以用以下形式编写代码,效果相同。

package io.binghe.concurrent.lab06;import java.text.DateFormat;import java.text.ParseException;import java.text.SimpleDateFormat;import java.util.concurrent.CountDownLatch;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;import java.util.concurrent.Semaphore;/** * @author binghe * @version 1.0.0 * @description SimpledateFormat线程安全问题通过Threadlocal解决 */public class Simpledateformattest06 { //执行总次数 private static final int EXECUTE_COUNT = 1000; /////同时运行的线程数 private static final int THREAD_COUNT = 20; private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>(); private static DateFormat getDateFormat(){ DateFormat dateFormat = threadLocal.get(); if(dateFormat == null){ dateFormat = new SimpleDateFormat("yyyy-MM-dd"); threadLocal.set(dateFormat); } return dateFormat; } public static void main(String[] args) throws InterruptedException { final Semaphore semaphore = new Semaphore(THREAD_COUNT); final CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_COUNT); ExecutorService executorService = Executors.newCachedThreadPool(); for (int i = 0; i < EXECUTE_COUNT; i++){ executorService.execute(() -> { try { semaphore.acquire(); try { getDateFormat().parse("2020-01-01"); } catch (ParseException e) { System.out.println("线程:"" + Thread.currentThread().getName() + " 格式化日期失败”); e.printStackTrace(); System.exit(1); }catch (NumberFormatException e){ System.out.println("线程:"" + Thread.currentThread().getName() + " 格式化日期失败”); e.printStackTrace(); System.exit(1); } semaphore.release(); } catch (InterruptedException e) { System.out.println(“信号量错误”); e.printStackTrace(); System.exit(1); } countDownLatch.countDown(); }); } countDownLatch.await(); executorService.shutdown(); System.out.println(“所有线程格式化日期成功”); }}

5.Datetimeformater

Datetimeformater是Java8提供的新日期时间API中的一类,Datetimeformater是线程安全的,Datetimeformater可以直接在高并发场景下使用Datetimeformater来处理日期的格式化操作。代码如下所示。

package io.binghe.concurrent.lab06;import java.time.LocalDate;import java.time.format.DateTimeFormatter;import java.util.concurrent.CountDownLatch;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;import java.util.concurrent.Semaphore;/** * @author binghe * @version 1.0.0 * @description 线程安全问题通过Datemeformater解决 */public class Simpledateformattest07 { //执行总次数 private static final int EXECUTE_COUNT = 1000; /////同时运行的线程数 private static final int THREAD_COUNT = 20; private static DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); public static void main(String[] args) throws InterruptedException { final Semaphore semaphore = new Semaphore(THREAD_COUNT); final CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_COUNT); ExecutorService executorService = Executors.newCachedThreadPool(); for (int i = 0; i < EXECUTE_COUNT; i++){ executorService.execute(() -> { try { semaphore.acquire(); try { LocalDate.parse("2020-01-01", formatter); }catch (Exception e){ System.out.println("线程:"" + Thread.currentThread().getName() + " 格式化日期失败”); e.printStackTrace(); System.exit(1); } semaphore.release(); } catch (InterruptedException e) { System.out.println(“信号量错误”); e.printStackTrace(); System.exit(1); } countDownLatch.countDown(); }); } countDownLatch.await(); executorService.shutdown(); System.out.println(“所有线程格式化日期成功”); }}

可以看出,DatetimeFormater类是线程安全的,DatetimeFormater类可以在高并发场景下直接用于处理日期的格式化操作。

输出结果如下所示。

所有线程格式日期成功

使用datetimeformatter类处理日期的格式化操作效率较高,建议在高并发业务场景的生产环境中使用。

6.joda-time方式

joda-time是第三方处理日期时间格式化的类库,线程安全。如果使用joda-time来处理日期和时间的格式化,则需要引入第三方类库。以maven为例,引入joda-time库如下所示。

<dependency><groupId>joda-time</groupId><artifactId>joda-time</artifactId><version>2.9.9</version></dependency>

引入joda-time库后,实现的程序代码如下所示。

package io.binghe.concurrent.lab06;import org.joda.time.DateTime;import org.joda.time.format.DateTimeFormat;import org.joda.time.format.DateTimeFormatter;import java.util.concurrent.CountDownLatch;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;import java.util.concurrent.Semaphore;/** * @author binghe * @version 1.0.0 * @description 线程安全问题通过Datemeformater解决 */public class Simpledateformattest08 { //执行总次数 private static final int EXECUTE_COUNT = 1000; /////同时运行的线程数 private static final int THREAD_COUNT = 20; private static DateTimeFormatter dateTimeFormatter = DateTimeFormat.forPattern("yyyy-MM-dd"); public static void main(String[] args) throws InterruptedException { final Semaphore semaphore = new Semaphore(THREAD_COUNT); final CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_COUNT); ExecutorService executorService = Executors.newCachedThreadPool(); for (int i = 0; i < EXECUTE_COUNT; i++){ executorService.execute(() -> { try { semaphore.acquire(); try { DateTime.parse("2020-01-01", dateTimeFormatter).toDate(); }catch (Exception e){ System.out.println("线程:"" + Thread.currentThread().getName() + " 格式化日期失败”); e.printStackTrace(); System.exit(1); } semaphore.release(); } catch (InterruptedException e) { System.out.println(“信号量错误”); e.printStackTrace(); System.exit(1); } countDownLatch.countDown(); }); } countDownLatch.await(); executorService.shutdown(); System.out.println(“所有线程格式化日期成功”); }}

这里需要注意的是,Datetime是org.joda.time包下的类别,DatetimeFormat类和DatetimeFormater类都是org.joda.time.format包下的类别如下所示。

import org.joda.time.DateTime;import org.joda.time.format.DateTimeFormat;import org.joda.time.format.DateTimeFormatter;

输出结果如下所示。

所有线程格式日期成功

采用joda-time库处理日期的格式化操作效率较高,建议在高并发业务场景的生产环境中使用。

解决Sim总结pledateformat类线程安全问题的方案

综上所述,在解决Simpledateformat类线程安全问题的几种方案中,局部变量法会创建Simpledateformat类对象,因为每次线程执行格式化时间时,都会导致大量Simpledateformat对象的创建,浪费运行空间和服务器性能,因为JVM创建和销毁对象需要性能。因此,不建议在高并发要求的生产环境中使用。

synchronized锁模式与lock锁模式在处理问题时基本相同。通过加锁,只有一个线程可以在同一时间执行格式化日期和时间操作。虽然这种方法减少了simpledateformat对象的创建,但由于同步锁的存在,性能下降。因此,不建议在高并发要求的生产环境中使用。

通过保存每个线程的SimpleDateFormat对象的副本,ThreadLocal使每个线程在运行时都能使用自己绑定的SimpleFormat对象,相互不干扰,执行性能较高,建议在高并发生产环境中使用。

DatetimeformaterJava 在8中提供的处理日期和时间类别中,Datetimeformater本身是线程安全的。经过压力测试,Datetimeformater处理日期和时间的性能效果良好(后面写了一篇关于高并发性能压力测试的文章)。因此,建议在高并发场景下使用生产环境。

joda-time是第三方处理日期和时间的类库,线程安全,性能经过高并发试验,建议在高并发场景下使用生产环境。

点击关注,第一次了解华为云新技术~