17 通过AtomicBoolean制作一个执行开关
Diego38 68 1

1. 前言

前面我们讲了 AtomicInteger 使用,它是针对 Integer 类型的原子更新类。

当我们针对 Boolean 类型变量进行多线程操作时,首先想到的就是针对这个 Boolean 变量做 volatile 修饰,使得一个线程对变量的修改能够立即被其他线程看得到。

但是解决不了原子更新的问题,比如生活中 "当门打开时把它关上", 假设门敞开的状态用 true/false 表示,这就需要 CAS 算法来解决,本节我们就学习针对 Boolean 类型的原子更新类 AtomicBoolean 是如何实现 CAS 的。

2. AtomicBoolean 的 API 说明

AtomicBoolean 的 API 相比 AtomicInteger 比较少,源码如此:

public class AtomicBoolean implements java.io.Serializable {
    private static final long serialVersionUID = 4654671469794556979L;
    // setup to use Unsafe.compareAndSwapInt for updates
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;

    static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicBoolean.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

    private volatile int value;

    /**
     * Creates a new {@code AtomicBoolean} with the given initial value.
     *
     * @param initialValue the initial value
     */
    public AtomicBoolean(boolean initialValue) {
        value = initialValue ? 1 : 0;
    }

    /**
     * Creates a new {@code AtomicBoolean} with initial value {@code false}.
     */
    public AtomicBoolean() {
    }

...

源码中有一个用于执行 CAS 本地方法的 Unsafe 辅助类,valueOffset 指向变量 value 的内存地址。而本身包含一个经过 volatile 修饰 int 变量,而不是 Boolean 类型,这是因为虚拟机底层是用 int 来代替 boolean 的,Unsafe 里面进行 CAS 操作的底层本地方法也是 compareAndSwapInt 或者 compareAndSwapLong,而没有 compareAndSwapBoolean。

  • compareAndSet 和 AtomicInteger 不同的是 AtomicBoolean 没有循环 CAS 的方法,只有两个 CAS 方法分别是 compareAndSet 和 weakCompareAndSet,在 JDK1.8 及之前的版本,compareAndSet 和 weakCompareAndSet 的实现是一样的,我们看下 compareAndSet 的代码。

    public final boolean compareAndSet(boolean expect, boolean update) {
          int e = expect ? 1 : 0;
          int u = update ? 1 : 0;
          return unsafe.compareAndSwapInt(this, valueOffset, e, u);
      }

    由于 valueOffset 地址指向的 value 是 int 类型,上述源码将 expect 和 update 转换为 1 或者 0,然后进行更新。

  • get

    public final boolean get() {
          return value != 0;
      }

    get 方法很简单,直接对 value 判断非 0 来得到 true 还是 false,当 value1 时返回 false,当 value0 时返回 true。

  • set

    public final void set(boolean newValue) {
          value = newValue ? 1 : 0;
      }

    同样 set 方法会通过传入的 value 值转换为 int 类型写入到 value 中。

其实如果没有使用 CAS 的场景,我们完全可以通过对 Boolean 变量进行 volatile 修饰来实现的,get 和 set 方法对应于对 volatile boolean 类型的直接读取和写入。

那 AtomicBoolean 的 CAS 通常用在什么场景呢?接下来我们以一个例子进行说明。

3. 使用 AtomicBoolean 制作一个执行开关

我们需要监控内存的使用量,只需要一个线程就足够了,为了防止多个线程同时执行监控操作,我们需要加一个 AtomicBoolean 开关,代码如下:

public class AtomicBooleanTest {

    public static AtomicBoolean starting = new AtomicBoolean();


    public static void memoryStats() {
        int mb = 1024 * 1024;
        Runtime instance = Runtime.getRuntime();
        System.out.println("当前虚拟机使用的内存: "
                + (instance.totalMemory() - instance.freeMemory()) / mb);
    }


    public static void main(String[] args) {
        Runnable task = () -> {
            if (starting.compareAndSet(false, true)) {
                System.out.println(Thread.currentThread().getName() + " 开始执行...");
                System.out.println("只会执行一次...");
                while (true) {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {}
                    memoryStats();
                }
            }
        };
        Thread taskThread1 = new Thread(task, "memoryTask1");
        taskThread1.start();

        Thread taskThread2 = new Thread(task, "memoryTask2");
        taskThread2.start();
    }
}

输出如下:

memoryTask1 开始执行...
只会执行一次...
当前虚拟机使用的内存: 12
当前虚拟机使用的内存: 12
当前虚拟机使用的内存: 12

我们新建了一个 Runnable 对象,用于循环输出当前内存使用量,并且启动 taskThread1 执行,通过一个 AtomicBoolean 变量来表示监控开关是否启动,通过 compareAndSet 方法,表示当开关关闭时打开开关,并且只打开一次。这样即使后续我们启动了 taskThread2 线程,由于执行 compareAndSet 返回 false,自动退出了线程,阻止了多线程同时运行监控的发生。

4. 总结

AtomicBoolean 内部是使用 int 类型的 value 来表示的,在做 compareAndSet、get、set 时是将参数中 Boolean 变量转换为 int 来执行的。AtomicBoolean 常用于状态开关场景,在开源中间件的使用非常普遍。

预览图
评论区

索引目录