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

§5-6 深入了解原子包

5-6.1 原子包简介

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

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

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

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

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

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

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

除了表示单变量和数组的类,改包还包含了更新器类(Updater),可用于获取 compareAndSet 以及对任意所选类中的任意所选 volatile 字段上的相关操作。这些类早于 VarHandle,其使用更为局限。AtomicReferenceFieldUpdater, AtomicIntegerFieldUpdaterAtomicLongFieldUpdater 是基于反射的工具,提供对有关字段类型的访问。这主要在同一结点中(例如树节点的链接)多个 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。

接下来,追踪 UnsafegetAndAddInt 方法:

//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  阅读(36)  评论(0)    收藏  举报