【并发编程】线程安全性

线程安全性

原子性

定义

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

引入

多线程情况下,进行count++操作。为了保证线程安全性,通常对该操作进行加锁,保证在count++的时候同步操作。Java提供了很多封装好的原子操作类。如可以替代刚刚提到加锁方式的AtomicInteger

分类

Atomic
原子更新基本类型

Atomic包提供了以下3个类,下面三个类提供的方法几乎一模一样:

·AtomicBoolean:原子更新布尔类型。
·AtomicInteger:原子更新整型。
·AtomicLong:原子更新长整型。

常用方法(AtomicInteger)

·int addAndGet(int delta):以原子方式将输入的数值与实例中的值(AtomicInteger里的 value)相加,并返回结果。
·boolean compareAndSet(int expect,int update):如果输入的数值等于预期值,则以原子方式将该值设置为输入的值。
·int getAndIncrement():以原子方式将当前值加1,注意,这里返回的是自增前的值。
·void lazySet(int newValue):最终会设置成newValue,使用lazySet设置值后,可能导致其他线程在之后的一小段时间内还是可以读到旧的值。
·int getAndSet(int newValue):以原子方式设置为newValue的值,并返回旧值。

AtomicInteger.java实现原理

public final int getAndIncrement() {        
    for (;;) {            
        int current = get();            
        int next = current + 1;            
        if (compareAndSet(current, next))                
            return current;        
    } 
} 
public final boolean compareAndSet(int expect, int update) {        
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update); 
}

​ 源码中for循环体的第一步先取得AtomicInteger里存储的数值,第二步对AtomicInteger的当 前数值进行加1操作,关键的第三步调用compareAndSet方法来进行原子更新操作,该方法先检 查当前数值是否等于current,等于意味着AtomicInteger的值没有被其他线程修改过,则将 AtomicInteger的当前数值更新成next的值,如果不等compareAndSet方法会返回false,程序会进 入for循环重新进行compareAndSet操作。

​ Atomic包提供了3种基本类型的原子更新,但是Java的基本类型里还有char、float和double
等。那么问题来了,如何原子的更新其他的基本类型呢?Atomic包里的类基本都是使用Unsafe 实现的,看一下Unsafe的源码。

/** 
 * 如果当前数值是expected,则原子的将Java变量更新成x 
 * @return 如果更新成功则返回true 
 */ 
public final native boolean compareAndSwapObject(Object o,long offset,Object expected,Object x); public final native boolean compareAndSwapInt(Object o, long offset,int expected,int x); 
public final native boolean compareAndSwapLong(Object o, long offset,long expected,long x);

​ 我们发现Unsafe只提供了3种CAS方法:compareAndSwapObject、compareAndSwapInt和compareAndSwapLong,再看AtomicBoolean源码,发现它是先把Boolean转换成整 型,再使用compareAndSwapInt进行CAS,所以原子更新char、float和double变量也可以用类似
的思路来实现。

原子更新数组

通过原子的方式更新数组里的某个元素,Atomic包提供了以下4个类。

·AtomicIntegerArray:原子更新整型数组里的元素。
·AtomicLongArray:原子更新长整型数组里的元素。
·AtomicReferenceArray:原子更新引用类型数组里的元素。

AtomicIntegerArray类主要是提供原子的方式更新数组里的整型,其常用方法如下。

int addAndGet(int i,int delta):以原子方式将输入值与数组中索引i的元素相加。
boolean compareAndSet(int i,int expect,int update):如果当前值等于预期值,则以原子方式将数组位置i的元素设置成update值。

注意:

数组value通过构造方法传递进去,然后AtomicIntegerArray会将当前数组 复制一份,所以当AtomicIntegerArray对内部的数组元素进行修改时,不会影响传入的数组。

原子更新引用类型

原子更新基本类型的AtomicInteger,只能更新一个变量,如果要原子更新多个变量,就需要使用这个原子更新引用类型提供的类。Atomic包提供了以下3个类。

·AtomicReference:原子更新引用类型。
·AtomicReferenceFieldUpdater:原子更新引用类型里的字段。
·AtomicMarkableReference:原子更新带有标记位的引用类型。可以原子更新一个布尔类型的标记位和引用类型。构造方法是AtomicMarkableReference(V initialRef,boolean initialMark)。

原子更新字段类

如果需原子地更新某个类里的某个字段时,就需要使用原子更新字段类,Atomic包提供 了以下3个类进行原子字段更新。

·AtomicIntegerFieldUpdater:原子更新整型的字段的更新器。
·AtomicLongFieldUpdater:原子更新长整型字段的更新器。
·AtomicStampedReference:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于原子的更新数据和数据的版本号,可以解决使用CAS进行原子更新时可能出现的 ABA问题。

要想原子地更新字段类需要两步。

第一步,因为原子更新字段类都是抽象类,每次使用的 时候必须使用静态方法newUpdater()创建一个更新器,并且需要设置想要更新的类和属性。

第二步,更新类的字段(属性)必须使用public volatile修饰符。

AtomicLong和LongAdder区别

1、普通类型的Long和double变量,jvm允许将64位的读操作或写操作拆成2个32位的操作,LongAddr核心是将热点数据分离,可以将AtomicLong内部核心数据value,分离成为一个数组,每个线程访问时,通过hash等算法映射到其中一个其中一个数字进行计数,而最终的计算结果为这个数组求和累加.LongAdder将AtomicLong单点的更新压力分散到各个节点上,在低并发的时候通过对base的直接更新,可以很很好的保障和AtomicLong性能基本一致。而在高并发的时候提高分散提高性能。

2、LongAddr缺点:在统计的时候如果有并发更新,可能会导致统计的数据有误差,实际使用中有高并发计数的时候,我们可以优先使用LongAddr,而不是继续使用AtomicLong,当然在线程竞争很低的情况下进行计数,使用AtomicLong还是更简单,更直接一些,并且效率会稍高一点。

synchronized、lock和Atomic对比

1、synchronized:依赖JVM;lock:依赖特殊的CPU指令,代码实现,ReentrantLock

2、synchronized可以修饰:

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

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

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

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

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

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

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

可见性

定义

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

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

1、线程交叉执行

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

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

可见性保证:

可见性-synchronized(JMM关于synchronized的两条规定)

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

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

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

1、对volatile变量写操作时,会在写操作后加入一条store屏障指令,将本地中的共享变量值刷新到主内存
2、对volatile变量读操作时,会在读操作前加入一条load屏障指令,从主内存中读取共享变量
3、使用volatile必须具备两个条件

​ 对变量的写操作不依赖于当前值

​ 没有包含在具有其他变量的不必要的式子中

​ 适合做状态标识量

有序性

定义

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

happens-before原则

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

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

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

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

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

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

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

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

注意:

如果两个操作的执行顺序无法从happens-before原则中推测出来,那么就不能保证有序性,此时虚拟机可以随意的进行重排序

猜你喜欢

转载自blog.csdn.net/zlt995768025/article/details/81434239