zoukankan      html  css  js  c++  java
  • Java并发编程(四):线程安全性

     

    一、定义

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

    二、线程安全性

    1)线程安全性的三个方面

    1. 原子性:提供了互斥访问,同一时刻只能有一个线程来对它进行操作。
    2. 可见性:一个线程对主内存的修改可以及时的被其它线程观察到。
    3. 有序性:一个线程观察其它线程中的指令执行顺序,由于指令重排序的存在,该观察结果一般杂乱无序。

    更多理解:并发编程之原子性、可见性、有序性的简单理解

    2)原子性-Atomic包

    Java并发编程之CAS

    1.AtomicXXX:CAS 、Unsafe.compareAndSwapInt

    代码演示

    import lombok.extern.slf4j.Slf4j;
    
    import java.util.concurrent.CountDownLatch;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.concurrent.Semaphore;
    import java.util.concurrent.atomic.AtomicInteger;
    
    /**
     * 计数到 5000  , 代码运行结果 为 5000
     */
    @Slf4j
    public class CountExample2 {
    
    
        // 请求总数
        public static int clientTotal = 5000;
    
        // 同时并发执行的线程数
        public static int threadTotal = 200;
    
        public static AtomicInteger count = new AtomicInteger(0);
    
        public static void main(String[] args) throws InterruptedException {
    
            ExecutorService executorService = Executors.newCachedThreadPool(); // 线程池
            final Semaphore semaphore = new Semaphore(threadTotal); //信号量
            final CountDownLatch countDownLatch = new CountDownLatch(clientTotal); // 计数器闭锁
            for (int i = 0;i<clientTotal;i++) {
                executorService.execute(() ->{
                    try {
                        semaphore.acquire();
                        add();
                        semaphore.release();
                    } catch (Exception e) {
                        log.error("exception ", e);
                    }
                    countDownLatch.countDown();
                });
            }
            countDownLatch.await();
            executorService.shutdown();
            log.info("count:{}", count.get());
        }
    
        private static void add(){
        // 先做增加操作, 再获取当前的值
            count.incrementAndGet();
            // 先获取当前的值, 在做增加操作
            // count.getAndIncrement();
        }
    }

    来看一下它的实现源码 , Atomic 在实现的时候, 使用了一个 unsafe 的类, unsafe 提供了一个 getAddAddInt的方法 , 来看一下 这个方法的实现

    incrementAndGet 方法实现

    /**
         * Atomically increments by one the current value.
         *
         * @return the updated value
         */
        public final int incrementAndGet() {
            return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
        }

    getAndAddInt 方法实现

    public final int getAndAddInt(Object var1, long var2, int var4) {
            int var5;
            do {
                var5 = this.getIntVolatile(var1, var2);
            } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
    
            return var5;
        }

    源码说明, 在这个源码实现里 , 用`do while 作为主体实现的 , 在 while 条件里 , 调用了一个核心的方法 compareAndSwapInt
    在这个 getAndAddInt 方法里 , 传来的第一个参数是 请求的对象 , 就是上面示例代码里的 count ,第二个值是当前的值 ,比如当前执行的是 2 +1 这个操作, var2 就是 2 ,第三个参数 var4 的值是 1 , 而 var5 是底层当前的值. 如果没有其他线程对对象 count 进行操作, 其返回的底层的值应该是 2 , 此时, 当前值 2 和底层获取到的值 2 是相等的, compareAndSwapInt判断当前参数 var2 和底层var5的值相等, 则执行相加操作 将 底层获取的值 var5 加上 被加数var4 ,这个方法的最终目的就是, 对于这个传过来的 对象 count 如果底层的值和当前的值时相等的, 就将其更新为目标值 .
    上面的代码的增加操作中, 在进行 2 + 1 操作的是时候, 对象 count 可能被其他线程更新, 当前值var2 就和 var5 不相等了, 所有就不能更新目标值 ,那么再次取出 底层的值 var5 , var2这个值再重新从当前对象 count 取一次, 再次判断是否符合更新要求 . 就是通过这样不停的循环, 当 var2 与 var5 完全相同的时候, 才进行更新值 . 这个 compareAndSwapInt 的核心 就是所谓的 CAS 的核心.

    2.AtomicLong、LongAdder

    Atomic 还有提供了 一个 类 AtomicLong , 其实现和 AtomicInteger 一样.

    在Java 8 里 , Atomic 提供了 一个 LongAdder 类 , 上面通过看 CAS 底层实现的时候知道了, 它是通过一个死循环,不断的长沙市修改目标值 , 直到修改成功 , 如果并发不是很好的情况下, 修改成功的几率很高 , 如果大量修改失败, 这些原子操作就会进行多次的循环尝试, 因此性能会受到一定的影响. 这里有一个额外的知识点, 对于 long double 类型的变量, jvm 允许将 64 位的读操作者 或者写操作 拆分成两个 32 位的操作 。

    LongAdder 这个类的设计 , 其核心是将热点数据分离, 比如它将 AtomicLong 内部的核心数据 value ,分离成一个数组 , 每个线程访问时候, 通过hash 等算法 ,将其映射到其中一个数字进行计数, 而最终的结果呢, 是这个数组的求和累加 . 热点数据value会被分离成多个的cell ,每个cell 独自维护内部的值, 当前对象的值由所有的cell 累计合成. 这样热点数据就进行了有效的分离 , 并提高了并行度 . 这样LongAdder 就相当于在 AtomicLong 基础上, 把单点的更新压力, 分散到各个节点上 。

    LongAdder 在低并发的时候通过直接操作base,可以很好的保证和Atomic的性能基本一致,在高并发的场景,通过热点分区来提高并行度。

    缺点:在统计的时候如果有并发更新,可能会导致结果有些误差

    实际运用中:优先使用LongAdder ,在线程竞争很低的情况下使用AtomicLong效率更高

    全局序列号使用AtomicLong。

    3.AtomicReference、AtomicReferenceFieldUpdater

    AtomicReference: 用法同AtomicInteger一样,但是可以放各种对象

    @Slf4j@ThreadSafepublicclassAtomicExample4{publicstaticAtomicReference count =newAtomicReference<>(0);publicstaticvoidmain(String[] args)throwsInterruptedException{// 2count.compareAndSet(0,2);// nocount.compareAndSet(0,1);// nocount.compareAndSet(1,3);// 4count.compareAndSet(2,4);// nocount.compareAndSet(3,5);        log.info("count:{}",count.get());    }}

    AtomicReferenceFieldUpdater

    @Slf4j@ThreadSafepublicclassAtomicExample5{@Getterprivatevolatileintcount =100;/**

        * AtomicIntegerFieldUpdater 核心是原子性的去更新某一个类的实例的指定的某一个字段

        * 构造函数第一个参数为类定义,第二个参数为指定字段的属性名,必须是volatile修饰并且非static的字段

        */privatestaticAtomicIntegerFieldUpdater updater = AtomicIntegerFieldUpdater.newUpdater(AtomicExample5.class,"count");publicstaticvoidmain(String[] args)throws InterruptedException{        AtomicExample5 example5 =newAtomicExample5();// 第一次 count=100 -> count->120 返回Trueif(updater.compareAndSet(example5,100,120)){log.info("update success 1:{}",example5.getCount());        }// count=120 -> 返回Falseif(updater.compareAndSet(example5,100,120)){log.info("update success 2:{}",example5.getCount());        }else{log.info("update field:{}",example5.getCount());        }    }}

    5.AtomicStampReference:CAS的ABA问题

    ABA问题:在CAS操作的时候,其他线程将变量的值A改成了B由改成了A,本线程使用期望值A与当前变量进行比较的时候,发现A变量没有变,于是CAS就将A值进行了交换操作,这个时候实际上A值已经被其他线程改变过,这与设计思想是不符合的

    解决思路:每次变量更新的时候,把变量的版本号加一,这样只要变量被某一个线程修改过,该变量版本号就会发生递增操作,从而解决了ABA变化

    /**    * Atomically sets the value of both the reference and stamp    * to the given update values if the    * current reference is {@code==} to the expected reference    * and the current stamp is equal to the expected stamp.    *    *@paramexpectedReference the expected value of the reference    *@paramnewReference the new value for the reference    *@paramexpectedStamp the expected value of the stamp(上面提到的版本号)    *@paramnewStamp the new value for the stamp    *@return{@codetrue} if successful    */publicbooleancompareAndSet(V  expectedReference,                                V  newReference,intexpectedStamp,intnewStamp){        Pair current = pair;returnexpectedReference == current.reference &&            expectedStamp == current.stamp &&            ((newReference == current.reference &&              newStamp == current.stamp) ||            casPair(current, Pair.of(newReference, newStamp)));    }

    6.AtomicLongArray

    可以指定更新一个数组指定索引位置的值

    /**    * Atomically sets the element at position {@codei} to the given value    * and returns the old value.    *    *@parami the index    *@paramnewValue the new value    *@returnthe previous value    */publicfinallonggetAndSet(inti,longnewValue){returnunsafe.getAndSetLong(array, checkedByteOffset(i), newValue);    }....../**    * Atomically sets the element at position {@codei} to the given    * updated value if the current value {@code==} the expected value.    *    *@parami the index    *@paramexpect the expected value    *@paramupdate the new value    *@return{@codetrue} if successful. False return indicates that    * the actual value was not equal to the expected value.    */publicfinalbooleancompareAndSet(inti,longexpect,longupdate){returncompareAndSetRaw(checkedByteOffset(i), expect, update);    }

    7.AtomicBoolean(平时用的比较多)

    compareAndSet方法也值得注意,可以达到同一时间只有一个线程执行这段代码

    /**    * Atomically sets the value to the given updated value    * if the current value {@code==} the expected value.    *    *@paramexpect the expected value    *@paramupdate the new value    *@return{@codetrue} if successful. False return indicates that    * the actual value was not equal to the expected value.    */publicfinalbooleancompareAndSet(booleanexpect,booleanupdate){inte = expect ?1:0;intu = update ?1:0;returnunsafe.compareAndSwapInt(this, valueOffset, e, u);    }

    AtomicBoolean介绍与使用

    8.原子性-锁

    1. synchronized:依赖JVM (主要依赖JVM实现锁,因此在这个关键字作用对象的作用范围内,都是同一时刻只能有一个线程进行操作的)
    2. Lock:依赖特殊的CPU指令,代码实现,ReentrantLock

    修饰的内容分类

    1. 修饰代码块:大括号括起来的代码,作用于调用的对象
    2. 修饰方法:整个方法,作用于调用的对象
    3. 修饰静态方法:整个静态方法,作用于所有对象
    4. 修饰类:括号括起来的部分,作用于所有对象

    3)可见性

    导致共享变量在线程间不可见的原因

    1、线程的交叉执行
    2、重排序结合线程交叉执行
    3、共享变量更新后的值没有在工作内存与主内存间及时更新

    可见性-synchronized

    JMM关于synchronized的两条规定:
    1、线程解锁前,必须把共享变量的最新值刷新到主内存
    2、线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从
    主内存中重新读取最新的值(注意:加锁与解锁是同一把锁)

    更多理解:关于java多线程中synchronized关键字的理解

    synchronized修饰方法和代码块的区别

    synchronized与Lock的区别

    可见性-volatile

    通过加入内存屏障和禁止重排序优化来实现
    1、对volatile变量写操作时,会在写操作后加入一条store屏障指令,将本地内存中的
    共享变量值刷新到主内存
    2、对volatile变量读操作时,会在读操作前加入一条load屏障指令,从主内存读取

    共享变量
    示意图:



     

    更多volatile知识:关于java多线程关键字volatile的理解

    4)有序性

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

    JMM允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
    可以通过volatile、synchronized、lock保证有序性。

    Happens-before原则

    先天有序性,即不需要任何额外的代码控制即可保证有序性,java内存模型一个列出了八种Happens-before规则,如果两个操作的次序不能从这八种规则中推倒出来,则不能保证有序性。

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

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

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

    传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C

    线程启动原则:Thread对象的start()方法先行发生于此线程的每一个动作

    线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生

    线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()方法返回值手段检测到线程已经终止执行

    对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始

    第一条规则要注意理解,这里只是程序的运行结果看起来像是顺序执行,虽然结果是一样的,jvm会对没有变量值依赖的操作进行重排序,这个规则只能保证单线程下执行的有序性,不能保证多线程下的有序性。

  • 相关阅读:
    线性代数思维导图——3.向量
    微分中值定理的基础题型总结
    构造函数
    Python课程笔记(七)
    0241. Different Ways to Add Parentheses (M)
    0014. Longest Common Prefix (E)
    0013. Roman to Integer (E)
    0011. Container With Most Water (M)
    0010. Regular Expression Matching (H)
    0012. Integer to Roman (M)
  • 原文地址:https://www.cnblogs.com/shamo89/p/10079711.html
Copyright © 2011-2022 走看看