从 CAS 到无锁编程

从 CAS 到无锁编程

先尝试实现一个 CAS 自旋锁,我们直接看 ChatGPT 给出的结果

public class SpinLock {  
    // 使用一个原子变量来表示锁的状态  
    private volatile boolean isLocked = false;  
  
  
    // 获取锁的方法  
    public void lock() {  
        while (true) {  
            // 如果当前没有被锁住,则尝试将锁设置为已锁定  
            if (!isLocked) {  
                // 通过 CAS 操作设置锁状态  
                if (!compareAndSet(false, true)) {  
                    continue;  
                }  
                break;  
            }  
        }  
    }  
  
    // 释放锁的方法  
    public void unlock() {  
        isLocked = false; // 释放锁  
    }  
  
    // CAS 比较并设置方法,模拟原子操作  
    private boolean compareAndSet(boolean expectedValue, boolean newValue) {  
        // 这个地方依然是有问题的  
        // 从 isLocked == expectedValue 这一步到 isLocked = newValue 一步是没有加锁的,也就是说,  
        // 执行完 isLocked == expectedValue 之后,执行 isLocked = newValue 之前,别的线程可能会修改 isLocked 的值  
        if (isLocked == expectedValue) {  
            isLocked = newValue;  
            return true;  
        }  
        return false;  
    }  
  
    public static void main(String[] args) throws InterruptedException {  
        SpinLock spinLock = new SpinLock();  
  
        // 模拟多个线程竞争锁  
        Runnable task = () -> {  
            spinLock.lock(); // 获取锁  
            try {  
                System.out.println(Thread.currentThread().getName() + " is working");  
                Thread.sleep(1000); // 模拟一些工作  
            } catch (InterruptedException e) {  
                Thread.currentThread().interrupt();  
            } finally {  
                spinLock.unlock(); // 释放锁  
                System.out.println(Thread.currentThread().getName() + " finished work");  
            }  
        };  
  
        // 创建多个线程  
        Thread thread1 = new Thread(task, "Thread-1");  
        Thread thread2 = new Thread(task, "Thread-2");  
        thread1.start();  
        thread2.start();  
  
        thread1.join();  
        thread2.join();  
    }  
}

执行结果

Thread-1 is working
Thread-2 is working
Thread-1 finished work
Thread-2 finished work

从结果看,确实实现了锁的效果,但是实际上这个代码是有问题的,问题在于 compareAndSet 方法,这个方法中从 isLocked == expectedValue 这一步到 isLocked = newValue 一步是没有加锁的,即并不是原子性的,也就是说,执行完 isLocked == expectedValue 之后,执行 isLocked = newValue 之前,别的线程可能会修改 isLocked 的值。

上面这个例子告诉我们,不能盲目相信 AI,它给出的代码可能是错的。

我们简单验证以下,依托于上面的代码,写一个支持原子累加的 Integer 类:

public class MyAtomInt {  
  
    private volatile int innerVal = 0;  
  
    public MyAtomInt(int initVal) {  
        this.innerVal = initVal;  
    }  
  
    private boolean compareAndSwap(int expected, int newVal) {  
        // 这一段无法保证原子操作,会有线程问题  
        if (innerVal == expected) {  
            innerVal = newVal;  
            return true;  
        }  
        return false;  
    }  
  
  
    public void add(int add) {  
        boolean success = false;  
        while (!success) {  
            int nowValue = getIntVal();  
            success = compareAndSwap(nowValue, nowValue + add);  
        }  
    }  
  
  
    // 获取当前值的办法  
    public int getIntVal() {  
        return innerVal;  
    }  
  
  
    public static void main(String[] args) {  
        MyAtomInt atomInt = new MyAtomInt(10);  
        Random random = new Random();  
        for (int i = 0; i < 10; i++) {  
            new Thread(() -> {  
                try {  
                    Thread.sleep(random.nextInt(10) * 100);  
                } catch (InterruptedException e) {  
                    // throw new RuntimeException(e);  
                }  
                atomInt.add(1);  
                System.out.println(atomInt.getIntVal());  
            }).start();  
        }  
    }  
}

执行结果是

11
12
14
13
15
16
17
17
18
19

17 出现了两次,也就是说出现了上面的代码线程不安全。
那我们如何保证 isLocked == expectedValueisLocked = newValue 的原子性呢?加锁可以实现,但是枷锁的话就变成了互斥锁了,这不是我们想要的,我们想要的是无锁的设计。
这个时候就只能使用 Unsafe 类了,通过其提供的 compareAndSwapXXX 方法,可直接去内存中对比、修改值,当然这也是危险操作,因此其名字就叫 Unsafe。

public class MyAtomIntFinal {  
  
    // setup to use Unsafe.compareAndSwapInt for updates  
    // 直接调用 Unsafe.getUnsafe() 会报错,只能在静态代码块中通过反射获取  
    private static Unsafe unsafe = null;  
    private static final long valueOffset;  
  
    static {  
        try {  
            //  
            Field field = Unsafe.class.getDeclaredField("theUnsafe");  
            field.setAccessible(true); // 使其可以访问  
            unsafe = (Unsafe) field.get(null);  
            valueOffset = unsafe.objectFieldOffset  
                    (MyAtomIntFinal.class.getDeclaredField("innerVal"));  
        } catch (Exception ex) {  
            throw new Error(ex);  
        }  
    }  
  
    private volatile int innerVal = 0;  
  
    public MyAtomIntFinal(int initVal) {  
        this.innerVal = initVal;  
    }  
  
    private boolean compareAndSwap(int expected, int newVal) {  
        return unsafe.compareAndSwapInt(this, valueOffset, expected, newVal);  
    }  
  
    // 原子增加方法  
    public void add(int add) {  
        boolean success = false;  
        while (!success) {  
            int nowValue = getIntVal();  
            success = compareAndSwap(nowValue, nowValue + add);  
        }  
    }  
  
    // 获取当前值的办法  
    public int getIntVal() {  
        return innerVal;  
    }  
  
  
    public static void main(String[] args) {  
        MyAtomIntFinal atomInt = new MyAtomIntFinal(10);  
        Random random = new Random();  
        for (int i = 0; i < 10; i++) {  
            try {  
                Thread.sleep(random.nextInt(10) * 100);  
            } catch (InterruptedException e) {  
                // throw new RuntimeException(e);  
            }  
            new Thread(() -> {  
                atomInt.add(1);  
                System.out.println(atomInt.getIntVal());  
            }).start();  
        }  
    }  
}

执行结果

11
12
13
14
15
16
17
18
19
20

此时我们再去看 AtomicInteger,会发现跟我们上面的写法以及差不多了

Unsafe

获取 Unsafe

有一点需要注意 Unsafe 类因为其直接操作内存的危险操作,因此在使用上故意设置了门槛,以防止滥用,因此,我们是无法直接初始化 Unsafe 类的,我们需要通过反射的方式才能获取 Unsafe 实例。
从 Java 9 开始,Unsafe 类的使用被认为是危险的,而且不推荐直接使用。Java 9 引入了 VarHandle 作为 Unsafe 的替代,它提供了更安全、更灵活的方式来进行低级的内存操作和原子操作。如果你使用的是 Java 9 或更高版本,应该考虑使用 VarHandle 来替代 Unsafe

import java.lang.invoke.*;

public class VarHandleExample {
    public static void main(String[] args) throws Exception {
        VarHandle handle = MethodHandles.lookup().findVarHandle(VarHandleExample.class, "value", int.class);
        VarHandleExample example = new VarHandleExample();

        // 设置字段值
        handle.set(example, 42);
        System.out.println("Value: " + example.value);
    }

    private volatile int value = 0;
}

了解 Unsafe

参考美团技术团队写的文章,写的非常的好:Java魔法类:Unsafe应用解析
如果链接无效,请看 转载:Java魔法类:Unsafe应用解析