Contents

AtomicMarkableReference 简介

1. 概述

在本教程中,我们将深入了解java.util.concurrent.atomic包中的AtomicMarkableReference类**的详细信息。

接下来,我们将介绍该类的 API 方法,并了解如何在实践中使用AtomicMarkableReference类。

2. 目的

AtomicMarkableReference是一个通用类,它封装了对Object的引用和boolean标志。这两个字段耦合在一起,可以一起或单独进行原子 更新

AtomicMarkableReference 也可能是解决ABA 问题 的一种方法。

3. 实施

让我们更深入地看一下AtomicMarkableReference类的实现:

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;
    // ...
}

请注意,AtomicMarkableReference 有一个包含引用和标志的静态嵌套类Pair

此外,我们看到两个变量都是final。结果,每当我们要修改这些变量时,都会创建Pair类的新实例,并替换旧实例

4. 方法

首先,要发现AtomicMarkableReference的用处,让我们从创建一个Employee POJO 开始:

class Employee {
    private int id;
    private String name;
    
    // constructor & getters & setters
}

现在,我们可以创建AtomicMarkableReference类的实例:

AtomicMarkableReference<Employee> employeeNode 
  = new AtomicMarkableReference<>(new Employee(123, "Mike"), true);

对于我们的示例,假设我们的AtomicMarkableReference实例表示组织结构图中的一个节点。它包含两个变量:对Employee类实例的reference和一个指示员工是活跃还是离开公司的mark

AtomicMarkableReference带有几种方法来更新或检索一个或两个字段。让我们一一看看这些方法:

4.1. getReference()

我们使用getReference方法返回reference变量的当前值:

Employee employee = new Employee(123, "Mike");
AtomicMarkableReference<Employee> employeeNode = new AtomicMarkableReference<>(employee, true);
Assertions.assertEquals(employee, employeeNode.getReference());

4.2. isMarked()

要获取mark变量的值,我们应该调用isMarked方法:

Employee employee = new Employee(123, "Mike");
AtomicMarkableReference<Employee> employeeNode = new AtomicMarkableReference<>(employee, true);
Assertions.assertTrue(employeeNode.isMarked());

4.3. get()

接下来,当我们想要同时检索当前reference和当前mark时,我们使用get方法。要获得mark,我们应该发送一个大小至少为 1的boolean数组作为参数,它将在索引 0 处存储boolean变量的当前值。同时,该方法将返回引用的当前值:

Employee employee = new Employee(123, "Mike");
AtomicMarkableReference<Employee> employeeNode = new AtomicMarkableReference<>(employee, true);
boolean[] markHolder = new boolean[1];
Employee currentEmployee = employeeNode.get(markHolder);
Assertions.assertEquals(employee, currentEmployee);
Assertions.assertTrue(markHolder[0]);

这种获取referencemark字段的方式有点奇怪,因为内部Pair类没有暴露给调用者。

Java在其公共 API中没有通用的*Pair<*T, U>类。主要原因是我们可能会过度使用它而不是创建不同的类型。

4.4. put()

如果我们想无条件地更新referencemark字段,我们应该使用set方法。如果作为参数发送的至少一个值不同,则referencemark将被更新:

Employee employee = new Employee(123, "Mike");
AtomicMarkableReference<Employee> employeeNode = new AtomicMarkableReference<>(employee, true);
Employee newEmployee = new Employee(124, "John");
employeeNode.set(newEmployee, false);

Assertions.assertEquals(newEmployee, employeeNode.getReference());
Assertions.assertFalse(employeeNode.isMarked());

4.5. compareAndSet()

接下来,如果当前reference等于预期reference,并且当前mark等于预期mark,则compareAndSet方法将referencemark都更新为给定的更新值。**

现在,让我们看看如何使用compareAndSet更新referencemark字段:

Employee employee = new Employee(123, "Mike");
AtomicMarkableReference<Employee> employeeNode = new AtomicMarkableReference<>(employee, true);
Employee newEmployee = new Employee(124, "John");
Assertions.assertTrue(employeeNode.compareAndSet(employee, newEmployee, true, false));
Assertions.assertEquals(newEmployee, employeeNode.getReference());
Assertions.assertFalse(employeeNode.isMarked());

此外,在调用compareAndSet方法时,如果字段已更新,我们将获得true ,如果更新失败,我们将获得false

4.6. weakCompareAndSet()

weakCompareAndSet方法应该是compareAndSet方法的弱版本。**也就是说,它不像 compareAndSet那样提供强大的内存排序保证。此外,在硬件级别获得独占访问可能会虚假失败。

这是 weakCompareAndSet方法的规范。然而,**目前,weakCompareAndSet只是 在底层调用compareAndSet方法 。**因此,它们具有相同的强大实现。

即使这两种方法现在具有相同的实现,我们也应该根据它们的规范使用它们。因此,我们应该将 weakCompareAndSet 视为一个弱原子

在某些平台和某些情况下,弱原子可能会更便宜。例如,如果我们要在循环中执行 compareAndSet  ,使用较弱的版本可能是一个更好的主意。在这种情况下,我们最终会在循环中更新状态,因此虚假失败不会影响程序的正确性。

底线是,弱原子在某些特定用例中可能很有用,因此并不适用于所有可能的场景。因此,当有疑问时,更喜欢更强的compareAndSet

4.7. attemptMark()

最后,我们有attemptMark方法。它检查当前reference是否等于作为参数发送的预期reference。如果它们匹配,它将自动将标记的值设置为给定的更新值:

Employee employee = new Employee(123, "Mike");
AtomicMarkableReference<Employee> employeeNode = new AtomicMarkableReference<>(employee, true);
Assertions.assertTrue(employeeNode.attemptMark(employee, false));
Assertions.assertFalse(employeeNode.isMarked());

重要的是要注意,即使预期和当前reference值相等,这种方法也可能会虚假失败。因此,我们应该注意方法执行返回的boolean

如果mark更新成功,则结果为true,否则为false。但是,当当前reference等于预期reference时重复调用会修改mark值。因此,建议在while循环结构中使用此方法

此失败可能是由于attemptMark方法用于更新字段的基础比较和交换 (CAS) 算法的结果。如果我们有多个线程尝试使用 CAS 更新相同的值,其中一个会设法更改值,而其他线程会收到更新失败的通知。