当前位置: 首页 > 科技观察

SimpleDateFormat线程不安全的5个解决方案!

时间:2023-03-14 22:15:06 科技观察

本文转载自微信公众号“Java中文社区”,作者雷哥。转载本文请联系Java中文社区公众号。1、什么是线程不安全?线程不安全也称为非线程安全。意思是在多线程执行中,如果程序的执行结果与预期的结果不匹配,就称为线程不安全。线程不安全代码SimpleDateFormat是线程不安全的典型例子。接下来,让我们来实现它。首先,我们创建10个线程来格式化时间。每次格式化要格式化的时间都不一样,所以如果程序正确执行,会打印出10个不同的值。接下来我们看一下具体的代码。实现:importjava.text.SimpleDateFormat;importjava.util.Date;importjava.util.concurrent.ExecutorService;importjava.util.concurrent.Executors;publicclassSimpleDateFormatExample{//创建SimpleDateFormat对象privatestaticSimpleDateFormatsimpleDateFormat=newSimpleDateFormat("mm:mainss");publicstaticvoidString[]args){//创建线程池ExecutorServicethreadPool=Executors.newFixedThreadPool(10);//执行10次格式化for(inti=0;i<10;i++){intfinalI=i;//线程池执行任务threadPool.execute(newRunnable(){@Overridepublicvoidrun(){//创建时间对象Datedate=newDate(finalI*1000);//执行时间格式并打印结果System.out.println(simpleDateFormat.format(date));}});}}}我们期望的正确结果是这样的(打印10次的值都不一样):然而上面程序的运行结果如下:从上面的结果可以看出当使用SimpleDateFormat的多线程时间格式化不是线程安全的。2、解决方案SimpleDateFormat线程不安全的解决方案一共包括以下5种:将SimpleDateFormat定义为局部变量;使用同步锁执行;使用Lock锁执行(类似方案二);使用线程本地;使用JDK8提供的DateTimeFormat。下面我们分别来看一下各个方案的具体实现。①将SimpleDateFormat改为局部变量将SimpleDateFormat定义为局部变量时,由于每个线程都是独占SimpleDateFormat对象的,相当于把多线程程序变成了“单线程”程序,所以不会有线程不安全感。具体实现代码如下:importjava.text.SimpleDateFormat;importjava.util.Date;importjava.util.concurrent.ExecutorService;importjava.util.concurrent.Executors;publicclassSimpleDateFormatExample{publicstaticvoidmain(String[]args){//创建一个线程池ExecutorServicethreadPool=Executors.newFixedThreadPool(10);//执行10次格式化for(inti=0;i<10;i++){intfinalI=i;//线程池执行任务threadPool.execute(newRunnable(){@Overridepublicvoidrun(){//创建一个SimpleDateFormat对象SimpleDateFormatsimpleDateFormat=newSimpleDateFormat("mm:ss");//创建一个时间对象Datedate=newDate(finalI*1000);//执行时间格式化并打印结果System.out.println(simpleDateFormat.format(date));}});}//任务执行完后关闭线程池threadPool.shutdown();}}上面程序的执行结果是:打印出来的结果都不同说明程序的执行是正确的,从上面的结果可以看出that将SimpleDateFormat定义为局部变量后,线程不安全的问题就可以顺利解决。②使用同步锁是解决线程不安全问题最常用的方法。接下来,我们首先使用synchronized来锁定时间格式化。实现代码如下:importjava.text.SimpleDateFormat;importjava.util.Date;importjava.util.concurrent.ExecutorService;importjava.util.concurrent.Executors;publicclassSimpleDateFormatExample2{//创建SimpleDateFormat对象privatestaticSimpleDateFormatsimpleDateFormat=newSimpleDateFormat("mm:SS");publicstaticvoidmain(String[]args){//创建线程池ExecutorPoolServicethreadPool=newThedreads10);//执行10次formatfor(inti=0;i<10;i++){intfinalI=i;//线程池执行任务threadPool.execute(newRunnable(){@Overridepublicvoidrun(){//创建时间对象Datedate=newDate(finalI*1000);//定义格式化结果Stringresult=null;synchronized(simpleDateFormat){//时间格式化结果=simpleDateFormat.format(date);}//打印结果System.out.println(result);}});}//任务执行完后关闭线程池threadPool.shutdown();}}以上的执行结果程序是:③使用Lock进行加锁在Java语言中,锁的常见实现方式有:二、除了synchronized,还可以使用手动加锁Lock,那么我们使用Lock改造线程不安全的代码,实现代码如下:importjava.text.SimpleDateFormat;importjava.util.Date;importjava.util.concurrent.ExecutorService;importjava.util.concurrent.Executors;importjava.util.concurrent.locks.Lock;importjava.util.concurrent.locks.ReentrantLock;/***锁解决线程不安全*/publicclassSimpleDateFormatExample3{//创建一个SimpleDateFormat对象privatestaticSimpleDateFormatsimpleDateFormat=newSimpleDateFormat("mm:ss");publicstaticvoidmain(String[]args){//创建线程池ExecutorServicethreadPool=Executors.newFixedThreadPool(10);//创建LocklockLocklock=newReentrantLock();//执行10次formatfor(inti=0;i<10;i++){intfinalI=i;//线程池执行任务threadPool.execute(newRunnable(){@Overridepublicvoidrun(){//创建时间对象Datedate=newDate(finalI*1000);//定义格式化结果Stringresult=null;//加锁lock.lock();try{//时间格式result=simpleDateFormat.format(date);}finally{//释放锁lock.unlock();}//打印结果System.out.println(result);}});}//任务执行完后关闭线程池threadPool.shutdown();}}上面程序的执行结果为:从上面的代码可以看出,手动锁比同步更麻烦④虽然使用ThreadLocal锁方案可以正确解决线程不安全的问题,但同时也引入了新的问题。加锁会让程序进入排队执行的过程,从而在一定程度上降低程序的执行效率,如下图所示:有没有解决线程不安全问题的方案,避免排队执行在同一时间?答案是肯定的,可以考虑使用ThreadLocal。ThreadLocal翻译成中文是线程局部变量的意思。ThreadLocal一词用于创建线程的私有(局部)变量。每个线程都有自己的私有对象,这样就可以避免线程不安全的问题。实现如下:知道了实现方案后,下面用具体的代码来演示一下ThreadLocal的使用。实现代码如下:importjava.text.SimpleDateFormat;importjava.util.Date;importjava.util.concurrent.ExecutorService;importjava.util.concurrent.Executors;/***ThreadLocal解决线程不安全*/publicclassSimpleDateFormatExample4{//Create一个ThreadLocal对象并设置默认值(newSimpleDateFormat)privatestaticThreadLocalthreadLocal=ThreadLocal.withInitial(()->newSimpleDateFormat("mm:ss"));publicstaticvoidmain(String[]args){//创建线程池ExecutorServicethreadPool=Executors.newFixedThreadPool(10);//执行10次格式化for(inti=0;i<10;i++){intfinalI=i;//Thread池执行任务threadPool.execute(newRunnable(){@Overridepublicvoidrun(){//创建时间对象Datedate=newDate(finalI*1000);//格式化时间Stringresult=threadLocal.get().format(date);//打印theresultSystem.out.println(result);}});}//任务执行完后关闭线程池threadPool.shutdown();}}上面程序的执行结果是:ThreadLocal和local的区别variables首先ThreadLocal不等于局部变量,这里的“局部变量”是指2.1示例代码中的局部变量。ThreadLocal与局部变量最大的区别是:ThreadLocal是线程的私有变量。如果使用线程池,可以复用ThreadLocal中的变量。是的,并且代码级别的局部变量在每次执行时都会创建新的局部变量。两者的区别如下图所示:关于ThreadLocal的更多信息可以访问雷哥之前的文章?⑤使用DateTimeFormatter上面4两种方案都是因为SimpleDateFormat不是线程安全的,所以我们需要加锁或者使用ThreadLocal来处理它。然而,在JDK8之后,我们有了新的选择。如果你使用的是JDK8+版本,可以直接使用JDK8中新的安全时间格式化工具类DateTimeFormatter来格式化时间。接下来我们具体实现一下。DateTimeFormatter的使用必须配合JDK8中新增的时间对象LocalDateTime一起使用。因此,在操作之前,我们可以将Date对象转换为LocalDateTime,然后通过DateTimeFormatter格式化时间。具体实现代码如下:importjava.time。LocalDateTime;importjava.time.ZoneId;importjava.time.format.DateTimeFormatter;importjava.util.Date;importjava.util.concurrent.ExecutorService;importjava.util.concurrent.Executors;/***DateTimeFormatter解决线程不安全*/publicclassSimpleDateFormatExample5{//创建一个DateTimeFormatter对象privatestaticDateTimeFormatterdateTimeFormatter=DateTimeFormatter.ofPattern("mm:ss");publicstaticvoidmain(String[]args){//创建线程池ExecutorServicethreadPool=Executors.newFixedThreadPool(10);//执行时间格式化10次for(inti=0;i<10;i++){intfinalI=i;//线程池执行任务threadPool.execute(newRunnable(){@Overridepublicvoidrun(){//创建时间对象Dateted=newDate(finalI*1000);//将Date转换为JDK8中的时间类型LocalDateTimeLocalDateTimelocalDateTime=LocalDateTime.ofInstant(date.toInstant(),ZoneId.systemDefault());//时间格式Stringresult=dateTimeFormatter.format(localDateTime);//打印结果System.out.println(result);}});}//任务执行完后关闭线程池threadPool.shutdown();}}上面程序的执行结果为:3.线程不安全原因分析理解为什么SimpleDateFormat是线程不安全的?我们需要查看和分析SimpleDateFormat的源码,那么我们从使用的方法format开始,源码如下:;booleanuseDateFormatSymbols=useDateFormatSymbols();for(inti=0;i>>8;intcount=compiledPattern[i++]&0xff;if(count==255){count=compiledPattern[i++]<<16;count|=compiledPattern[i++];}switch(tag){caseTAG_QUOTE_ASCII_CHAR:toAppendTo.append((char)count);break;caseTAG_QUOTE_CHARS:toAppendTo.append(compiledPattern,i,count);i+=count;break;default:subFormat(tag,count,delegate,toAppendTo,useDateFormatSymbols);break;}}returntoAppendTo;}也许是运气好,没想到开始分析的时候找到了第一个方法线程的问题不安全感从上面的源码可以看出,在执行SimpleDateFormat.format方法的时候,会使用calendar.setTime方法来转换输入的时间,那么我们想象这样一个场景:线程1执行calendar.setTime(date)方法,将用户输入的时间转换为后续格式化所需的时间;线程1暂停执行,线程2获取CPU时间片开始执行;线程2执行calendar.setTime(date)方法修改时间;线程2暂停执行,线程1发现CPU时间片继续执行,因为线程1和线程2使用同一个对象,时间已经被线程2修改了,所以此时线程1继续执行的时候,有就会出现线程安全问题了。正常情况下,程序的执行是这样的:非线程安全的执行流程是这样的:在多线程执行的情况下,最后格式化线程1的date1和线程2的date2,因为执行顺序是转换成date2格式,而不是线程1date1格式和线程2date2格式,这样会导致线程不安全。4.各方案优缺点总结如果你使用的是JDK8+版本,可以直接使用线程安全的DateTimeFormatter来格式化时间。如果你使用的是JDK8以下的版本或者修改旧的SimpleDateFormat代码,可以考虑使用synchronized或者ThreadLocal来解决线程不安全的问题。因为方案1的局部变量方案每次执行都会新建一个对象,所以不推荐。synchronized的实现比较简单,使用ThreadLocal可以避免加锁和排队执行的问题。