AtomicMarkableReference指南

2023/06/07

1. 概述

在本文中,我们将深入了解java.util.concurrent.atomic包中AtomicMarkableReference类的细节

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

2. 目的

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

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,其中包含reference和mark字段

此外,我们可以看到这两个变量都使用final修饰。因此,每当我们想要修改这些变量时,就会创建Pair类的一个新实例,并替换旧实例

4. 方法

首先,要发现AtomicMarkableReference的有用性,让我们从创建一个员工POJO开始:

class Employee {
	private int id;
	private String name;

	// constructor & getters & setters
}

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

AtomicMarkableReference<Employee> employeeNode = new AtomicMarkableReference<>(employee,true);

对于我们的示例,假设我们的AtomicMarkableReference实例代表组织结构图中的一个节点。它包含两个变量:对Employee类实例的引用和一个标记,该标记指示员工是否在职或者已离开公司。

AtomicMarkableReference提供了多种方法来更新或检索一个或两个字段,让我们一一看一下这些方法。

4.1 getReference()

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

@Test
void whenUsingGetReferenceMethod_thenCurrentReferenceShouldBeReturned() {
    Employee employee = new Employee(123, "Mike");
    AtomicMarkableReference<Employee> employeeNode = new AtomicMarkableReference<>(employee, true);

    assertEquals(employee, employeeNode.getReference());
}

4.2 isMarked()

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

@Test
void givenMarkValueAsTrue_whenUsingIsMarkedMethod_thenMarkValueShouldBeTrue() {
    Employee employee = new Employee(123, "Mike");
    AtomicMarkableReference<Employee> employeeNode = new AtomicMarkableReference<>(employee, true);

    assertTrue(employeeNode.isMarked());
}

4.3 get()

接下来,当我们想要检索当前引用和当前标记时,我们使用get()方法。为了获得标记,我们应该将一个大小至少为1的布尔数组作为参数传递,该数组将在索引0处存储布尔变量的当前值。同时,该方法将返回引用的当前值:

@Test
void whenUsingGetMethod_thenCurrentReferenceAndMarkShouldBeReturned() {
    Employee employee = new Employee(123, "Mike");
    AtomicMarkableReference<Employee> employeeNode = new AtomicMarkableReference<>(employee, true);

    boolean[] markHolder = new boolean[1];
    Employee currentEmployee = employeeNode.get(markHolder);

    assertEquals(employee, currentEmployee);
    assertTrue(markHolder[0]);
}

这种获取引用和标记字段的方式有点奇怪,因为内部Pair类不向调用方公开。

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

4.4 set()

如果我们想要无条件地更新引用和标记字段,我们应该使用set()方法。如果作为参数传递的值中至少有一个不同,则引用和标记值将被更新:

@Test
void givenNewReferenceAndMark_whenUsingSetMethod_thenCurrentReferenceAndMarkShouldBeUpdated() {
    Employee employee = new Employee(123, "Mike");
    AtomicMarkableReference<Employee> employeeNode = new AtomicMarkableReference<>(employee, true);

    Employee newEmployee = new Employee(124, "John");
    employeeNode.set(newEmployee, false);

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

4.5 compareAndSet()

接下来,如果当前引用等于预期引用,并且当前标记等于预期标记,则compareAndSet()方法将引用和标记更新为给定的更新值

让我们看看如何使用compareAndSet()更新引用字段和标记字段:

@Test
void givenCurrentReferenceAndCurrentMark_whenUsingCompareAndSet_thenReferenceAndMarkShouldBeUpdated() {
    Employee employee = new Employee(123, "Mike");
    AtomicMarkableReference<Employee> employeeNode = new AtomicMarkableReference<>(employee, true);
    Employee newEmployee = new Employee(124, "John");

    assertTrue(employeeNode.compareAndSet(employee, newEmployee, true, false));
    assertEquals(newEmployee, employeeNode.getReference());
    assertFalse(employeeNode.isMarked());
}

此外,在调用compareAndSet()方法时,如果字段被更新,则返回true;如果更新失败,则为false。

4.6 weakCompareAndSet()

weakCompareAndSet()方法应该是compareAndSet()方法的较弱版本,也就是说,它不像compareAndSet()那样提供强大的内存排序保证。此外,它可能会错误地无法在硬件级别获得独占访问。

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

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

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

底线是,弱原子在某些特定的用例中是有用的,因此并不适用于所有可能的场景。如果有任何疑问,请选择更强的compareAndSet()。

4.7 attemptMark()

最后,对于attemptMark()方法,它检查当前引用是否等于作为参数传递的预期引用。如果它们匹配,它会自动将标记值原子设置为给定的更新值:

@Test
void givenTheSameObjectReference_whenUsingAttemptMarkMethod_thenMarkShouldBeUpdated() {
    Employee employee = new Employee(123, "Mike");
    AtomicMarkableReference<Employee> employeeNode = new AtomicMarkableReference<>(employee, true);

    assertTrue(employeeNode.attemptMark(employee, false));
    assertFalse(employeeNode.isMarked());
}

值得注意的是,即使预期引用值和当前引用值相等,此方法也可能会错误地失败。因此,我们应该注意方法执行返回的布尔值

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

此故障可能是由attemptMark()方法用于更新字段的compare-and-swap(CAS)算法造成的,如果我们有多个线程试图使用CAS更新同一个值,其中一个线程会设法更改该值,而另一个线程会收到更新失败的通知。

5. 总结

在本快速指南中,我们学习了AtomicMarkableReference类是如何实现的。此外,我们还说明了如何通过类的公共API方法来原子地更新其属性。

与往常一样,本教程的完整源代码可在GitHub上获得。

Show Disqus Comments

Post Directory

扫码关注公众号:Taketoday
发送 290992
即可立即永久解锁本站全部文章