精华内容
下载资源
问答
  • Java中线程安全和线程安全解析和示例
    千次阅读
    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将会分为三个步骤完成(非原子性):

    1. 某线程从内存中读取count
    2. 某线程修改count值。
    3. 某线程将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:16
    1、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

     

     

     

     

     

     

     

     

     

     

     

     

     

    展开全文
  • 我们知道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)**的源码

    image-20210805827464

    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()+" 格式化失败 ");
    				}
            }
        }
    }
    

    image-20210805754416

    出现了两次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()+" 格式化失败 ");
    			}
    		}
        }
    }
    

    image-20210805936439

    由图可知,已经保证了线程安全,但这种方案不建议在高并发场景下使用,因为会创建大量的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()+" 格式化失败 ");
    			}
    		}
        }
    }
    

    image-2021080591591
    如图所示,线程是安全的。定义了全局变量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();
    			}
    		}
        }
    }
    

    image-20210805940496
    由结果可知,加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();
    			}
    		}
        }
    }
    

    image-202108059729

    使用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()+" 格式化失败 ");
    			}
    		}
    	}
    }
    

    image-2021080591443373
    使用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);    ...}
    

    image-20210728914309

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

    实践

    /**
     * 年月格式 {@link FastDateFormat}:yyyy-MM
     */
    public static final FastDateFormat NORM_MONTH_FORMAT = FastDateFormat.getInstance(NORM_MONTH_PATTERN);
    

    image-2021072895013629

    //FastDateFormatpublic static FastDateFormat getInstance(final String pattern) {   return CACHE.getInstance(pattern, null, null);}
    

    image-20210728205104833

    image-2021072895259113

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

    结论

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

    img

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

    2、加锁:synchronized锁和Lock锁

    3、使用ThreadLocal方式

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

    5、使用FastDateFormat 替换SimpleDateFormat(FastDateFormat 是线程安全的,Apache Commons Lang包支持,java8之前推荐此用法)

    推荐相关文章

    hutool日期时间系列文章

    1DateUtil(时间工具类)-当前时间和当前时间戳

    2DateUtil(时间工具类)-常用的时间类型Date,DateTime,Calendar和TemporalAccessor(LocalDateTime)转换

    3DateUtil(时间工具类)-获取日期的各种内容

    4DateUtil(时间工具类)-格式化时间

    5DateUtil(时间工具类)-解析被格式化的时间

    6DateUtil(时间工具类)-时间偏移量获取

    7DateUtil(时间工具类)-日期计算

    8ChineseDate(农历日期工具类)

    9LocalDateTimeUtil(JDK8+中的{@link LocalDateTime} 工具类封装)

    10TemporalAccessorUtil{@link TemporalAccessor} 工具类封装

    其他

    要探索JDK的核心底层源码,那必须掌握native用法

    万字博文教你搞懂java源码的日期和时间相关用法

    展开全文
  • 为什么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:44
    SimpleDateFormat为什么是线程不安全的? 其实这已经是个老生常谈的问题了,这篇博客旨在提醒自己,方便回忆。同时希望可以帮助到需要的伙伴。如果有哪些地方有错误或合适,欢迎大家指出。 我知道它是线程不安全的...
  • 图解HashMap为什么线程不安全

    万次阅读 多人点赞 2020-06-10 16:43:01
    HashMap的线程不安全主要体现在下面两个方面: 1.在JDK1.7中,当并发执行扩容操作时会造成环形链和数据丢失的情况。 2.在JDK1.8中,在并发执行put操作时会发生数据覆盖的情况。 JDK1.7 在JDK1.7中,扩容数据时要进行...
  • Java集合线程不安全问题总结

    千次阅读 2022-02-09 19:38:31
    Java集合线程不安全之并发修改异常 上代码 public class Demo { public static void main(String[] args) { List<String> myList = new ArrayList<>(); // 启动30个线程同时对list进行操作 for (int...
  • LinkedHashMap线程不安全解决

    千次阅读 2022-02-17 20:14:41
    LinkedHashMap线程不安全解决
  • 线程安全与线程安全

    千次阅读 2019-06-08 16:08:29
    1、是线程安全与线程安全 线程安全就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程能进行访问直到该线程读取完,其他线程才可使用。不会出现数据一致或者数据污染。 ...
  • 为什么StringBuilder是线程安全的 为什么StringBuffer是线程安全
  • ArrayList线程安全与Vector线程安全

    千次阅读 2018-08-30 10:46:22
    首先说一下什么是线程安全:线程安全就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程能进行访问直到该线程读取完,其他线程才可使用。不会出现数据一致或者数据污染。...
  • 经常会看到说HashMap是线程安全的,ConcurrentHashMap是线程安全的等等说法,不禁有个疑问,什么是线程安全?什么样的类是线程安全的? 1.什么是线程安全性(what) 线程安全定义,最核心是正确性, 正确性:多...
  • jdk1.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)
  • 容易忽略的ConcurrentHashMap 线程不安全行为

    万次阅读 多人点赞 2019-01-21 15:27:32
    线程安全的基础知识 首先解释什么是线程安全:在多线程中对一种数据类型的参数进行共享时,各个线程可以正确的执行,不会出现数据错误的情况就是线程安全。 接下来我们看一段常见的线程代码: public class ...
  • SimpleDateFormat为什么是线程不安全

    千次阅读 2020-09-03 19:10:22
    SimpleDateFormat是 Java 提供的个格式化和解析日期的工具类,但是你是否在夜深人静的时候想过,自己通过SimpleDateFormat格式化日期的时候会不会出现线程安全方面的问题呢? 李四就是这样,清晨,第二天顶着两个...
  • 线程不安全的原因及其解决

    千次阅读 2020-06-11 23:18:44
    一、什么是线程安全和线程安全 定义:线程安全:多线程并发执行某个代码时,产生了逻辑上的错误,结果和预期值相同 线程安全是指多线程执行时没有产生逻辑错误,结果和预期值相同 二、线程安全产生的原因 ...
  • 什么是线程安全和线程安全

    千次阅读 2019-12-02 16:57:42
    1、线程安全: 指多个线程在执行同一段代码的时候采用加锁机制,使每次的执行结果和单线程执行的结果都是一样的,存在执行程序时出现意外结果。 2、线程安全: 是指提供加锁机制保护,有可能出现多个线程...
  • SimpleDateFormat线程不安全及解决办法

    万次阅读 多人点赞 2017-06-10 15:15:23
    以前没有注意到SimpleDateFormat线程不安全的问题,写时间工具类,一般写成静态的成员变量,不知,此种写法的危险性!在此讨论一下SimpleDateFormat线程不安全问题,以及解决方法。 为什么SimpleDateFormat不安全? ...
  • 既然使用同一个方法,那为什么说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; } } 编写一个简单的...
  • 最新的详细测试 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是线程不安全的呢,之前面试的时候也遇到到这样的问题,但是当时只停留在***知道是***的层面上,并...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 1,009,994
精华内容 403,997
关键字:

线程不安全

友情链接: JSP购物商城项目.zip