多线程(四)volatile关键字

volatile关键字保证线程的原子性。在java.util.concurrent包里包含的主要就是一些与并发实现相关的类,首先来看一下最为基础的原子类(java.util.concurrent.atomic)

根据修改的数据类型,可以将java.util.concurrent.atomic包中的原子操作类可以分为4类。

1. 基本类型: AtomicInteger, AtomicLong, AtomicBoolean ;
2. 数组类型: AtomicIntegerArray, AtomicLongArray, AtomicReferenceArray ;
3. 引用类型: AtomicReference, AtomicStampedRerence, AtomicMarkableReference ;
4. 对象的属性修改类型: AtomicIntegerFieldUpdater, AtomicLongFieldUpdater, AtomicReferenceFieldUpdater 。

这些类存在的目的是对相应的数据进行原子操作。所谓原子操作,是指操作过程(在CPU运行期间)不会被中断,保证数据操作是以原子方式进行的。下面对AtomicInteger源码进行分析

public class AtomicInteger extends Number implements java.io.Serializable {
    private static final long serialVersionUID = 6214790243416807050L;

    // 用 Unsafe.compareAndSwapInt 进行更新
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;//字段value在类中的偏移量
    /*静态初始化valueOffset值,通过class.getDeclaredField("value")通过反射获取对象的字段
value,再通过unsafe.objectFieldOffset(Field)获取该Field在对象中的偏移量*/
    static {
      try {
        valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));
      } catch (Exception ex) { throw new Error(ex); }
    }
    /*被volatile修饰的value,volatile关键字保证了在多线程中value的值是可见的,任何一个线程修改
了value值,会将其立即写回内存当中*/
    private volatile int value;    
    //对int进行原子操作,进行初始化的构造函数
    public AtomicInteger(int initialValue) {
        value = initialValue;
    }
    //空的构造方法
    public AtomicInteger() {}
    //获取当前值
    public final int get() {
        return value;
    }
    //设置value值
    public final void set(int newValue) {
        value = newValue;
    }
    //最终设置value值,通过unsafe.putOrderedInt()设置新值
    public final void lazySet(int newValue) {
        unsafe.putOrderedInt(this, valueOffset, newValue);
    }
    //原子操作,将value值设置为newValue,并返回修改前的value值
    public final int getAndSet(int newValue) {
        /*无限循环,保证每次如果compareAndSet这个方法失败之后,能重新进行尝试,直到成功将value
值设置为newValue。保证了原子性*/
        for (;;) {
            int current = get();    //获取当前值
            if (compareAndSet(current, newValue)) //期望值就是当前值,而newValue为update值
                return current;
        }
    }
/*如果expect值和valueOffset处的值一样,将update值放到valueOffset处,compareAndSet保证了:
a) 只有field的值为expect时;b) 将field的值修改为update的值;这两步是原子完成的。同时field一定
为一个volatile属性,而volatile保证了属性在线程间的可见性,以及防止了指令的重排序。
*/
    public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }
/*
因此之所以在getAndSet方法中调用一个for循环,即保证如果调用compareAndSet这个方法返回为false时,
能再次尝试进行修改value的值,直到修改成功,并返回修改前value的值。

整个代码能保证在多线程时具有线程安全性,并且没有使用java中任何锁的机制,所依靠的便是Unsafe这个类
中调用的该方法具有原子性,这个原子性的保证并不是靠java本身保证,而是靠一个更底层的与操作系统相关
的特性实现。
*/
    /*在jdk9之前两个是一样的*/
    public final boolean weakCompareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }
    /*对当前value值+1,其他的getAndSet一样分析,获得旧值*/
    public final int getAndIncrement() {
        for (;;) {
            int current = get();
            int next = current + 1;
            if (compareAndSet(current, next))
                return current;
        }
    }
    /*对当前value值-1,其他的getAndSet一样分析,获得旧值*/
    public final int getAndDecrement() {
        for (;;) {
            int current = get();
            int next = current - 1;
            if (compareAndSet(current, next))
                return current;
        }
    }
    /*对当前value值+给定的值delta,其他的getAndSet一样分析,获得旧值*/
    public final int getAndAdd(int delta) {
        for (;;) {
            int current = get();
            int next = current + delta;
            if (compareAndSet(current, next))
                return current;
        }
    }
    /*对当前value值+1,其他的getAndSet一样分析,获得新值*/
    public final int incrementAndGet() {
        for (;;) {
            int current = get();
            int next = current + 1;
            if (compareAndSet(current, next))
                return next;
        }
    }
    /*对当前value值-1,其他的getAndSet一样分析,获得新值*/
    public final int decrementAndGet() {
        for (;;) {
            int current = get();
            int next = current - 1;
            if (compareAndSet(current, next))
                return next;
        }
    }
    /*对当前value值+指定值delta,其他的getAndSet一样分析,获得新值*/
    public final int addAndGet(int delta) {
        for (;;) {
            int current = get();
            int next = current + delta;
            if (compareAndSet(current, next))
                return next;
        }
    }
    /*返回当前值的字符串*/
    public String toString() {
        return Integer.toString(get());
    }
    //获取value值
    public int intValue() {
        return get();
    }
    //转化为long型在获取
    public long longValue() {
        return (long)get();
    }
    //转化为float型在获取
    public float floatValue() {
        return (float)get();
    }
    //转化为double型在获取
    public double doubleValue() {
        return (double)get();
    }

}

了解这这个原子类,拿一篇文章顺顺思路:https://blog.csdn.net/mazhimazh/article/details/18908493

volatile

volatile 的特性

volatile变量自身具有下列特性:

① 可见性/一致性:对一个 volatile 变量的读,总是能看到(任意线程)对这个 volatile 变量最后的写入。
② 原子性:对任意单个 volatile 变量的读/写具有原子性,但类似于 volatile++这种复合操作不具有原子性。

Q:volatile是如何保证可见性的了?

A:在多核处理器中,当进行一个volatile变量的写操作时,JIT编译器生成的汇编指令会在写操作的指令前在上一个“lock”前缀。“lock”前缀的指令在多核处理器下会引发了两件事情:

① 将当前处理器缓存行的数据会写回到系统内存。
② 这个写回内存的操作会引起在其他CPU里缓存了该内存地址的数据无效。

扫描二维码关注公众号,回复: 2530622 查看本文章

因此更确切的来说,因为操作缓存的最小单位为一个缓存行,所以每次对volatile变量自身的操作,都会使其所在缓存行的数据会写回到主存中,这就使得其他任意线程对该缓存行中变量的读操作总是能看到最新写入的值( 会从主存中重新载入该缓存行到线程的本地缓存中 )。当然,也正是因为缓存每次更新的最小单位为一个缓存行,这导致在某些情况下程序可能出现“伪共享”的问题。嗯,好像有些个跑题,“伪共享”并不属于本文范畴,这里就不进行展开讨论。

好了,目前为止我们已经了解volatile变量自身所具有的特性了。注意,这里只是volatile自身所具有的特性,而volatile对线程的内存可见性的影响比volatile自身的特性更为重要。

volatile 写-读建立的 happens before 关系

happens-before 规则中有这么一条:
volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。

happens-before的这个规则会保证volatile写-读具有如下的内存语义:

  • volatile写的内存语义:

当写一个 volatile 变量时,JMM 会把该线程对应的本地内存中的共享变量值刷新到主内存。

  • volatile读的内存语义:

当读一个 volatile 变量时,JMM 会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。
为了实现 volatile 的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。因为内存屏障是一组处理器指令,它并不由JVM直接暴露,因此JVM会根据不同的操作系统插入不同的指令以达成我们所要内存屏障效果。

从整体执行效率的角度考虑,JMM 选择了在每个 volatile 写的后面插入一个 StoreLoad 屏障。

StoreLoad屏障
指令示例:Store1; StoreLoad; Load2
确保Store1数据对其他处理器变得可见(指刷新到内存)先于Load2及所有后续装载指令的装载。StoreLoad Barriers会使该屏障之前的所有内存访问指令(存储和装载指令)完成之后,才执行该屏障之后的内存访问指令。StoreLoad Barriers是一个“全能型”的屏障,它同时具有其他3个屏障的效果(LoadLoad Barriers、StoreStore Barriers、LoadStore Barriers)

好了,到现在我们知道了volatile的内存语义( happens-before关系 )会保证volatile写操作之前的读写操作不会被重排序到volatile写操作之后,并且保证了写操作后将线程本地内存(可能包含了多个缓存行)中所有的共享变量值都刷新到主内存中。这样其他线程总是能在volatile写操作后的读取操作中得到该线程中所有共享变量的正确值。这是volatile的happens-before关系( 通过内存屏障实现 )带给我们的结果。注意,这个和volatile变量自身的特性是不同的,volatile自身仅仅是保证了volatile变量本身的可见性。而volatile的happens-before关系则保证了操作不会被重排序同时保证了线程本地内存中所有共享变量的可见性。

猜你喜欢

转载自blog.csdn.net/xiao1_1bing/article/details/81330187