• 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会对没有变量值依赖的操作进行重排序,这个规则只能保证单线程下执行的有序性,不能保证多线程下的有序性。

  • 相关阅读:
    MySQL5.6 单列、多列索引以及IN语句的优化(翻译)
    curl
    HTML meta
    access limit
    document、location、body 属性方法
    ASP对Excel的基本操作
    IE6/IE7/FF(火狐Firefox)及其他浏览器的兼容性通用解决方法
    CSS兼容IE与Firefox要点分析
    PHP函数
    vs2005 修改新增文件时的默认编码方式(转)
  • 原文地址:https://www.cnblogs.com/shamo89/p/10079711.html
Copyright © 2020-2023  润新知