多线程-- 二.线程安全性

线程安全/不安全

线程安全性:

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

    线程安全性体现在三个方面:

    1.原子性:互斥访问,同一个时刻只能有一个线程来对它进行操作,  如Atomic包,锁

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

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

一.原子性:

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

    方法1:atomic包

    JDK里面提供了一个包,叫atomic包,这个包里面提供了很多AtomicXXX类,他们都是通过CAS来完成原子性的.如下图,获取结果后是线程安全的.

①.AtomicXXX

源码剖析:

    这个方法里面用了一个unsafe的类.

    点进去

    注意上图的方法中的参数.

        var 1是当前对象,也就是count对象.

        var 2是当前值,var 4是增加量.(假如我们要做2+1的操作,那么第二个参数var2就是2,第三个参数var4就是1.)

        var 5是我们通过调用底层方法getIntVolatile,得到的底层当前的值.也就是说,假如没有线程来处理我们count的时候,那么var 5 应该就是2.

    这里面有一个compareAndSwapInt的方法,这个方法是java底层的方法,它要达到的效果就是:

    对于count这个对象(var1),如果当前的值2(var2),和底层的值2(var5)相同的话,就把它更新为后面的值2+1=3(var5+var4);

    否则的话,重新取出var5,var2相当于是从var1中取一次,也会变成3,继续判断,如果跟底层值相同的话,编程3+1=4;

    通过do while语句不断循环,直到当前值和底层值完全相同的话,才更新为后面的值.

    这个方法的核心,就是CAS的核心.

    CAS的ABA问题,意思是变量从A变为B,又变为了A.解决办法就是让当前版本号+1.

②.AtomicLong和LongAdder

单独说一下AtomicLong这个类,在JDK8中,有一个跟它很像的类,叫LongAdder.

这两个类比较:

    之前我们AtomicXX类,底层CAS是一个do while死循环,不断的尝试,直到修改成功.如果竞争不激烈的时候成功率很高,否则失败概率会提高.如果大量修改失败的话,会多次尝试,性能受到影响

    对于普通类型的Long和Double变量,JVM会将64位的读写操作拆成2个32位的操作.这个LongAdder类的思想,是热点数据分离.LongAdder相当于在AtomicLong的基础上,将单点的更新压力分散到各个节点上,在低并发情况下通过对base的直接更新,可以很好的保证和Atomic的性能基本一致;而在高并发时候通过分散提高性能.

    实际使用中,如果处理高并发计数,优先使用LongAdder;如果线程竞争很低的话,还是用AtomicLong就可以

③.AtomicReferemce类和AtomicReferemceFieldUpdater类

AtomicReferemce类:

AtomicReferemceFieldUpdater类:

方法2:锁

    ①.Synchronized 

    一个JAVA的关键字,依赖JVM实现锁.因此在这个关键字作用对象的作用范围内,都是同一时刻只能有一个线程可以进行操作的.

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

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

    修饰静态方法:修饰范围是整个静态方法,作用于所有对象.

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

    ②.Lock

    JDK提供的一个代码层面的锁,依赖特殊的CPU指令,实现类里面,有代表性的类:Reentrantlock

简单对比:

synchronized:不可中断锁,适合竞争不激烈,可读性好.

Lock:可中断锁(调用Unlock方法就可以),多样化同步,竞争激烈时能维持常态.

Atomic:竞争激烈时也能维持常态,比Lock性能好,缺点是每次只能同步一个值.

   

二.可见性:

    一个线程对主内存的修改可以及时的被其它线程观察到.

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

    1.线程交叉执行

    2.重排序结合线程交叉执行

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

对于可见性,JVM提供了synchronized和volatile

①.synchronized:

JMM关于synchronized的两条规定:

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

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

②.volatile:

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

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

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

但是要注意,直接使用volatile进行+操作,是线程不安全的.

volatile不是原子性.不适合计数场景.通常来说,使用volatile特别适合作为状态标识量.标记啥的.如下图

还有一种场景叫 double check.

三.有序性:

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

有序性:heppens-before原则

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

2.锁定规则:    一个unLock操作先行发生与后面对同一个锁的Lock操作.

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

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

5.线程启动规则

6.线程中断规则

7.线程终结规则

8.对象终结规则

(前4个比较重要)


    

猜你喜欢

转载自blog.csdn.net/zhmystic/article/details/82117486