# 一、简介

以原子方式更新对象引用赋值操作不是线程安全的,若不想用锁来实现,可以用AtomicReference<V>这个类。

# 二、自旋锁的设计【案例一】

描述:自旋锁,通常指的是一个线程重复尝试获取锁的一个过程。当锁被占用时,这个线程就会一直循环尝试(通常会添加一个睡眠以避免过于频繁的访问),这个过程便称之为自旋。

下面通过AtomicReferenceAtomicInteger原子标志设计一个简单的自旋锁spinlockSpinLock类:

import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;

public class SpinLock {

    // AtomicReference.compareAndSet()能够保证其原子性 
    private final AtomicReference<Thread> owner;

    private final AtomicInteger count;

    public SpinLock() {
        owner = new AtomicReference<Thread>();
        count = new AtomicInteger(0);
        
    }

    public void lock() {
        Thread thread = Thread.currentThread();
        if (thread == owner.get()) {
            count.incrementAndGet();
            return;
        }
        owner.compareAndSet(null, thread);
    }

    public void unlock() {
        Thread thread = Thread.currentThread();
        if (count.get() > 0) {
            count.decrementAndGet();
        }
        owner.compareAndSet(thread, null);
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

我们最后通过并发数据竞争验证上述自旋锁是否能够实现保护作用。

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Main {

    static SpinLock spinLock = new SpinLock();
    static int a = 0;

    public static void main(String[] args) throws InterruptedException {

        Thread[] threads = new Thread[10];
        for (int i = 0; i < 10; i++) {
            threads[i] = new Thread(Main::incre);
        }
        for (Thread thread : threads) {
            thread.start();
            thread.join();
        }

        System.out.println(a == 10000000 ? "Accepted" : "Wrong design of the spinlock");

    }

    private static void incre() {
        for (int i = 0; i < 1000000; i++) {
            spinLock.lock();
            ++a;
            spinLock.unlock();
        }
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

运行结果:Accepted

# 三、并发修改用户姓名【案例二】

如果通过案例二了解了AtomicReference可直接忽略案例二

一个线程使用user对象,10个线程负责更新该对象。那么就可以用AtomicReference这个类实现只允许同时一个线程修改对象。

@org.junit.Test
public void atomicUserTest() {
    final User user = new User("zzx", 23);
    AtomicReference<User> ref = new AtomicReference<>(user);
    for (int i = 0 ; i < 10 ; i++) {
        new Thread(new Runnable() {
            @SneakyThrows
            @Override
            public void run() {
                // 重点,我们看下ref操作是不是原子的
                if (ref.compareAndSet(user, new User("fj", 18))) {
                    System.out.println(Thread.currentThread().getId() + "Success");
                    System.out.println(ref.get().toString());
                }
            }
        }).start();
    }
}

class User {

    private String name;

    private Integer age;

    public User() {
    }

    public User(String name, Integer age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return super.toString();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54

运行结果:12Success

# 四、AtomicReference 源码分析

AtomicReference源码:可以看到它持有一个volatile修饰的对象引用valueOffset,并通过unsafe类来操作该引用。

public class AtomicReference<V> implements java.io.Serializable {
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;

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

    /** 该方法会将入参的expect变量所指向的对象和AtomicReference中的引用对象进行比较,
    如果两者指向同一个对象,则将AtomicReference中的引用对象重新置为update,修改成功返回true,失败则返回false。
    也就是说,AtomicReference其实是比较对象的引用。 **/
    public final boolean compareAndSet(V expect, V update) {
        return unsafe.compareAndSwapObject(this, valueOffset, expect, update);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 五、AtomicStampedReference

CAS操作可能存在ABA的问题:假如一个值原来是A,变成了B,又变成了A,那么CAS检查时会发现它的值没有发生变化,但是实际上却变化了。一般来讲这并不是什么问题,比如数值运算,线程其实根本不关心变量中途如何变化,只要最终的状态和预期值一样即可。

但是,有些操作会依赖于对象的变化过程,此时的解决思路一般就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A就会变成1A-2B-3A

AtomicStampedReference就是上面所说的加了版本号的AtomicReferenceAtomicStampedReference原理:先来看下如何构造一个AtomicStampedReference对象,AtomicStampedReference只有一个构造器:可以看到,除了传入一个初始的引用变量initialRef外,还有一个initialStamp变量,initialStamp其实就是版本号(或者说时间戳),用来唯一标识引用变量。

在构造器内部,实例化了一个Pair对象,Pair对象记录了对象引用和时间戳信息,采用int作为时间戳,实际使用的时候,要保证时间戳唯一(一般做成自增的),如果时间戳如果重复,还会出现ABA的问题。

public AtomicStampedReference(V initialRef, int initialStamp) {
    pair = Pair.of(initialRef, initialStamp);
}
1
2
3

AtomicStampedReference的所有方法,其实就是Unsafe类针对这个Pair对象的操作。和AtomicReference相比,AtomicStampedReference中的每个引用变量都带上了pair.stamp这个版本号,这样就可以解决CAS中的ABA问题了。

AtomicStampedReference使用案例:

// 创建AtomicStampedReference对象,持有Foo对象的引用,初始为null,版本为0
AtomicStampedReference<Foo>  asr = new AtomicStampedReference<>(null,0);  

// 版本号
int oldStamp=0         

//尝试以CAS方式更新引用对象,并将版本号+1
asr.compareAndSet(oldRef, null, oldStamp, oldStamp + 1)   
1
2
3
4
5
6
7
8

上述模板就是AtomicStampedReference的一般使用方式,注意下compareAndSet方法:

public boolean compareAndSet(V   expectedReference,
                                V   newReference,
                                int expectedStamp,
                                int newStamp) {
    Pair<V> current = pair;
    return
        expectedReference == current.reference &&
        expectedStamp == current.stamp &&
        ((newReference == current.reference &&
            newStamp == current.stamp) ||
            casPair(current, Pair.of(newReference, newStamp)));
}
1
2
3
4
5
6
7
8
9
10
11
12

我们知道,AtomicStampedReference内部保存了一个pair对象,该方法的逻辑:如果AtomicStampedReference内部pair的引用变量、时间戳与入参expectedReferenceexpectedStamp都一样,说明期间没有其它线程修改过AtomicStampedReference,可以进行修改。此时,会创建一个新的Pair对象(casPair方法,因为PairImmutable类)。但这里有段优化逻辑,就是如果newReference == current.reference && newStamp == current.stamp,说明用户修改的新值和AtomicStampedReference中目前持有的值完全一致,那么其实不需要修改,直接返回true即可。

# 六、AtomicMarkableReference

我们在讲ABA问题的时候,引入了AtomicStampedReference 可以给引用加上版本号,追踪引用的整个变化过程,如:A -> B -> C -> D - > A,通过AtomicStampedReference,我们可以知道,引用变量中途被更改了3次。

但是,有时候,我们并不关心引用变量更改了几次,只是单纯的关心是否更改过,所以就有了AtomicMarkableReference

public AtomicMarkableReference(V initialRef, boolean initialMark) {
    pair = Pair.of(initialRef, initialMark);
}

public boolean compareAndSet(V       expectedReference,
                                V       newReference,
                                boolean expectedMark,
                                boolean newMark) {
    Pair<V> current = pair;
    return
        expectedReference == current.reference &&
        expectedMark == current.mark &&
        ((newReference == current.reference &&
            newMark == current.mark) ||
            casPair(current, Pair.of(newReference, newMark)));
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

可以看到,AtomicMarkableReference的唯一区别就是不再用int标识引用,而是使用boolean变量——表示引用变量是否被更改过。

从语义上讲,AtomicMarkableReference对于那些不关心引用变化过程,只关心引用变量是否变化过的应用会更加友好。

(adsbygoogle = window.adsbygoogle || []).push({});