目录
  • simpledateformat线程为什么是线程不安全的呢?
    • 验证simpledateformat线程不安全
    • 解决方案2:加锁:synchronized锁和lock锁 加synchronized锁
      • 加lock锁
    • 解决方案3:使用threadlocal方式
      • 解决方案4:使用datetimeformatter代替simpledateformat
        • 解决方案5:使用fastdateformat 替换simpledateformat
          • fastdateformat源码分析

        场景

        在java8以前,要格式化日期时间,就需要用到simpledateformat

        但我们知道simpledateformat是线程不安全的,处理时要特别小心,要加锁或者不能定义为static,要在方法内new出对象,再进行格式化。很麻烦,而且重复地new出对象,也加大了内存开销。

        simpledateformat线程为什么是线程不安全的呢?

        来看看simpledateformat的源码,先看format方法:

        // called from format after creating a fielddelegate
            private stringbuffer format(date date, stringbuffer toappendto,
                                        fielddelegate delegate) {
                // convert input date to time field list
                calendar.settime(date);
        		...
            }
        

        问题就出在成员变量calendar,如果在使用simpledateformat时,用static定义,那simpledateformat变成了共享变量。那simpledateformat中的calendar就可以被多个线程访问到。

        simpledateformat的parse方法也是线程不安全的:

         public date parse(string text, parseposition pos)
            {
             ...
                 date parseddate;
                try {
                    parseddate = calb.establish(calendar).gettime();
                    // if the year value is ambiguous,
                    // then the two-digit year == the default start year
                    if (ambiguousyear[0]) {
                        if (parseddate.before(defaultcenturystart)) {
                            parseddate = calb.addyear(100).establish(calendar).gettime();
                        }
                    }
                }
                // an illegalargumentexception will be thrown by calendar.gettime()
                // if any fields are out of range, e.g., month == 17.
                catch (illegalargumentexception e) {
                    pos.errorindex = start;
                    pos.index = oldstart;
                    return null;
                }
        
                return parseddate;  
         }
        

        由源码可知,最后是调用**parseddate = calb.establish(calendar).gettime();**获取返回值。方法的参数是calendar,calendar可以被多个线程访问到,存在线程不安全问题。

        我们再来看看**calb.establish(calendar)**的源码

        calb.establish(calendar)方法先后调用了cal.clear()cal.set(),先清理值,再设值。但是这两个操作并不是原子性的,也没有线程安全机制来保证,导致多线程并发时,可能会引起cal的值出现问题了。

        验证simpledateformat线程不安全

        public class simpledateformatdemotest {
        
        	private static simpledateformat simpledateformat = new simpledateformat("yyyy-mm-dd hh:mm:ss");
        
            public static void main(string[] args) {
            		//1、创建线程池
                executorservice pool = executors.newfixedthreadpool(5);
                //2、为线程池分配任务
                threadpooltest threadpooltest = new threadpooltest();
                for (int i = 0; i < 10; i++) {
                    pool.submit(threadpooltest);
                }
                //3、关闭线程池
                pool.shutdown();
            }
        
        
            static class  threadpooltest implements runnable{
        
                @override
                public void run() {
        				string datestring = simpledateformat.format(new date());
        				try {
        					date parsedate = simpledateformat.parse(datestring);
        					string datestring2 = simpledateformat.format(parsedate);
        					system.out.println(thread.currentthread().getname()+" 线程是否安全: "+datestring.equals(datestring2));
        				} catch (exception e) {
        					system.out.println(thread.currentthread().getname()+" 格式化失败 ");
        				}
                }
            }
        }
        

        出现了两次false,说明线程是不安全的。而且还抛异常,这个就严重了。

        解决方案

        解决方案1:不要定义为static变量,使用局部变量

        就是要使用simpledateformat对象进行format或parse时,再定义为局部变量。就能保证线程安全。

        public class simpledateformatdemotest1 {
        
            public static void main(string[] args) {
            		//1、创建线程池
                executorservice pool = executors.newfixedthreadpool(5);
                //2、为线程池分配任务
                threadpooltest threadpooltest = new threadpooltest();
                for (int i = 0; i < 10; i++) {
                    pool.submit(threadpooltest);
                }
                //3、关闭线程池
                pool.shutdown();
            }
        
        
            static class  threadpooltest implements runnable{
        
        		@override
        		public void run() {
        			simpledateformat simpledateformat = new simpledateformat("yyyy-mm-dd hh:mm:ss");
        			string datestring = simpledateformat.format(new date());
        			try {
        				date parsedate = simpledateformat.parse(datestring);
        				string datestring2 = simpledateformat.format(parsedate);
        				system.out.println(thread.currentthread().getname()+" 线程是否安全: "+datestring.equals(datestring2));
        			} catch (exception e) {
        				system.out.println(thread.currentthread().getname()+" 格式化失败 ");
        			}
        		}
            }
        }
        

        由图可知,已经保证了线程安全,但这种方案不建议在高并发场景下使用,因为会创建大量的simpledateformat对象,影响性能。

        解决方案2:加锁:synchronized锁和lock锁 加synchronized锁

        simpledateformat对象还是定义为全局变量,然后需要调用simpledateformat进行格式化时间时,再用synchronized保证线程安全。

        public class simpledateformatdemotest2 {
        
        	private static simpledateformat simpledateformat = new simpledateformat("yyyy-mm-dd hh:mm:ss");
        
            public static void main(string[] args) {
            		//1、创建线程池
                executorservice pool = executors.newfixedthreadpool(5);
                //2、为线程池分配任务
                threadpooltest threadpooltest = new threadpooltest();
                for (int i = 0; i < 10; i++) {
                    pool.submit(threadpooltest);
                }
                //3、关闭线程池
                pool.shutdown();
            }
        
            static class  threadpooltest implements runnable{
        
        		@override
        		public void run() {
        			try {
        				synchronized (simpledateformat){
        					string datestring = simpledateformat.format(new date());
        					date parsedate = simpledateformat.parse(datestring);
        					string datestring2 = simpledateformat.format(parsedate);
        					system.out.println(thread.currentthread().getname()+" 线程是否安全: "+datestring.equals(datestring2));
        				}
        			} catch (exception e) {
        				system.out.println(thread.currentthread().getname()+" 格式化失败 ");
        			}
        		}
            }
        }
        

        如图所示,线程是安全的。定义了全局变量simpledateformat,减少了创建大量simpledateformat对象的损耗。但是使用synchronized锁,
        同一时刻只有一个线程能执行锁住的代码块,在高并发的情况下会影响性能。但这种方案不建议在高并发场景下使用

        加lock锁

        加lock锁和synchronized锁原理是一样的,都是使用锁机制保证线程的安全。

        public class simpledateformatdemotest3 {
        
        	private static simpledateformat simpledateformat = new simpledateformat("yyyy-mm-dd hh:mm:ss");
        	private static lock lock = new reentrantlock();
            public static void main(string[] args) {
            		//1、创建线程池
                executorservice pool = executors.newfixedthreadpool(5);
                //2、为线程池分配任务
                threadpooltest threadpooltest = new threadpooltest();
                for (int i = 0; i < 10; i++) {
                    pool.submit(threadpooltest);
                }
                //3、关闭线程池
                pool.shutdown();
            }
        
            static class  threadpooltest implements runnable{
        
        		@override
        		public void run() {
        			try {
        				lock.lock();
        					string datestring = simpledateformat.format(new date());
        					date parsedate = simpledateformat.parse(datestring);
        					string datestring2 = simpledateformat.format(parsedate);
        					system.out.println(thread.currentthread().getname()+" 线程是否安全: "+datestring.equals(datestring2));
        			} catch (exception e) {
        				system.out.println(thread.currentthread().getname()+" 格式化失败 ");
        			}finally {
        				lock.unlock();
        			}
        		}
            }
        }
        

        由结果可知,加lock锁也能保证线程安全。要注意的是,最后一定要释放锁,代码里在finally里增加了lock.unlock();,保证释放锁。
        在高并发的情况下会影响性能。这种方案不建议在高并发场景下使用

        解决方案3:使用threadlocal方式

        使用threadlocal保证每一个线程有simpledateformat对象副本。这样就能保证线程的安全。

        public class simpledateformatdemotest4 {
        
        	private static threadlocal<dateformat> threadlocal = new threadlocal<dateformat>(){
        		@override
        		protected dateformat initialvalue() {
        			return new simpledateformat("yyyy-mm-dd hh:mm:ss");
        		}
        	};
            public static void main(string[] args) {
            		//1、创建线程池
                executorservice pool = executors.newfixedthreadpool(5);
                //2、为线程池分配任务
                threadpooltest threadpooltest = new threadpooltest();
                for (int i = 0; i < 10; i++) {
                    pool.submit(threadpooltest);
                }
                //3、关闭线程池
                pool.shutdown();
            }
        
            static class  threadpooltest implements runnable{
        
        		@override
        		public void run() {
        			try {
        					string datestring = threadlocal.get().format(new date());
        					date parsedate = threadlocal.get().parse(datestring);
        					string datestring2 = threadlocal.get().format(parsedate);
        					system.out.println(thread.currentthread().getname()+" 线程是否安全: "+datestring.equals(datestring2));
        			} catch (exception e) {
        				system.out.println(thread.currentthread().getname()+" 格式化失败 ");
        			}finally {
        				//避免内存泄漏,使用完threadlocal后要调用remove方法清除数据
        				threadlocal.remove();
        			}
        		}
            }
        }
        

        使用threadlocal能保证线程安全,且效率也是挺高的。适合高并发场景使用

        解决方案4:使用datetimeformatter代替simpledateformat

        使用datetimeformatter代替simpledateformat(datetimeformatter是线程安全的,java 8+支持)
        datetimeformatter介绍 传送门:

        public class datetimeformatterdemotest5 {
        	private static datetimeformatter datetimeformatter = datetimeformatter.ofpattern("yyyy-mm-dd hh:mm:ss");
        
        	public static void main(string[] args) {
        		//1、创建线程池
        		executorservice pool = executors.newfixedthreadpool(5);
        		//2、为线程池分配任务
        		threadpooltest threadpooltest = new threadpooltest();
        		for (int i = 0; i < 10; i++) {
        			pool.submit(threadpooltest);
        		}
        		//3、关闭线程池
        		pool.shutdown();
        	}
        
        
        	static class  threadpooltest implements runnable{
        
        		@override
        		public void run() {
        			try {
        				string datestring = datetimeformatter.format(localdatetime.now());
        				temporalaccessor temporalaccessor = datetimeformatter.parse(datestring);
        				string datestring2 = datetimeformatter.format(temporalaccessor);
        				system.out.println(thread.currentthread().getname()+" 线程是否安全: "+datestring.equals(datestring2));
        			} catch (exception e) {
        				e.printstacktrace();
        				system.out.println(thread.currentthread().getname()+" 格式化失败 ");
        			}
        		}
        	}
        }
        

        使用datetimeformatter能保证线程安全,且效率也是挺高的。适合高并发场景使用

        解决方案5:使用fastdateformat 替换simpledateformat

        使用fastdateformat 替换simpledateformat(fastdateformat 是线程安全的,apache commons lang包支持,不受限于java版本)

        public class datetimeformatterdemotest5 {
        	private static datetimeformatter datetimeformatter = datetimeformatter.ofpattern("yyyy-mm-dd hh:mm:ss");
        
        	public static void main(string[] args) {
        		//1、创建线程池
        		executorservice pool = executors.newfixedthreadpool(5);
        		//2、为线程池分配任务
        		threadpooltest threadpooltest = new threadpooltest();
        		for (int i = 0; i < 10; i++) {
        			pool.submit(threadpooltest);
        		}
        		//3、关闭线程池
        		pool.shutdown();
        	}
        
        
        	static class  threadpooltest implements runnable{
        
        		@override
        		public void run() {
        			try {
        				string datestring = datetimeformatter.format(localdatetime.now());
        				temporalaccessor temporalaccessor = datetimeformatter.parse(datestring);
        				string datestring2 = datetimeformatter.format(temporalaccessor);
        				system.out.println(thread.currentthread().getname()+" 线程是否安全: "+datestring.equals(datestring2));
        			} catch (exception e) {
        				e.printstacktrace();
        				system.out.println(thread.currentthread().getname()+" 格式化失败 ");
        			}
        		}
        	}
        }
        

        使用fastdateformat能保证线程安全,且效率也是挺高的。适合高并发场景使用

        fastdateformat源码分析

         apache commons lang 3.5
        //fastdateformat
        @overridepublic string format(final date date) {
            return printer.format(date);
        }
        @override public string format(final date date) 
        {
            final calendar c = calendar.getinstance(timezone, locale);
            c.settime(date);
            return applyrulestostring(c);
        }

        源码中 calender 是在 format 方法里创建的,肯定不会出现 settime 的线程安全问题。这样线程安全疑惑解决了。那还有性能问题要考虑?

        我们来看下fastdateformat是怎么获取的

        fastdateformat.getinstance();fastdateformat.getinstance(chinese_date_time_pattern);
        

        看下对应的源码

        /**
         * 获得 fastdateformat实例,使用默认格式和地区 *
         * @return fastdateformat 
        */
        public static fastdateformat getinstance() {
            return cache.getinstance();
        }
        /**
         * 获得 fastdateformat 实例,使用默认地区
         * 支持缓存 * 
        * @param pattern 使用{@link java.text.simpledateformat} 相同的日期格式
         * @return fastdateformat 
        * @throws illegalargumentexception 日期格式问题 
        */
        public static fastdateformat getinstance(final string pattern) {
            return cache.getinstance(pattern, null, null);
        }

        这里有用到一个cache,看来用了缓存,往下看

        private static final formatcache < fastdateformat > cache = new formatcache < fastdateformat > ()
        {
            @override protected fastdateformat createinstance(final string pattern, final timezone timezone, final locale locale) {
                return new fastdateformat(pattern, timezone, locale);
            }
        };
        //abstract class formatcache<f extends format>
        
        {
            ... private final concurrentmap < tuple, f > cinstancecache = new concurrenthashmap <> (7);
            private static final concurrentmap < tuple, string > c_date_time_instance_cache = new concurrenthashmap <> (7);
            ...
        }

        在getinstance 方法中加了concurrentmap 做缓存,提高了性能。且我们知道concurrentmap 也是线程安全的。

        实践

        /**
         * 年月格式 {@link fastdateformat}:yyyy-mm
         */
        public static final fastdateformat norm_month_format = fastdateformat.getinstance(norm_month_pattern);
        

        //fastdateformat
        public static fastdateformat getinstance(final string pattern) {
            return cache.getinstance(pattern, null, null);
        }

        如图可证,是使用了concurrentmap 做缓存。且key值是格式,时区和locale(语境)三者都相同为相同的key。

        结论

        这个是阿里巴巴 java开发手册中的规定:

        1、不要定义为static变量,使用局部变量

        2、加锁:synchronized锁和lock锁

        3、使用threadlocal方式

        4、使用datetimeformatter代替simpledateformat(datetimeformatter是线程安全的,java 8+支持)

        5、使用fastdateformat 替换simpledateformat(fastdateformat 是线程安全的,apache commons lang包支持,java8之前推荐此用法)

        到此这篇关于java的simpledateformat线程不安全的几种解决方案的文章就介绍到这了,更多相关java simpledateformat线程不安全内容请搜索www.887551.com以前的文章或继续浏览下面的相关文章希望大家以后多多支持www.887551.com!