美文网首页多线程并发编程
《java并发编程实战》-(2)-线程安全性

《java并发编程实战》-(2)-线程安全性

作者: 小超人爱小土豆 | 来源:发表于2018-08-23 23:10 被阅读134次

        这里是最全的线程安全性总结,如果觉得总结的还不错的话,不要吝啬你的小心心啊,哈哈。

    一、安全性定义:

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

    二、主要体现在三个方面:

        1、原子性;2、可见性;3、 有序性;

        2.1原子性:

        原子性举个栗子,我们在做普通int变量++的操作的时候,会出现跟预期不同的结果。主要的原因是,看上去++是一个独立的操作,但是这个操作却不是原子性的,它实际的操作序列是“读取-修改-写入”。这个时候会出现两个线程获取到同样值,同时做++,后写入。这样就会导致有一次执行没有生效。

        解决办法嘛,有两种,一种是加锁,这个很好理解,是显示的将一个++操作变成一个原子性操作。加了锁,不让别的线程(未获取到锁的线程)影响到目前正在执行的线程操作。保证了安全性的同时带来性能方面的问题。

        还有一种是使用原子类 atomic定义的变量,来做计数操作。atomic包中的类都是用过CAS方式来保证原子性的。

        2.1.1CAS:

        atomic中使用了一个叫unsafe的类。类中有一个getAndAddInt的方法调用了一个叫 compareAndSwapInt的方法。而这个方法也是一个native的方法。主要有四个参数

    CompareAndSwap

        var1:需要改变的对象。var2:内存中的value。var5:期望的值。var5+var4更新后的值。var4:步长(++的话var4就是1)。

        CAS没有通过锁机制(阻塞的方式)来保证原子性,是通过操作系统级别的指令来控制(对内存中的共享数据进行操作的一种特殊指令。这个指令会对内存中的共享数据做原子的读写操作。简单介绍一下这个指令的操作过程:首先,CPU 会将内存中将要被更改的数据与期望的值做比较。然后,当这两个值相等时,CPU 才会将内存中的数值替换为新的值。否则便不做操作。最后,CPU 会将旧的数值返回。)

        CAS这是一种乐观锁的思路,它相信在它修改之前,没有其它线程去修改它;而Synchronized是一种悲观锁,它认为在它修改之前,一定会有其它线程去修改它,悲观锁效率很低。详细的CAS问题也可以看这篇文章 《java并发编程实战》-(2)-线程安全性-(CAS)

        2.1.1-1Atomiclong与Longadder的区别

        我们刚才通过分析得出atomic类是通过cas不断循环来保证原子性,这样就会存在一个问题,是高并发的时候,可能要尝试(循环)很多次才能成功。

        LongAdder在AtomicLong的基础上将单点的更新压力分散到各个节点,在低并发的时候通过对base的直接更新可以很好的保障和AtomicLong的性能基本保持一致,而在高并发的时候通过分散提高了性能。 

      缺点是LongAdder在统计的时候如果有并发更新,可能导致统计的数据有误差。

        2.1.1-2 AtomicRefernce与AtomicIntegerFieldUpdater

        AtomicIntegerFieldUpdater这个是为了解决对象的属性,而AtomicInteger只能解决基本类型

        2.1.2锁:

        synchronized:依赖JVM

        Lock:依赖特殊的CPU指令,代码实现,ReentrantLock、

        可以修饰代码块、修饰方法  (都是作用于调用的对象)、   修饰静态方法、修饰类(作用于所有对象)

        要注意一个问题,父类的方法中加了synchronized修饰,子类不会继承这个修饰,要实现这个效果只能显示的定义一下。

        总结:

        synchronized:不可中断的同步锁,适合竞争不激烈,可读性好。

        lock:可中断锁,多样化同步,竞争激烈时能维持常态

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

    2.2可见性

        首先要清楚导致共享变量在线程之间不可见的原因:

        (1)线程交叉执行;

        (2)重排序结合线程交叉执行;

        (3)共享变量更新后的值没有在工作内存与主存之间及时更新;

        2.2.1可见性-synchronized 

        JMM关于synchronized的两条规定:

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

        线程加锁时,将清空工作内存中共享变量的值,从而使共享变量时需要从主内存中重新读取最新的值(需要注意的一点是,加锁与解锁是同一把锁

        2.2.2可见性-volatile

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

        内存屏障:

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

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

    volatile写 volatile读

        volatile保证了线程之间的可见性,但是不能保证线程之间的正确性(不具备原子性)

        主要原因是在用volatile关键字修饰的变量上,做++操作的时候,其实是执行了三部,1获取主内存中的Count,2是+1 3是返回给主内存中,但是由于会出现,两个线程同时读到了主内存的值都是一样的,加一之后的值也是一样的,这样就是少加了一次,导致跟预期的值不同。

        主要适用于  不同线程之间状态标志的一个变量

    volatile boolean inited = false;

    //线程1

    context = loadContext();

    inited = true;

    //线程2

    while(!inited){

        sleep();

    }

    doSomethingWithConfig(context);

    2.3有序性

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

        volatile synchronized lock  都可以保证线程的有序性。

        2.3.1happens-before原则

        如果两个操作的秩序无法从happens-before原则推倒出来,jvm就可以随意的指令重排。

    happens-before-1 happens-before-2 happens-before-3 happens-before-4

    相关文章

      网友评论

        本文标题:《java并发编程实战》-(2)-线程安全性

        本文链接:https://www.haomeiwen.com/subject/goduiftx.html