手写锁(一)-三种cpu原子指令-简单锁

753 阅读3分钟

介绍

本文是锁相关的第一篇文章,本系列文章计划从cpu指令开始,一步一步实现锁,把简单锁,可重入锁,公平锁,读写锁都用介绍的cpu指令实现一遍

三种cpu指令

Test-and-set(链接

在计算机科学中,检查并设置(test-and-set-lock,TSL)是一种不可中断的原子运算。TSL对某个存储器位置写入1(set)并返回其旧值。

使用Test-and-set实现自旋锁

function lock(boolean *lock) {
    while (test_and_set (lock) == 1);
}

当旧值为0视,程序可以得到锁,否则将会一直尝试将1写入,直到其他线程释放锁(把值改为0)

Fetch-and-add(链接

fetch-and-add是CPU指令(FAA),对内存位置执行增加一个数量的原子操作。具体内容为:令x 变为x + a,其中x是个内存位置,a是个值

可以用fetch-and-add实现简单公平锁

volatile int lockCount;
volatile int unLockCount;

public void lock(){
    int cur=fetchAndAdd(lockCount,1);
    while(cur!=unLockCount);
}

public void unLock(){
    fetchAndAdd(unLockCount,1);
}

每个线程调用lock函数是,会根据顺序生成一些列连续的序号,如这里有三个线程A,B,C,分别调用lock函数,A将获得序号0,B为1,C为2 此时unLockCount也为0,所以线程A不会进入while循环,B,C在while循环中一直运行,无法在函数lock中返回,而A返回后得到锁。 A调用unLock锁后,unLock变为1,此时B获得锁,B调用unLock后,C获得锁 因为获得锁的顺序是严格按照调用lock的顺序,所以是公平锁

compare and swap(链接

比较并交换(compare and swap, CAS),是原子操作的一种,可用于在多线程编程中实现不被打断的数据交换操作,从而避免多线程同时改写某一数据时由于执行顺序不确定性以及中断的不可预知性产生的数据不一致问题。 该操作通过将内存中的值与指定数据进行比较,当数值一样时将内存中的数据替换为新的值。

后续文章的锁实现均是通过cas实现。cas实现简单自旋锁

public class LockSpin {
    AtomicInteger atomicInteger=new AtomicInteger();
    
    public void lock(){
        while (!atomicInteger.compareAndSet(0,1));
    }
    
    public void unLock(){
        atomicInteger.set(0);
    }
}

以上代码可以看到通过cas很容易就实现了简单锁,但是我们发现这个简单锁有一个致命的缺点,就是自己可以和自己死锁。如下代码:

static LockSpin lockSpin=new LockSpin();
    public static void main(String[] args) {
        lockSpin.lock();
        methodA();
        lockSpin.unLock();
    }
    
    public static void methodA(){
        lockSpin.lock();
        //do something
        lockSpin.unLock();
    }

我们先获得锁后,执行函数 methodA,但在methodA中,再一次调用了lockSpin.lock(),发现此时lockSpin中atomicInteger值已经是1了,所以就会死锁在methodA中的第一行lockSpin.lock()。自己会和自己死锁。如何解决这个问题呢?这就需要我们实现可重入锁,可重入锁将在下篇文章介绍