【Java 多线程】5 - 6 深入了解原子包

§5-6 深入了解原子包

5-6.1 原子包简介

官方文档中对于原子包的介绍:

原子包 java.util.concurrent.atomic 是并发包中的一个子包,该包是一个小型工具包,提供了免锁、线程安全的单变量类。

每个 AtomicBoolean, AtomicInteger, AtmoicLong 和 AtomicReference 类实例都提供了对应类型变量的访问和更新。每个类也为对应类型提供了适当的工具方法。例如,AtomicLong 和 AtomicInteger 提供了原子自增方法。其中一种应用是生成数字序列,如下:

class Sequencer {
    private final AtomicLong sequenceNumber = new AtomicLong(17);
    public long next() {
        return sequenceNumber.getAndIncrement();
    }
}

所含值的任意变换由两种方式提供,一种是低级的读取-修改-写入操作(如 compareAndSet),另一种是高级方法(如 getAndUpdate)。

这些类都不能够处于一般目的替换 java.lang.Integer 和有关类。他们并没有定义诸如 equals, hashCode 和 compareTo 方法。因为原子变量可能会变化,不适于用作哈希表的键。

AtomicIntegerArray, AtomicLongArray 以及 AtomicReferenceArray 类进一步延展了原子操作,使得类支持对应类型的数组操作。值得注意的是,这些类对数组元素提供了 volatile 访问语义。

除了表示单变量和数组的类,改包还包含了更新器类(Updater),可用于获取 compareAndSet 以及对任意所选类中的任意所选 volatile 字段上的相关操作。这些类早于 VarHandle,其使用更为局限。AtomicReferenceFieldUpdater, AtomicIntegerFieldUpdater 和 AtomicLongFieldUpdater 是基于反射的工具,提供对有关字段类型的访问。这主要在同一结点中(例如树节点的链接)多个 volatile 字段都各自依赖于原子更新的原子数据结构中更加有用。这些类在何时以及怎样使用原子更新方面,提供了更好的灵活性,但需要更为笨拙的基于反射的设置、更不便的使用,和更弱的保障。

AtomicMarkableReference 类将一个布尔值与一个引用关联。例如,该比特位可能用于一个数据结构内部,表示引用的对象逻辑上已被删除。AtomicStampedReference 类将一个整型与一个引用关联。这可能用于,例如,表示一系列更新的对应版本号。

概要:

自 JDK 1.5 起,Java 提供了原子包,这个高中的原子操作类提供了一种用法简单、性能高效、线程安全的更新变量的方式。根据变量类型,可将该包中的类分为四种类型:原子更新基本类型、原子更新数组、原子更新引用、原子更新属性(字段)、累加器。如下表所示:

分类 类
原子更新基本类型 AtomicBoolean,AtomicInteger, AtomicLong
原子更新引用类型 AtomicMarkableReference<V>, AtomicReference<V>, AtomicStampedReference<V>
原子更新数组类型 AtomicIntegerArray, AtomicLongArray, AtomicReferenceArray<E>
原子更新对象属性 AtomicIntegerFieldUpdater<T>, AtomicLongFieldUpdater<T>, AtomicReferenceFieldUpdater<T,V>

下文内容部分参考自:

这一次,彻底搞懂Java并发包中的Atomic原子类 - 掘金 (juejin.cn)

5-6.2 原子更新基本类型

原子更新基本类型主要有布尔型、整型、长整型,每一个实例都表示一个可以原子更新的值。

由于原子更新基本类型的三个类用法几乎一模一样,这里仅以 AtomicInteger 为例,介绍其用法、从内存和源码分析。

构造方法:

构造方法 描述
AtomicInteger() 创建一个新的原子更新整型,初始值为 0
AtomicInteger(int initialValue) 创建一个新的原子更新整型,并给定初始值

常用方法:

方法 描述
int get() 返回当前值
int getAndIncrement() 以原子方式自增,并返回自增前的值
int getAndDecrement() 以原子方式自减,并返回自减前的值
int getAndAdd(int delta) 以原子方式相加,并返回相加前的值
int incrementAndGet() 以原子方式自增,并返回自增后的值
int decrementAndGet() 以原子方式自减,并返回自减后的值
int addAndGet(int delta) 以原子方式相加,并返回相加后的值
void set(int newValue) 设置新的值

示例:

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicDemo {
    public static void main(String[] args) throws InterruptedException {
        //原子更新基本类型演示
        incrementPrimitives();
    }

    private static void incrementPrimitives() throws InterruptedException {
        AtomicInteger primitive = new AtomicInteger();

        System.out.println("原子更新基本类型:以整型为例");
        new Thread(() -> {
            for (int i = 0 ; i < 1000; i++) {
                primitive.incrementAndGet();
            }
        }).start();

        new Thread(() -> {
            for (int i = 0 ; i < 1000; i++) {
                primitive.incrementAndGet();
            }
        }).start();

        Thread.sleep(1000);
        System.out.println("最终结果:" + primitive);
    }
}

运行结果:

原子更新基本类型:以整型为例
最终结果:2000

结构:以 AtomicInteger 为例,看看它的结构。

public class AtomicInteger extends Number implements java.io.Serializable {
    private static final long serialVersionUID = 6214790243416807050L;
    
    private static final Unsafe U = Unsafe.getUnsafe();
    private static final long VALUE = U.objectFieldOffset(AtomicInteger.class, "value");
    
    private volatile int value;		// 存储所维护的整型变量
    
    // 方法...
}

5-6.3 原子更新引用类型

原子更新引用类型和原子更新基本类型一样,每个实例都只维护一个可以原子更新的引用类型变量。

原子更新引用类型有三种:AtomicReference(原子更新引用)、AtomicMarkableReference(带标记的原子更新引用类型)、AtomicStampedReference(带版本号的原子更新引用类型)。

其中,AtomicMarkableReference 使用一个布尔标记关联引用,AtomicStampedReference 将一个整型数值与引用关联起来,作为数据原子更新的版本号,用于解决 CAS 算法中的 ABA 问题。

构造方法:

构造方法 描述
AtomicReference()
AtomicReference(V initialValue)
使用 null 初值/给定初值创建一个新的原子更新引用
AtomicMarkableReference(V initialRef, boolean initialMark) 使用给定初值创建一个新的原子更新标记引用
AtomicStampedReference(V initialRef, int initialStamp) 使用给定初值创建一个新的原子更新戳引用

常用方法:

常用方法 描述
V get() 返回当前值
V getAndSet(V newValue) 将引用原子更新为指定新值,并返回旧值
void set(V newValue) 将引用原子更新为指定新值
boolean compareAndSet(V expectedValue, V newValue) 若当前值等于期望值,则原子更新该引用为指定新值

示例:

import java.util.concurrent.atomic.AtomicReference;

public class AtomicDemo {
    public static void main(String[] args) throws InterruptedException {
        //原子更新引用类型演示
        referenceDemo();
    }

    private static void referenceDemo() {
        System.out.println("原子更新引用演示:");
        AtomicReference<Coupon> ref = new AtomicReference<>();
        Coupon kfc = new Coupon("Vivo50", 50);
        Coupon mcDonoald = new Coupon("Wheat Gate", 45);

        ref.set(mcDonoald);
        ref.compareAndSet(mcDonoald, kfc);
        System.out.println("优惠券:" + ref.get().getTitle() + ", 余量:" + ref.get().getRemains());
    }

    static class Coupon {
        private String title;
        private int remains;

        public Coupon(String title, int remains) {
            this.title = title;
            this.remains = remains;
        }
    }
}

运行结果:

原子更新引用演示:
优惠券:Vivo50, 余量:50

结构:

public class AtomicReference<V> implements java.io.Serializable {
    private static final long serialVersionUID = 1848883965231344442L;
    private static final VarHandle VALUE;
    
    static {
        try {
            MethodHandles.Lookup l = MethodHandles.lookup();
        	VALUE = l.findVarHandle(AtomicReference.class, "value", Object.class);
        } catch (ReflectiveOperationException e) {
            throw new ExceptionInIntializerError(e);
        }
    }
    
    // 方法...
}
public class AtomicMarkableReference<V> {
    private static class Pair<T> {
        final T reference;
        final boolean mark;
        
        private Pair(T reference, boolean mark) {
            this.reference = reference;
            this.mark = mark;
        }
        
       	static <T> Pair<T> of(T reference, boolean mark) {
            return new Pair<T>(reference, mark);
        }
    }
    
    private volatile Pair<V> pair;
    
    public AtomicMarkableReference(V initialRef, boolean initialMark) {
        pair = Pair.of(initialRef, initialMark):
    }
    
    // 方法...
}
public class AtomicStampedReference<V> {
    private static class Pair<T> {
        final T reference;
        final int stamp;
        
        private Pair<T>(T reference, int stamp) {
            this.reference = reference;
            this.stamp = stamp;
        }
        
        static <T> Pair<T> of(T reference, int stamp) {
            return new Pair<T>(reference, stamp);
        }
    }
    
    private volatile Pair<T> pair;
    
    public AtomicStampedReference(V initialRef, int initialStamp) {
        pair = Pair.of(initialRef, initialStamp);
    }
    
    // 方法...
}

5-6.4 原子更新数组类型

原子更新数组类型更新的是数组当中的元素。原子更新数组类型也有三种:AtomicIntegerArray, AtomicLongArray 以及 AtomicReferenceArray。

构造方法:

构造方法 描述
AtomicIntegerArray(int length)
AtomicIntegerArray(int[] array)
创建一个具有指定长度,元素初始为 0 的原子整型数组
创建一个与指定数组长度、元素一致的原子整型数组
AtomicLongArray(int length)
AtomicLongArray(long[] array)
创建一个具有指定长度,元素初始为 0 的原子长整型数组
创建一个与指定数组长度、元素一致的原子长整型数组
AtomicReferenceArray(int length)
AtomicReferenceArray(E[] array)
创建一个具有指定长度,元素初始为空的原子引用数组
创建一个与指定数组长度、元素一致的原子引用数组

常用方法:

常用方法 描述
int/long/E get(int i) 返回数组中指定索引处的元素
int/long/E getAndSet(int i, int/long/E newValue) 原子地将数组指定索引处的元素更新为指定新值,并返回旧值
boolean compareAndSet(int i, int/long/E expectedValue, int/long/E newValue) 当元素当前值与期望值相等时,将元素值修改为指定新值
int length() 返回数组长度
int/long addAndGet(int i, int/long delta)
int/long decrementAndGet(int i)
int/long getAndAdd(int i, int/long delta)
int/long getAndDecrement(int i)
int/long getAndIncrement(int i)
int/long IncrementAndGet(int i)
语义同原子更新基本类型

结构:以 AtomicIntegerArray 为例。

public class AtomicIntegerArray implements java.io.Serializable {
    private static final long serialVersionUID = 2862133569453604235L;
    private static final VarHandle AA = MethodHandles.arrayElementVarHandle(int[].class);
    private final int[] array;
    
    public final void set(int i, int newValue) {
        AA.setVolatile(array, i, newValue);
    }
    
    public final int getAndSet(int i, int newValue) {
        return (int) AA.getAndSet(array, i, newValue);
    }
    
    public final boolean compareAndSet(int i, int expectedValue, int newValue) {
        return AA.compareAndSet(array, i, expectedValue, newValue);
    }
    
    // 方法...
}

AtomicIntegerArray 内部维护了一个 final 修饰的 int 数组,类中所有操作都是针对数组元素的原子操作,保证了线程安全。

5-6.5 原子更新对象属性

若只想更新某个对象中的某个字段,可以使用对象字段的原子类。有三个类:AtomicIntegerFieldUpdater, AtomicLongFieldUpdater, AtomicReferenceUpdater。

使用条件:

  1. 被操作的字段不能是静态的(static);
  2. 被操作的字段不能是最终的(final);
  3. 被操作的字段必须由 volatile 修饰;
  4. 属性必须对于当前的更新器所在区域可见;

构造方法:该类的构造方法声明为 protected,但提供了静态的工厂方法,返回一个该类的对象。

静态方法 描述
AtomicIntegerFieldUpdater<U> newUpdater(Class<U> tclass, String fieldName) 创建并返回一个关联对象指定字段的更新器
AtomicLongFieldUpdater<U> newUpdater(Class<U> tclass, String fieldName) 创建并返回一个关联对象指定字段的更新器
AtomicReferenceFieldUpdater<U,W> newUpdater(Class<U> tclass, Class<W> wclass, String fieldName) 创建并返回一个关联对象指定字段的更新器

示例:以上文的 Coupon 为例。

import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;

public class AtomicDemo {
    public static void main(String[] args) {
        //更新器演示
        updaterDemo();
    }
    
    private static void updaterDemo() {
        Coupon kfc = new Coupon("Vivo50", 50);
        Coupon mcDonoald = new Coupon("Wheat Gate", 50);
        AtomicIntegerFieldUpdater<Coupon> updater = AtomicIntegerFieldUpdater.newUpdater(Coupon.class, "remains");

        //利用更新器更新字段
        updater.set(kfc, 100);
        System.out.println("使用更新器修改对象字段,修改后:");
        System.out.println("KFC: " + kfc.getRemains() + ", MCD: " + mcDonoald.getRemains());
    }

    static class Coupon {
        private String title;
        private volatile int remains;

        public Coupon(String title, int remains) {
            this.title = title;
            this.remains = remains;
        }
        
        //...
    }
}

运行结果:

使用更新器修改对象字段,修改后:
KFC: 100, MCD: 50

5-6.6 CAS 算法与自旋

文首提到,原子包中所提供的类是免锁的线程安全类,而无锁的实现依赖于 CAS 算法。

5-6.6.1 CAS 算法介绍

无锁核心:自旋 + CAS 算法。

CAS 算法:CAS 全称为 Compare and Swap,比较与交换。CAS 机制主要用于一个变量(操作)进行原子性的操作。

CAS 的表达式为 compareAndSet(V,E,N)。其中,V 表示要更新的变量,E 表示期望值,N 表示修改值。调用该函数修改变量 V 时,会先查看 V 的值是否符合期望值 E。若符合,则会将该值更新为 E;否则,V 的值不符合期望值,说明有其他线程正在修改该变量,则不更新变量,而是重新读取变量的值再次调用该函数尝试更新变量,直至修改成功。这个重新读取再尝试的循环过程称为自旋。

自旋锁:自旋锁是一种轻量级的锁机制,用于解决对某项资源的互斥使用。在任何时刻,自旋锁最多只能有一个持有者。一旦该锁由某一线程持有,其他线程尝试获取该锁时,会一直循环(忙等待)检测锁是否被释放,线程不会被阻塞(non-blocking),而不会进入线程挂起或睡眠状态,带来一定程度上的消耗。

自旋锁的原理较为简单,若持有者县城能够在短时间内释放锁,那么这些等待锁的竞争线程不需要进入阻塞状态,只需要等一等(自旋)即可。但是,长时间不释放锁的话,自旋锁将会十分消耗性能,因为这时可能有大量的线程在自旋,占用 CPU 资源。

CAS 缺点:CAS 可以实现无锁状态下的同步,但也有一些局限性。

  1. 只能保证一个共享变量的原子性:CAS 不如监视器锁和可重入锁一样可保证一段代码和多个变量的同步,要想实现多变量同步,还是需要加锁处理;
  2. 存在性能开销问题:由于 CAS 需要自旋操作,若长时间自旋都无法成功修改变量,会给 CPU 带来很大的开销;
  3. ABA 问题:CAS 是通过检查变量值是否发生改变从而保证原子性。设存在两个线程,线程 1 和 2 同时都读到了变量值为 A,但此时线程 1 将变量的值改为了 B,又从 B 改回了 A,期间线程 2 一直没有抢到 CPU 的执行权,直到线程 1 将变量改回原值后才得以执行。这时,线程 2 就不清楚该变量的值曾变过,发生了 ABA 问题。ABA 的问题处理方法比较简单,只需要在更新值的同时更新版本号即可,AtomicStampedReference 就是通过这种方式解决这一问题的。

CPU 对 CAS 的支持:在操作系统中 CAS 操作实际上是一条 CPU 的原子指令,在操作系统中属于一种原语,原语由多条指令组成,其执行是连续不可中断的。CAS 操作看上去是一个先比较后交换的操作,但实际上是由 CPU 保证了原子操作。

5-6.6.4 从源码角度看 CAS

源码解析:这里以 AtomicInteger 为例,看看 CAS 是如何工作的。

这里,我们主要来看看 incrementAndGet 方法的源码。

//incrementAndGet 源码
public final int incrementAndGet() {
    return U.getAndAddInt(this, VALUE, 1) + 1;
}

方法通过一个变量 U,调用了其对应类中的方法。追踪该变量,可以看到,AtomicInteger 中定义了该变量:

private static final Unsafe U = Unsafe.getUnsafe();

其中,Unsafe 是一个执行低级、不安全操作的方法的集合。由于类中大多数方法都是低级的不安全方法,因此只有受信任的代码才能够调用类中的方法。

在处理多线程并发问题时,源码在底层大量地使用了 Unsafe 的 API。

接下来,追踪 Unsafe 的 getAndAddInt 方法:

//return U.getAndAddInt(this, VALUE, 1) + 1;
//offset = VALUE,这实际上是报告具有给定名称字段在其类的存储分配中的位置
public final int getAndAddInt(Object o, long offset, int delta) {
    int v;
    //自旋锁的循环(自旋)
    do {
        //原生方法(native),获取旧值
        v = getIntVolatile(o, offset);
    } while (!weakCompareAndSetInt(o, offset, v, v + delta));
    //循环体的条件语句处调用方法 weakCompareAndSetInt(Object o, long offset, int expected, int x);
    //其底层又调用了 compareAndSetInt(o, offset, expected, x),方法返回值为boolean类型
    //其底层所调用的方法是原生方法,若 Java 变量当前拥有期望值(expected),则将变量原子更新为 x
    //整个循环使用了CAS算法,o为内存值,v为旧值(期望值),v + delta则是修改后的值
    //作用:比较内存中的值与旧值是否相等,是则将修改后的值写入内存,返回true,修改成功;否则不等,无法修改,返回false,修改失败;
    //若变量并没有拥有期望值,则继续等待,不断获取旧值,继续自旋
    //若变量拥有期望值,则自旋结束
    
    return v;
}

这样来看,Unsafe::getAndAddInt 的语义实际上是在做修改,修改完成后将旧值返回。但再来看 incrementAndGet 方法的返回语句:

return U.getAndAddInt(this, VALUE, 1) + 1;

那么方法最终就会修改并返回更新后的值。

同样地,getAndIncrement 方法在底层也调用了 Unsafe::getAndAddInt 方法,指示其返回语句为:

return U.getAndAddInt(this, VALUE, 1);

5-6.6.5 CAS 与 Unsafe

若细看 AtomicInteger 的源码,所有的操作都是通过 Unsafe 的成员变量实现。这是一个位于 jdk.internal.misc 的类,这个类中提供了大量低执行级别、不安全的操作。虽然该类及其所有方法都是公开的,但应当有限制地使用该类,仅有可信代码才能够获取该类的实例。

获取实例:该类的构造方法私有化,采用单例模式设计,提供了一个静态方法返回其唯一实例。

静态方法 描述
getUnsafe() 返回 Unsafe 实例,使得调用者能够执行不安全操作。

该类的单例模式设计:

public class Unsafe {
    private Unsafe() {}
    
    private static final Unsafe theUnsafe = new Unsafe();
    
    public static Unsafe getUnsafe() {
        return theUnsafe;
    }
}

Unsafe 中主要的 CAS 算法:

// 都是不可重写的原生方法,参数 o 指的是给定对象,offset 指的是对象内存偏移量,通过该偏移量迅速定位字段
public final native boolean compareAndSetInt(Object o, long offset, int expected, int x);
public final native boolean compareAndSetLong(Object o, long offset, long expected, long x);

这些方法都是原生方法,调用底层代码实现。在 JDK 1.8 中还引入了 getAndAddInt, getAndAddLong, getAndSetInt, getAndSetLong 等方法来支持不同类型的 CAS 操作。

除了 CAS 算法,Unsafe 类还提供了线程调度(park, unpark)、对象相关(例如绕过构造方法创建对象)、内存操作(例如堆外内存分配)、数组相关(例如定位数组中每个元素在内存中位置)等功能。

5-6.7 乐观锁与悲观锁

乐观锁:从乐观的角度出发,假设每次获取数据时数据都不会被篡改,因此不会上锁。只是在每次修改共享数据时,先检查数据是否被篡改,若没有,则修改数据,若有,则重新获取最新的值。(CAS)

悲观锁:从最坏的角度出发,认为每次获取数据的时候,都有可能被修改。因此每次在操作共享数据前,都会上锁保护数据。(监视器锁)

posted @ 2023-09-09 10:36  Zebt  阅读(80)  评论(0)    收藏  举报