Java并发编程(concurrent)

基础知识:

CPU多级缓存:CPU速度远大于内存速度。

CPU多级缓存的问题:
    缓存一致性(MESI协议):用于保证多核CPU cache缓存共享数据的一致性问题。

    乱序执行优化:处理器为提高运算速度而作出违背代码原有顺序的优化。(单核稳定,多核易错)

如何解决?=====》首先弄清部分概念:Java内存模型(Java Memory Model)

同步的八种操作和规则:

    1.锁定(lock):作用于主内存的变量,把一个变量标识为一条线程独占状态。

    2.解锁(unlock):作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。

    3.读取(read):作用于主内存的变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用。

    4.load(载入):作用于工作内存的变量,他把read操作从主内存中得到的变量值放入工作内存的变量副本中。

    5.使用(use):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎。

扫描二维码关注公众号,回复: 2322386 查看本文章

    6.赋值(assign):作用于工作内存的变量,把一个从执行引擎接收到的值赋值给工作内存的变量。

    7.存储(store):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。

    8.写入(write):作用于主内存的变量,把store操作从工作内存中一个变量的值传送到主内存的变量中。

规则:
    1.如果把一个变量从主内存中复制到工作内存中,就需要按顺序地执行read和load操作;如果把变量从工作内存中同步回主内存中,就要按顺序的执行store和write操作。但是Java内存模型只要求上述操作必须按照顺序执行,而没有保证必须是连续执行的。

    2.不允许read和load、store和write操作之一单独出现。

    3.不允许一个线程丢弃它最近assign操作,即变量在工作内存中改变了之后必须同步到主内存中。

    4.不允许一个线程无原因地(没有发生过任何assign操作)把数据从工作内存中同步回主内存中。

    5.一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量。即就是一个变量实施use和store操作之前,必循先执行assign和load操作。

    6.一个变量在同一时刻只允许一条线程对其进行lock操作,但是lock操作可以被同一条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操纵,变量才能被解锁。lock和unlock必须成对出现。

    7.如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行load或assign操作初始化变量的值。

    8.如果一个变量事先没有被lock操作锁定,则不允许对它进行unlock操作,也不允许unlock一个被其他线程锁定的变量。

    9.对一个变量执行unlock操作之前,必须先把此变量同步到主内存中(执行store和write操作)


并发的优势和风险:

    优势1:速度:同时处理多个请求,响应更快;复杂的操作可以分成多个进程同时进行。

    优势2:设计:程序设计在某些情况下可以有更多的选择。

    优势3:资源利用:CPU能够在等待IO的时候做一些其他的事情。

    风险1:安全性:多个线程共享数据可能会产生期望不相符的结果。

    风险2:活跃性:某个操作无法继续执行下去会产生活跃性问题,比如死锁、饥饿等问题。

    风险3:性能:线程过多使的CPU频繁切换,调度时间增多;同步机制;消耗过多内存


解决方案:

一、线程安全类:

    定义:当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些进程将如何交替执行,并且在主调代码中不需要额外的同步或者协同,这些类都能表现出正确行为,那么这个类是线程安全的。

    体现在三个方面:

    1.原子性:提供了互斥访问,同一时刻只能有一个线程来对它进行操作。

        原子性---Atomic包:

        (1)AtomicXXX:CAS/Unsafe.compareAndSwapInt

             public static AtomicInteger count=new AtomicInteger(0);

             count.incrementAndGet();

                该方法核心代码:
                public final int getAndAddInt(Object var1, long var2, int var4) {
                    int var5;
                    do {
                    var5 = this.getIntVolatile(var1, var2);
                    //工作内存中的var2值和主内存中var5值不断比较,相同执行var5 + var4操作
                } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

                return var5;
                   }

        (2)AutomicLong、LongAdder(区别)

            LongAdder的高明之处可能在于将之前单个节点的并发分散到各个节点的,这样从而提高在高并发时候的效率。

       (3)AtomicReference、AtomicReferenceFieldUpdater类====》更新数据

               private static AtomicReference<Integer> count=new AtomicReference<Integer>(0);

            //原子性更新类的某个实例指定的某个字段count,count需要特殊关键词volatile修饰
            private static AtomicIntegerFieldUpdater<AtomicExample5> updater= 
            AtomicIntegerFieldUpdater.newUpdater(AtomicExample5.class,"count");

        (4)AtmoicStampReference:CAS的ABA问题:变量值虽然相同,但是版本不同了。


        (5)AtomicBoolean
            //如何让某段代码只执行一次
            private static AtomicBoolean ishappend=new AtomicBoolean(false);


        原子性---锁:

            synchronized:依赖JVM---不可中断锁,适合竞争不激烈,可读性好。

                1.修饰代码块:大括号起来的代码,作用于调用的对象

                2.修饰方法:整个方法,作用于调用的对象

                -----子类无法继承父类的synchronized

                3.修饰静态方法:整个静态方法,作用于所有的对象

                4.修饰类:括号起来的部分,作用于所有对象


            Lock:依赖特殊的CPU指令,代码实现,ReentrantLock---可中断锁,多样化同步,竞争激烈时能维持常态


            Atomic:竞争激烈时能维持常态,比Lock性能好,只能同步一个值。

    2.可见性:一个线程对内存的修改可以及时的被其他线程观察到。

        导致共享变量在线程中不可见的原因:线程交叉执行;重排序结合线程交叉执行;共享变量更新后的值没有在工作内存与主存间及时更新。

        可见性:

        synchronized--JMM关于synchronized的两条规定:

            线程解锁前,必须把共享变量的最新值刷新到主内存中;

            线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值。

        volatile--通过加入内存屏障和禁止重排序优化来实现

            对volatile变量写操作时,会在写操作后加入一条store平渣指令,将本地内存中的共享变量值刷新到主内存中。

            对volatile变量读操作时,会在读操作前加入一条load屏障指令,从主内存中读取共享变量。

            ---CPU指令级实现,并不是线程安全的,不具有原子性。

        适合场景:状态标识量;doubleCheck(安全发布对象双重检测机制);

            例子: volatile boolean inited=false;

                  //线程1:
                  context=loadContext();
                  inited=true;

                  //线程2:
                  while(!inited){
                      sleep();
                  }
                  doSomethingWithConfig(context);


    3.有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序的存在,该观察结果一般杂乱无序。

        Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响单线程程序的执行,却会影响到多线程并发执行的正确性。

        Java先天有序性:happen...before原则

            1.程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作。

                -----虽然java虚拟机会重排序,但是只是对数据不依赖的指令重排序,最后执行的顺序看起来就是代码书写的顺序;无法保证多线程执行的正确性。

            2.锁定规则:一个unlock操作先行发生于后面对同一个锁的lock操作。

            3.volatile变量原则:对一个变量的写操作先行发生于后面对这个变量的读操作。

            4.传递原则:A在B前,B在C前,则A在C前。

            5-8:线程启动原则、线程中断原则、线程终结原则、对象终结原则。

        -----如果两个操作的执行次序无法从该八个原则中推导出来,就不能保证有序性,虚拟机可以对他们进行重排序。


二、安全发布对象:

    发布对象:使一个对象能够被当前范围之外的代码所使用。

    对象逸出:一个错误的发布。当一个对象还没有构造完成时,就使它被其他线程所见。


    安全发布对象的四种方法(对应视频5-2-5.3):
        
        1.在静态初始化函数中初始化一个对象引用。

        2.将对象的引用保存到volatile类型域或者AtomicReference对象中。

        3.将对象的引用保存到某个正确构造对象的final类型域中。

        4.将对象的引用保存到一个由锁保护的域中。


        example1: 懒汉模式,单例实例在第一次使用时创建===>线程不安全
        
        example2: 饿汉模式,单例实例在类装载创建=>线程安全;不足:构造方法中有过多处理会使得类加载慢;如果不调用会造成资源浪费;
        
        example3: synchronized使懒汉模式线程安全,但是不推荐    

        example4:懒汉模式+双重检测机制+同步锁===》线程不安全

        example5:懒汉模式+双重检测机制+同步锁+volatile===》线程安全

        example6: 饿汉模式,使用静态块

        example7:  枚举模式:最安全的===》对象的创建放在枚举内

                    public class SingletonExample7 {
                        //私有构造函数
                        private SingletonExample7(){
                        }
                        //静态的工厂方法
                        public static SingletonExample7 getInstance(){
                            return Singleton.INSTANCE.getInstance();
                        }
                        
                        private enum Singleton{
                            INSTANCE;
                            private SingletonExample7 singleton;
                            
                            //jvm保证这个方法只会被调用一次
                            Singleton(){
                                singleton=new SingletonExample7();
                            }
                            public SingletonExample7 getInstance(){
                                return singleton;
                            }
                        }

三、不可变对象:
    
    1.对象创建后其状态不能修改

    2.对象所有域都是final类型

    3.对象是正确创建的(创建期间,this引用没有逸出)


    创建不可变对象的方法:

     1.final(如果是引用型变量初始化以后不能指向另外一个对象,但是可以修改)

     2.Collections.unmodifiableXXX:Collection、List、Set、Map...(Collections.unmodifiableMap处理后不可修改,如果修改执行会抛出异常)

     3.Guava:ImmutableXXX:Collection、List、Set、Map...(生成对象初始化以后不可以修改,如果修改执行会抛出异常)


四、线程封闭:将对象封装到一个线程中来保证线程安全。

    1.堆栈封闭:局部变量不会被多个线程共享,无并发问题。

    2.ThreadLocal线程封闭:特别好的封闭方法;Map-->key对应线程,value对应对象。

        ThreadLocal用于保存某个线程共享变量:对于同一个static ThreadLocal,不同线程只能从中get,set,remove自己的变量,而不会影响其他线程的变量。

        1、ThreadLocal.get: 获取ThreadLocal中当前线程共享变量的值。

        2、ThreadLocal.set: 设置ThreadLocal中当前线程共享变量的值。

        3、ThreadLocal.remove: 移除ThreadLocal中当前线程共享变量的值。

        4、ThreadLocal.initialValue: ThreadLocal没有被当前线程赋值时或当前线程刚调用remove方法后调用get方法,返回此方法值。

        ThreadLocal用法详解和原理:https://www.cnblogs.com/coshaho/p/5127135.html


    总结和补充:

    常见线程不安全类与写法:

        1.StringBuilder---->StringBuffer(方法都使用了synchronized)

        2.SimpleDateFormat---->JodaTime

        3.ArrayList、HashSet、HashMap等Collections

        4.先检查后执行:if(condition(a)){handle(a)}


五、线程安全---同步容器类(并不一定线程安全)

    1.ArrayList--->Vector,Stack

    2.HashMap--->HashTable(key和value不能为null)

    3.Collections.synchronizedXXX(List、Set、Map)

    缺陷:多数容器类都是非线程安全的,即使部分容器是线程安全的,由于使用sychronized进行锁控制,导致读/写均需进行锁操作,性能很低。

    同步容器类详情:https://www.cnblogs.com/dolphin0520/p/3933404.html


六、线程安全---并发容器J.U.C
    
    ArrayList-->CopyOnwriteArrayList(适合读多写少)

    HashSet、TreeSet--->CopyOnWriteArraySet、ConcurrentSkipListSet

    HashMap、TreeMap--->CopyOnWriteArrayMap、ConcurrentSkipListMap(不允许null)

    并发容器J.U.C相对于同步器:https://www.cnblogs.com/daoqidelv/p/6753162.html

    总结和补充:

    安全共享对象策略:

    1.线程限制:一个被线程限制的对象,由线程独占,并且只能被占有它的线程修改。

    2.共享只读:一个共享只读的对象,在没有额外同步的情况下,可以被多个线程并发访问,但是任何线程都不能修改它。

    3.线程安全对象:一个线程安全的对象或者容器,在内部通过同步机制来保证线程安全,所以其他线程无需额外的同步就可以通过公共接口随意访问它。

    4.被守护对象:被守护对象只能通过获取特定的锁来访问。

J.U.C(java.util.concurrent)包:

    核心:AQS(AbstractQueuedSynchronized)

    AQS设计思想:

        1.使用Node实现FIFO队列,可以用于构建或者其他同步装置的基础框架。

        2.利用一个int类型表示状态。

        3.使用方法是继承,子类通过继承并通过实现它的方法管理其状态,(acquire和release)的方法操纵状态。

        4.可以同时实现排他锁和共享锁模式(独占和共享)。

    AQS同步组件:CountDownLatch、Semaphore、CyclicBarrier、ReentrantLock、Condition、FutureTask

    CountDownLatch:

        CountDownLatch是一个同步工具,它主要用线程执行之间的协作。CountDownLatch 的作用和 Thread.join()方法类似,让一些线程阻塞直到另一些线程完成一系列操作后才被唤醒。在直接创建线程的年代(Java 5.0 之前),我们可以使用 Thread.join()。在线程池出现后,因为线程池中的线程不能直接被引用,所以就必须使用 CountDownLatch了。


        实现原理:计数器的值由构造函数传入,并用它初始化AQS的state值。当线程调用await方法时会检查state的值是否为0,如果是就直接返回(即不会阻塞);如果不是,将表示该节点的线程入列,然后将自身阻塞。当其它线程调用countDown方法会将计数器减1,然后判断计数器的值是否为0,当它为0时,会唤醒队列中的第一个节点,由于CountDownLatch使用了AQS的共享模式,所以第一个节点被唤醒后又会唤醒第二个节点,以此类推,使得所有因await方法阻塞的线程都能被唤醒而继续执行。


    Semaphore:

        Semaphore是一个计数信号量,它的本质是一个”共享锁”。

        信号量维护了一个信号量许可集。线程可以通过调用acquire(),来获取信号量的许可;当信号量中有可用的许可时,线程能获取该许可;否则线程必须等待,直到有可用的许可为止。线程可以通过release()来释放它所持有的信号量许可。

        适用场景:例如数据库连接的数量有限,远低于并发访个数。


    CyclicBarrier:

        CyclicBarrier字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。CyclicBarrier默认的构造方法是CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用await方法告诉CyclicBarrier我已经到达了屏障,然后当前线程被阻塞。

    
        CountDownLatch和CyclicBarrier的区别:

        CountDownLatch: 一个线程(或者多个), 等待另外N个线程完成某个事情之后才能执行。 

        CyclicBarrier: N个线程相互等待,任何一个线程完成之前,所有的线程都必须等待。

        这样应该就清楚一点了,对于CountDownLatch来说,重点是那个“一个线程”, 是它在等待,而另外那N的线程在把“某个事情”做完之后可以继续等待,可以终止。

        而对于CyclicBarrier来说,重点是那N个线程,他们之间任何一个没有完成,所有的线程都必须等待。

        CountDownLatch 是计数器, 线程完成一个就记一个, 就像 报数一样, 只不过是递减的.

        而CyclicBarrier更像一个水闸, 线程执行就想水流, 在水闸处都会堵住, 等到水满(线程到齐)了, 才开始泄流.


    ReentrantLock:    

        synchronized是jvm层面的锁,是不会引发死锁的。ReentrantLock需要加锁和解锁,有死锁可能。

        在JDK5.0版本之前,重入锁的性能远远好于synchronized关键字,JDK6.0版本之后synchronized 得到了大量的优化,二者性能也不分伯仲,但是重入锁是可以完全替代synchronized关键字的。除此之外,重入锁又自带一系列高逼格:可中断响应、锁申请等待限时、公平锁。另外可以结合Condition来使用。


    适用场景:

        1.只有少量线程的时候适合Synchronized

        2.线程不少,但是增长可以预估的适合ReentrantLock

    
    Condition:

        Condition的作用是对锁进行更精确的控制。Condition中的await()方法相当于Object的wait()方法,Condition中的signal()方法相当于Object的notify()方法,Condition中的signalAll()相当于Object的notifyAll()方法。不同的是Object中的wait(),notify(),notifyAll()方法是和"同步锁" (synchronized关键字)捆绑使用的;而Condition是需要与"互斥锁"/"共享锁"捆绑使用的。


    FutureTask: 

        在Java中一般通过继承Thread类或者实现Runnable接口这两种方式来创建多线程,但是这两种方式都有个缺陷,就是不能在执行完成后获取执行的结果,因此Java1.5之后提供了Callable和Future接口,通过它们就可以在任务执行完毕之后得到任务的执行结果。

       详见: http://www.importnew.com/25286.html


    Fork/Join框架详解:

        Fork/Join框架是Java 7提供的一个用于并行执行任务的框架,是一个把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果的框架。

        Fork/Join框架要完成两件事情:

       1.任务分割:首先Fork/Join框架需要把大的任务分割成足够小的子任务,如果子任务比较大的话还要对子任务进行继续分割。

       2.执行任务并合并结果:分割的子任务分别放到双端队列里,然后几个启动线程分别从双端队列里获取任务执行。子任务执行完的结果都放在另外一个队列里,启动一个线程从队列里取数据,然后合并这些数据。

       详见: https://www.cnblogs.com/senlinyang/p/7885964.html


    BlockingQueue:

        在新增的Concurrent包中,BlockingQueue很好的解决了多线程中,如何高效安全“传输”数据的问题。通过这些高效并且线程安全的队列类,为我们快速搭建高质量的 多线程程序带来极大的便利。多线程环境中,通过队列可以很容易实现数据共享,比如经典的“生产者”和“消费者”模型中,通过队列可以很便利地实现两者之间的数据共享。假设我们有若干生产者线程,另外又有若干个消费者线程。如果生产者线程需要把准备好的数据共享给消费者线程,利用队列的方式来传递数据,就可以很方便地解决他们之间的数据共享问题。但如果生产者和消费者在某个时间段内,万一发生数据处理速度不匹配的情况呢?理想情况下,如果生产者产出数据的速度大于消费者消费的速度,并且当生产出来的数据累积到一定程度的时候,那么生产者必须暂停等待一下(阻塞生产者线程),以便等待消费者线程把累积的数据处理完毕,反之亦然。然而,在concurrent包发布以前,在多线程环境下,我们每个程序员都必须去自己控制这些细节,尤其还要兼顾效率和线程安全,而这会给我们的程序带来不小的复杂度。好在此时,强大的concurrent包横空出世了,而他也给我们带来了强大的BlockingQueue。

        详见:https://www.cnblogs.com/KingIceMou/p/8075343.html

线程池:

        new Thread()弊端:

            1.每次new Thread 新建对象,性能差。

            2.线程缺乏统一管理,可能无限制的新建线程,相互竞争,有可能占用过多系统资源导致死机或OOM(Out Of Memory)

            3.缺少更多功能,如更多执行、定期执行、线程中断。

        线程池的好处:

            1.重用存在的线程,减少对象创建、消亡的开销,性能佳。

            2.可有效控制最大并发线程数,提高系统资源利用率,同时可以避免过多资源竞争,避免阻塞。

            3.提供定时执行、定期执行、单线程、并发数控制等功能。


    线程池类:ThreadPoolExecutor

            1.corePoolSize:核心线程数量

            2.maxmumPoolSize:线程最大线程数

            3.workQueue:阻塞队列,存储等待执行的任务,很重要,会对线程池运行过程产生重大影响。

            4.keepAliveTime:线程没有任务执行时最多保持多久时间终止

            5.unit:keepAliveTime时间单位

            6.threadFactory:线程工厂,用来创建线程

            7.rejectHandler:当拒绝处理任务时的策略


    ThreadPoolExecutor提供的方法:

        基础方法:

            1.execute():提交任务,交给线程池执行

            2.submit():提交任务,能够返回执行结果=execute+Future

            3.shutdown():关闭线程池,等待任务都完成

            4.shutdownNow():关闭线程池,不等待任务执行完

        监控方法:

            1.getTaskCount():线程池已执行和未执行的任务总数。

            2.getCompletedTaskCount():已完成的任务数量

            3.getPoolSize():线程池当前的线程数量。

            4.getPoolSize():线程池当前的线程数量

            5.getActiveCount():当前线程池中正在执行任务的线程数量。

    线程池---Executor框架接口:

        1.Executors.newCachedThreadPool

        2.Executors.newFixedThreadPool

        3.Executrors.newSingleThreadPool

        4.Executors.newScheduledTreadPool


多线程扩展知识:

    多线程产生死锁的条件:互斥条件、请求和保持条件、不剥夺条件、环路等待条件

多线程并发最佳实践:

            1.使用本地变量

            2.使用不可变类

            3.最小化锁的作用于范围:s=1/(1-a+a/n)---->阿姆达尔公式

                阿姆达尔曾致力于并行处理系统的研究。对于固定负载情况下描述并行处理效果的加速比s,阿姆达尔经过深入研究给 出了S=1/(1-a+a/n)其中,a为并行计算部分所占比例,n为并行处理结点个数。这样,当1-a=0时,(即没有串行,只有并行)最大加速比s=n;当a=0时(即只有串行,没有并行),最小加速比s=1;当n→∞时,极限加速比s→ 1/(1-a),这也就是加速比的上限例如,若串行代码占整个代码的25%,则并行处理的总体性能不可能超过4。

            4.使用线程池的Executor,而不是直接new Thread执行

            5.宁可使用同步也不要使用线程的wait和notify

            6.使用BlockingQueue实现生产-消费模式

            7.使用并发集合而不是加了锁的同步集合

            8.使用Semaphore创建有界的访问

            9.宁可使用同步代码块也不要使用同步方法

            10.避免使用静态变量

Spring与线程安全:https://blog.csdn.net/qq_38306026/article/details/79220911

HashMap和ConcurrentHashMap的原理、区别

猜你喜欢

转载自blog.csdn.net/Colin_Qichao/article/details/81143801
今日推荐