-
2020-05-16 19:38:25
简介
本文作为多线程编程的第一篇文章,将从一个简单的例子开始,带你真正从代码层次理解什么是线程不安全,以及为什么会出现线程不安全的情况。文章中将提供一个完整的线程不安全示例,希望你可以跟随文章,自己真正动手运行一下此程序,体会一下多线程编程中必须要考虑的线程安全问题。
一.什么是线程安全
《Java Concurrency In Practice》作者Brian Goetz的定义:“当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象是线程安全的。
为什么不把所有操作都做成线程安全的?
实现线程安全是有成本的,比如线程安全的程序运行速度会相对较慢、开发的复杂度也提高了,提高了人力成本。
二.从经典的线程不安全的示例开始
经典案例: 两个线程,共同读写一个全局变量
count
,每个线程执行10000次count++
,count
的最终结果会是20000吗,在心中猜测一下运行结果?经典案例的代码实现:
package com.study.synchronize.object; /** * 线程不安全案例:两个线程同时累加同一个变量,结果值会小于实际值 */ public class ConcurrentProblem implements Runnable { private static ConcurrentProblem concurrentProblem = new ConcurrentProblem(); private static int count; public static void main(String[] args) { Thread thread1 = new Thread(concurrentProblem); Thread thread2 = new Thread(concurrentProblem); thread1.start(); thread2.start(); try { // 等待两个线程都运行结束后,再打印结果 thread1.join(); thread2.join(); } catch (InterruptedException e) { e.printStackTrace(); } //期待结果是20000,但是结果会小于这个值 System.out.println(count); } /** * 多线程问题原因:count++这行代码要分三步执行;1:读取;2:修改;3:写入。 * 在这三步中,任何一步都可能被其他线程打断,导致值还没来得及写入,就被其他线程读取或写入,这就是多线程并行操作统一变量导致的问题。 */ @Override public void run() { for (int k = 0; k < 10000; k++) { count++; } } }
多次运行结果:
count
最终值会小于等于20000。三.剖析问题:多线程累加为什么会有小于预期值这种情况呢
1.理解JVM如何执行
count++
程序执行
count++
这个操作时,JVM将会分为三个步骤完成(非原子性):- 某线程从内存中读取
count
。 - 某线程修改
count
值。 - 某线程将
count
重新写入内存。
这个操作过程应该很好理解,你可简单的类比为
把大象装进冰箱里的三个步骤
。在单线程中执行上述代码是不会出现小于2万
的这种情况,为什么多线程就发生了跟预期不一致的情况呢?为了彻底弄清楚这个问题,你需要先理解什么是线程?线程像
病毒
一样,不能够独立的存活于世间,需要寄生在宿主细胞
中。线程也是不能够独立的生存在系统中,线程
需要依附于进程
存在。什么是进程?进程是代码在数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位 线程是进程的一个执行路径,一个进程至少有一个线程,多个线程则会共享进程中的资源。
2.理解问题的根源:
有了对线程的认识后,我们再去思考,
count++
的3个步骤,由于线程会共享进程中的资源,所以在这三步中,任何一步都可能被其他线程打断,导致count
值还没来得及写入,就被其他线程读取或写入。3.脑补还原出错的流程:
- 假如
count
值为1,线程1读取到count
值后,将count
修改为2,此时还没来得及将结果写入内存,内存中的count值还是1。 - 另一个线程2,读取到
count
值为1后,也将其修改为2,并成功写入内存中,此时内存中的count
值变为了2。 - 随后线程1也将
count
的结果2写入到内存中,count
在内存中的结果依然是2(理应为3)。
上述场景中,两个线程各自执行了一次
count++
,但count值却只增加了1,这就是问题所在。总结
多线程可以并行执行一些任务,提高处理效率,但也同时带来了新的问题,也就是线程安全问题,多个线程之间,操作同一资源时,也出现了让人意向不到的的情况,其原因是这些操作可能不是原子性操作,简单的说,我们肉眼看起来程序执行了一步操作,但在JVM中可能需要分多个步骤执行,多个线程可能会打乱了JVM的执行顺序,随后也就发生了不可预知的问题。
那么在Java中,怎么应对这种问题呢?Java随着版本的升级,提供了很多解决方案,比如:Concurrent包中的类。但我们下一篇文章,将讲解一种最简单、最方便的一种解决方案,上述案例代码仅仅通过增加一个单词,就可以轻松避免线程安全的问题,它就是
synchronized
关键字。喜欢本文,请收藏和点赞,也请继续阅读本专栏的其他文章,本专栏将结合各种场景代码,彻底讲透彻java中的并发问题和
synchronized
各种使用场景。更多相关内容 - 某线程从内存中读取
-
HashMap为什么线程不安全?
2021-02-04 17:01:161、HashMap线程不安全原因: 原因: JDK1.7 中,由于多线程对HashMap进行扩容,调用了HashMap#transfer(),具体原因:某个线程执行过程中,被挂起,其他线程已经完成数据迁移,等CPU资源释放后被挂起的线程重新...一、学习目标
1、HashMap线程不安全原因:
原因:
- JDK1.7 中,由于多线程对HashMap进行扩容,调用了HashMap#transfer(),具体原因:某个线程执行过程中,被挂起,其他线程已经完成数据迁移,等CPU资源释放后被挂起的线程重新执行之前的逻辑,数据已经被改变,造成死循环、数据丢失。
- JDK1.8 中,由于多线程对HashMap进行put操作,调用了HashMap#putVal(),具体原因:假设两个线程A、B都在进行put操作,并且hash函数计算出的插入下标是相同的,当线程A执行完第六行代码后由于时间片耗尽导致被挂起,而线程B得到时间片后在该下标处插入了元素,完成了正常的插入,然后线程A获得时间片,由于之前已经进行了hash碰撞的判断,所有此时不会再进行判断,而是直接进行插入,这就导致了线程B插入的数据被线程A覆盖了,从而线程不安全。
改善:
- 数据丢失、死循环已经在在JDK1.8中已经得到了很好的解决,如果你去阅读1.8的源码会发现找不到HashMap#transfer(),因为JDK1.8直接在HashMap#resize()中完成了数据迁移。
2、HashMap线程不安全的体现:
- JDK1.7 HashMap线程不安全体现在:死循环、数据丢失
- JDK1.8 HashMap线程不安全体现在:数据覆盖
二、HashMap线程不安全、死循环、数据丢失、数据覆盖的原因
1、JDK1.7 扩容引发的线程不安全
HashMap的线程不安全主要是发生在扩容函数中,其中调用了JDK1.7 HshMap#transfer():
void transfer(Entry[] newTable, boolean rehash) { int newCapacity = newTable.length; for (Entry<K,V> e : table) { while(null != e) { Entry<K,V> next = e.next; if (rehash) { e.hash = null == e.key ? 0 : hash(e.key); } int i = indexFor(e.hash, newCapacity); e.next = newTable[i]; newTable[i] = e; e = next; } } }
这段代码是HashMap的扩容操作,重新定位每个桶的下标,并采用头插法将元素迁移到新数组中。头插法会将链表的顺序翻转,这也是形成死循环的关键点。理解了头插法后再继续往下看是如何造成死循环以及数据丢失的。
2、扩容造成死循环和数据丢失
假设现在有两个线程A、B同时对下面这个HashMap进行扩容操作:
正常扩容后的结果是下面这样的:
但是当线程A执行到上面transfer函数的第11行代码时,CPU时间片耗尽,线程A被挂起。即如下图中位置所示:
此时线程A中:e=3、next=7、e.next=null
当线程A的时间片耗尽后,CPU开始执行线程B,并在线程B中成功的完成了数据迁移
重点来了,根据Java内存模式可知,线程B执行完数据迁移后,此时主内存中newTable和table都是最新的,也就是说:7.next=3、3.next=null。
随后线程A获得CPU时间片继续执行newTable[i] = e,将3放入新数组对应的位置,执行完此轮循环后线程A的情况如下:
接着继续执行下一轮循环,此时e=7,从主内存中读取e.next时发现主内存中7.next=3,此时next=3,并将7采用头插法的方式放入新数组中,并继续执行完此轮循环,结果如下:
此时没任何问题。
上轮next=3,e=3,执行下一次循环可以发现,3.next=null,所以此轮循环将会是最后一轮循环。
接下来当执行完e.next=newTable[i]即3.next=7后,3和7之间就相互连接了,当执行完newTable[i]=e后,3被头插法重新插入到链表中,执行结果如下图所示:
上面说了此时e.next=null即next=null,当执行完e=null后,将不会进行下一轮循环。到此线程A、B的扩容操作完成,很明显当线程A执行完后,HashMap中出现了环形结构,当在以后对该HashMap进行操作时会出现死循环。
并且从上图可以发现,元素5在扩容期间被莫名的丢失了,这就发生了数据丢失的问题。
3、JDK1.8中的线程不安全
上面的扩容造成的数据丢失、死循环已经在在JDK1.8中已经得到了很好的解决,如果你去阅读1.8的源码会发现找不到HashMap#transfer(),因为JDK1.8直接在HashMap#resize()中完成了数据迁移。
为什么说 JDK1.8会出现数据覆盖的情况? 我们来看一下下面这段JDK1.8中的put操作代码:
其中第六行代码是判断是否出现hash碰撞,假设两个线程A、B都在进行put操作,并且hash函数计算出的插入下标是相同的,当线程A执行完第六行代码后由于时间片耗尽导致被挂起,而线程B得到时间片后在该下标处插入了元素,完成了正常的插入,然后线程A获得时间片,由于之前已经进行了hash碰撞的判断,所有此时不会再进行判断,而是直接进行插入,这就导致了线程B插入的数据被线程A覆盖了,从而线程不安全。
除此之前,还有就是代码的第38行处有个++size,我们这样想,还是线程A、B,这两个线程同时进行put操作时,假设当前HashMap的zise大小为10,当线程A执行到第38行代码时,从主内存中获得size的值为10后准备进行+1操作,但是由于时间片耗尽只好让出CPU,线程B快乐的拿到CPU还是从主内存中拿到size的值10进行+1操作,完成了put操作并将size=11写回主内存,然后线程A再次拿到CPU并继续执行(此时size的值仍为10),当执行完put操作后,还是将size=11写回内存,此时,线程A、B都执行了一次put操作,但是size的值只增加了1,所有说还是由于数据覆盖又导致了线程不安全。
三、如何使HashMap在多线程情况下进行线程安全操作?
使用 Collections.synchronizedMap(map),包装成同步Map,原理就是在HashMap的所有方法上synchronized。
例如:Collections.SynchronizedMap#get()
public V get(Object key) { synchronized (mutex) { return m.get(key); } } 复制代码
我认为主要可以通过以下三种方法来实现:
1.替换成Hashtable,Hashtable通过对整个表上锁实现线程安全,因此效率比较低
2.使用Collections类的synchronizedMap方法包装一下。方法如下:
public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m) 返回由指定映射支持的同步(线程安全的)映射
3.使用ConcurrentHashMap,它使用分段锁来保证线程安全
通过前两种方式获得的线程安全的HashMap在读写数据的时候会对整个容器上锁,而ConcurrentHashMap并不需要对整个容器上锁,它只需要锁住要修改的部分就行了
四、总结
1、HashMap线程不安全原因:
原因:
- JDK1.7 中,由于多线程对HashMap进行扩容,调用了HashMap#transfer(),具体原因:某个线程执行过程中,被挂起,其他线程已经完成数据迁移,等CPU资源释放后被挂起的线程重新执行之前的逻辑,数据已经被改变,造成死循环、数据丢失。
- JDK1.8 中,由于多线程对HashMap进行put操作,调用了HashMap#putVal(),具体原因:假设两个线程A、B都在进行put操作,并且hash函数计算出的插入下标是相同的,当线程A执行完第六行代码后由于时间片耗尽导致被挂起,而线程B得到时间片后在该下标处插入了元素,完成了正常的插入,然后线程A获得时间片,由于之前已经进行了hash碰撞的判断,所有此时不会再进行判断,而是直接进行插入,这就导致了线程B插入的数据被线程A覆盖了,从而线程不安全。
改善:
- 数据丢失、死循环已经在在JDK1.8中已经得到了很好的解决,如果你去阅读1.8的源码会发现找不到HashMap#transfer(),因为JDK1.8直接在HashMap#resize()中完成了数据迁移。
2、HashMap线程不安全的体现:
- JDK1.7 HashMap线程不安全体现在:死循环、数据丢失
- JDK1.8 HashMap线程不安全体现在:数据覆盖
转载自:https://juejin.cn/post/6917526751199526920
-
java的SimpleDateFormat线程不安全出问题了,虚竹教你多种解决方案
2021-08-05 17:12:14我们知道SimpleDateFormat是线程不安全,本文会介绍多种解决方案来保证线程安全。技术活,该赏 点赞再看,养成习惯
场景
在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介绍 传送门:万字博文教你搞懂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()+" 格式化失败 "); } } } }
使用DateTimeFormatter能保证线程安全,且效率也是挺高的。适合高并发场景使用。解决方案5:使用FastDateFormat 替换SimpleDateFormat
使用FastDateFormat 替换SimpleDateFormat(FastDateFormat 是线程安全的,Apache Commons Lang包支持,不受限于java版本)
public class FastDateFormatDemo6 { private static FastDateFormat fastDateFormat = FastDateFormat.getInstance("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 = fastDateFormat.format(new Date()); Date parseDate = fastDateFormat.parse(dateString); String dateString2 = fastDateFormat.format(parseDate); 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 实例,使用默认地区<br> * 支持缓存 * * @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);
//FastDateFormatpublic 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之前推荐此用法)
推荐相关文章
hutool日期时间系列文章
2DateUtil(时间工具类)-常用的时间类型Date,DateTime,Calendar和TemporalAccessor(LocalDateTime)转换
9LocalDateTimeUtil(JDK8+中的{@link LocalDateTime} 工具类封装)
10TemporalAccessorUtil{@link TemporalAccessor} 工具类封装
其他
-
为什么StringBuilder是线程不安全的?
2020-09-17 17:58:21在前面的面试题讲解中我们对比了String、StringBuilder和StringBuffer的区别,其中一项便提到StringBuilder是非线程安全的,那么是什么原因导致了StringBuilder的线程不安全呢? 原因分析 如果你看了StringBuilder或...在前面的面试题讲解中我们对比了String、StringBuilder和StringBuffer的区别,其中一项便提到StringBuilder是非线程安全的,那么是什么原因导致了StringBuilder的线程不安全呢?
原因分析
如果你看了StringBuilder或StringBuffer的源代码会说,因为StringBuilder在append操作时并未使用线程同步,而StringBuffer几乎大部分方法都使用了synchronized关键字进行方法级别的同步处理。
上面这种说法肯定是正确的,对照一下StringBuilder和StringBuffer的部分源代码也能够看出来。
StringBuilder的append方法源代码:
@Override public StringBuilder append(String str) { super.append(str); return this; }
StringBuffer的append方法源代码:
@Override public synchronized StringBuffer append(String str) { toStringCache = null; super.append(str); return this; }
对于上面的结论肯定是没什么问题的,但并没有解释是什么原因导致了StringBuilder的线程不安全?为什么要使用synchronized来保证线程安全?如果不是用会出现什么异常情况?
下面我们来逐一讲解。
异常示例
我们先来跑一段代码示例,看看出现的结果是否与我们的预期一致。
@Test public void test() throws InterruptedException { StringBuilder sb = new StringBuilder(); for (int i = 0; i < 10; i++) { new Thread(() -> { for (int j = 0; j < 1000; j++) { sb.append("a"); } }).start(); } // 睡眠确保所有线程都执行完 Thread.sleep(1000); System.out.println(sb.length()); }
上述业务逻辑比较简单,就是构建一个StringBuilder,然后创建10个线程,每个线程中拼接字符串“a”1000次,理论上当线程执行完成之后,打印的结果应该是10000才对。
但多次执行上面的代码打印的结果是10000的概率反而非常小,大多数情况都要少于10000。同时,还有一定的概率出现下面的异常信息“
Exception in thread "Thread-0" java.lang.ArrayIndexOutOfBoundsException at java.lang.System.arraycopy(Native Method) at java.lang.String.getChars(String.java:826) at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:449) at java.lang.StringBuilder.append(StringBuilder.java:136) at com.secbro2.strings.StringBuilderTest.lambda$test$0(StringBuilderTest.java:18) at java.lang.Thread.run(Thread.java:748) 9007
线程不安全的原因
StringBuilder中针对字符串的处理主要依赖两个成员变量char数组value和count。StringBuilder通过对value的不断扩容和count对应的增加来完成字符串的append操作。
// 存储的字符串(通常情况一部分为字符串内容,一部分为默认值) char[] value; // 数组已经使用数量 int count;
上面的这两个属性均位于它的抽象父类AbstractStringBuilder中。
如果查看构造方法我们会发现,在创建StringBuilder时会设置数组value的初始化长度。
public StringBuilder(String str) { super(str.length() + 16); append(str); }
默认是传入字符串长度加16。这就是count存在的意义,因为数组中的一部分内容为默认值。
当调用append方法时会对count进行增加,增加值便是append的字符串的长度,具体实现也在抽象父类中。
public AbstractStringBuilder append(String str) { if (str == null) return appendNull(); int len = str.length(); ensureCapacityInternal(count + len); str.getChars(0, len, value, count); count += len; return this; }
我们所说的线程不安全的发生点便是在append方法中count的“+=”操作。我们知道该操作是线程不安全的,那么便会发生两个线程同时读取到count值为5,执行加1操作之后,都变成6,而不是预期的7。这种情况一旦发生便不会出现预期的结果。
抛异常的原因
回头看异常的堆栈信息,回发现有这么一行内容:
at java.lang.String.getChars(String.java:826)
对应的代码就是上面AbstractStringBuilder中append方法中的代码。对应方法的源代码如下:
public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) { if (srcBegin < 0) { throw new StringIndexOutOfBoundsException(srcBegin); } if (srcEnd > value.length) { throw new StringIndexOutOfBoundsException(srcEnd); } if (srcBegin > srcEnd) { throw new StringIndexOutOfBoundsException(srcEnd - srcBegin); } System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin); }
其实异常是最后一行arraycopy时JVM底层发生的。arraycopy的核心操作就是将传入的String对象copy到value当中。
而异常发生的原因是明明value的下标只到6,程序却要访问和操作下标为7的位置,当然就跑异常了。
那么,为什么会超出这么一个位置呢?这与我们上面讲到到的count被少加有关。在执行str.getChars方法之前还需要根据count校验一下当前的value是否使用完毕,如果使用完了,那么就进行扩容。append中对应的方法如下:
ensureCapacityInternal(count + len);
ensureCapacityInternal的具体实现:
private void ensureCapacityInternal(int minimumCapacity) { // overflow-conscious code if (minimumCapacity - value.length > 0) { value = Arrays.copyOf(value, newCapacity(minimumCapacity)); } }
count本应该为7,value长度为6,本应该触发扩容。但因为并发导致count为6,假设len为1,则传递的minimumCapacity为7,并不会进行扩容操作。这就导致后面执行str.getChars方法进行复制操作时访问了不存在的位置,因此抛出异常。
这里我们顺便看一下扩容方法中的newCapacity方法:
private int newCapacity(int minCapacity) { // overflow-conscious code int newCapacity = (value.length << 1) + 2; if (newCapacity - minCapacity < 0) { newCapacity = minCapacity; } return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0) ? hugeCapacity(minCapacity) : newCapacity; }
除了校验部分,最核心的就是将新数组的长度扩充为原来的两倍再加2。把计算所得的新长度作为Arrays.copyOf的参数进行扩容。
小结
经过上面的分析,是不是真正了解了StringBuilder的线程不安全的原因?我们在学习和实践的过程中,不仅要知道一些结论,还要知道这些结论的底层原理,更重要的是学会分析底层原理的方法。
原文链接:《为什么StringBuilder是线程不安全的?》
程序新视界
公众号“ 程序新视界”,一个让你软实力、硬技术同步提升的平台,提供海量资料
-
为什么HashMap线程不安全
2021-06-30 14:30:24我们都知道HashMap是线程不安全的,但是HashMap的使用频率在所有map中确实属于比较高的。因为它可以满足我们大多数的场景了。 Map类继承图 上面展示了java中Map的继承图,Map是一个接口,我们常用的实现类有... -
SimpleDateFormat为什么是线程不安全的?
2022-02-07 15:26:44SimpleDateFormat为什么是线程不安全的? 其实这已经是个老生常谈的问题了,这篇博客旨在提醒自己,方便回忆。同时希望可以帮助到需要的伙伴。如果有哪些地方有错误或不合适,欢迎大家指出。 我不知道它是线程不安全的... -
图解HashMap为什么线程不安全?
2020-06-10 16:43:01HashMap的线程不安全主要体现在下面两个方面: 1.在JDK1.7中,当并发执行扩容操作时会造成环形链和数据丢失的情况。 2.在JDK1.8中,在并发执行put操作时会发生数据覆盖的情况。 JDK1.7 在JDK1.7中,扩容数据时要进行... -
Java集合线程不安全问题总结
2022-02-09 19:38:31Java集合线程不安全之并发修改异常 上代码 public class Demo { public static void main(String[] args) { List<String> myList = new ArrayList<>(); // 启动30个线程同时对list进行操作 for (int... -
LinkedHashMap线程不安全解决
2022-02-17 20:14:41LinkedHashMap线程不安全解决 -
线程安全与线程不安全
2019-06-08 16:08:291、是线程安全与线程不安全 线程安全就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可使用。不会出现数据不一致或者数据污染。 ... -
为什么StringBuilder是线程不安全的而StringBuffer是线程安全的
2019-10-08 13:55:07为什么StringBuilder是线程不安全的 为什么StringBuffer是线程安全的 -
ArrayList线程不安全与Vector线程安全
2018-08-30 10:46:22首先说一下什么是线程不安全:线程安全就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可使用。不会出现数据不一致或者数据污染。... -
【并发】为什么HashMap是线程不安全的?
2020-03-23 13:43:01经常会看到说HashMap是线程不安全的,ConcurrentHashMap是线程安全的等等说法,不禁有个疑问,什么是线程安全?什么样的类是线程安全的? 1.什么是线程安全性(what) 线程安全定义,最核心是正确性, 正确性:多... -
简述:如何解决HashMap线程不安全的问题?
2021-11-14 07:55:37jdk1.8中HashMap为什么线程不安全? 会出现数据覆盖。 JDK1.7和JDK1.8中HashMap为什么是线程不安全的? 怎么解决HashMap线程不安全的问题? 1.使用HashTable替代HashMap HashTable的put操作,有synchronized... -
【Java】HashMap线程不安全
2020-04-02 00:26:03大家都知道HashMap不安全,HashTable是安全的,HashTable安全是因为加了synchronized锁,那今天来看下HashMap为何不安全 jdk1.7中的HashMap 看源码 /** * Transfers all entries from current table to ... -
解决线程不安全的list的问题(Vector、Collections、CopyOnWriteArrayList)
2022-01-02 11:03:30解决线程不安全的list的问题(Vector、Collections、CopyOnWriteArrayList) -
容易忽略的ConcurrentHashMap 线程不安全行为
2019-01-21 15:27:32线程安全的基础知识 首先解释什么是线程安全:在多线程中对一种数据类型的参数进行共享时,各个线程可以正确的执行,不会出现数据错误的情况就是线程安全。 接下来我们看一段常见的线程代码: public class ... -
SimpleDateFormat为什么是线程不安全
2020-09-03 19:10:22SimpleDateFormat是 Java 提供的个格式化和解析日期的工具类,但是你是否在夜深人静的时候想过,自己通过SimpleDateFormat格式化日期的时候会不会出现线程安全方面的问题呢? 李四就是这样,清晨,第二天顶着两个... -
线程不安全的原因及其解决
2020-06-11 23:18:44一、什么是线程不安全和线程安全 定义:线程不安全:多线程并发执行某个代码时,产生了逻辑上的错误,结果和预期值不相同 线程安全是指多线程执行时没有产生逻辑错误,结果和预期值相同 二、线程不安全产生的原因 ... -
什么是线程安全和线程不安全
2019-12-02 16:57:421、线程安全: 指多个线程在执行同一段代码的时候采用加锁机制,使每次的执行结果和单线程执行的结果都是一样的,不存在执行程序时出现意外结果。 2、线程不安全: 是指不提供加锁机制保护,有可能出现多个线程... -
SimpleDateFormat线程不安全及解决办法
2017-06-10 15:15:23以前没有注意到SimpleDateFormat线程不安全的问题,写时间工具类,一般写成静态的成员变量,不知,此种写法的危险性!在此讨论一下SimpleDateFormat线程不安全问题,以及解决方法。 为什么SimpleDateFormat不安全? ... -
为什么StringBuilder线程不安全,但StringBuffer线程安全?
2020-08-16 22:51:47既然使用同一个方法,那为什么说StringBuilder线程不安全呢? 原因在于StringBuffer在append()方法上使用了synchronized,而StringBuilder没有使用。 以下是来自AbstractStringBuilder的append()方法的具体实现: 1 ... -
java中哪些集合是线程安全的,哪些是线程不安全的
2019-05-09 11:41:42线程安全和线程不安全的集合3. 如何综合考虑线程不安全和效率低的问题 1. 常见集合 这里自己总结了一些比较常见的java集合,对于这些集合的特点和区别后期博客中会进行总结介绍: 2.什么叫“集合是线程不安全的” ... -
为什么StringBuilder是线程不安全的
2019-08-30 14:40:12通常我们都知道说StringBuilder是线程不安全的,那如果继续追问下去,为什么StringBuilder是线程不安全的,该怎么回答呢? 首先需要明确地知道StringBuilder它内部的组织结构 来看源代码中,StringBuilder的抽象... -
i++ 线程不安全示例详解
2019-07-04 13:17:59一个线程不安全的计数器 package com.thread.xgb; public class UnsafeCounter { public int count = 0; public void add() { count++; } public int get() { return count; } } 编写一个简单的... -
String,StringBuffer与StringBuilder的区别|线程安全与线程不安全
2018-11-16 12:38:45最新的详细测试 https://www.cnblogs.com/shangxiaofei/p/10465031.html ... String 字符串常量 StringBuffer 字符串变量(线程安全) StringBuilder 字符串变量(非线程安全) 简要的说, String 类型... -
StringBuilder为什么线程不安全?
2019-08-30 10:08:24作者:千山 juejin.im/post/5d6228046fb...我:StringBuilder不是线程安全的,StringBuffer是线程安全的 面试官:那StringBuilder不安全的点在哪儿? 我:。。。(哑巴了) 在这之前我只记住了StringBuilder不是... -
JDK1.7和JDK1.8中HashMap为什么是线程不安全的?
2019-03-30 19:57:35只要是对于集合有一定了解的一定都知道HashMap是线程不安全的,我们应该使用ConcurrentHashMap。但是为什么HashMap是线程不安全的呢,之前面试的时候也遇到到这样的问题,但是当时只停留在***知道是***的层面上,并...