Java 实现CLH锁/MCS锁

136 阅读4分钟

CLH锁/MCS锁

一、引文

1.1 SMP(Symmetric Multi-Processor) 对称多处理器结构,指服务器中多个CPU对称工作,每个CPU访问内存地址所需要时间相同。其主要特征是共享,包含对CPU,内存,I/O等进行共享。 SMP能够保证内存一直想,但这些共享的资源很可能成为性能瓶颈,随着CPU数量的增加,每个CPU都要访问相同的内存资源,可能导致内存访问冲突,可能会导致CPU资源的浪费。常用的PC机就属于这种。 1.2 NUMA(Non-Uniform Memory Access) 非一致存储访问,将CPU分为CPU模块,每个CPU模块由多个CPU组成,并且具有独立的本地内存、I/O槽口等,模块之间可以通过互联模块相互访问,访问本地内存的速度将远远敢于访问远地内存(系统内其它节点的内存)的速度,这也是非一致存储访问的由来。NUMA较好地解决SMP的扩展问题,当CPU数量增加时,因为访问远地内存的延时远远超过本地内存,系统性能无法线性增加。

二、CLH

CLH(Craig, Landin, and Hagersten locks):是一个自旋锁,能确保无饥饿性,提供先来先服务的公平性。 CLH锁也是一种基于链表的可扩展、公平的自旋锁,神器线程只在本地变量上自旋,它不断轮询前驱的状态,如果发现前驱释放了锁就结束自旋。

当一个线程需要获取锁时:

  1. 创建一个QNode, 将其中的locked设置true表示需要获取锁。

  2. 线程对tail 用AtomicReference为确保并发线程安全调用getAndSet方法,使自己成为队列的尾部,同时获取一个指向其前驱节点的引用preNode

  3. 该线程就在前驱节点的locked字段上自旋,直到前驱节点释放锁

  4. 当一个线程需要释放锁时,将当前节点locked域设置为false,同时回收前驱节点

    三、例子:
    package com.lvyuanj.workspace.locks;
    ​
    import lombok.Data;
    ​
    import java.util.concurrent.TimeUnit;
    import java.util.concurrent.atomic.AtomicReference;
    import java.util.concurrent.locks.Condition;
    import java.util.concurrent.locks.Lock;
    ​
    /**
     * @author yuanjlv
     * @date 2022年03月06日 17:35
     * @see 类描述:
     */
    @Data
    public class CLHLock implements Lock {
    ​
        private String name;
        // 指向当前节点
        private ThreadLocal<Node> curNodeLocal = new ThreadLocal<>();
    ​
        private AtomicReference<Node> tail = new AtomicReference<>(null);
    ​
        public CLHLock(){
             tail.getAndSet(Node.EMPTY);
        }
    ​
        public CLHLock(String name){
            this.name = name;
            tail.getAndSet(Node.EMPTY);
        }
    ​
        @Override
        public void lock() {
            Node curNode = new Node(true, null);
            
            Node preNode = null;              
            // CAS 自旋: 将当前节点插入到队列的尾部          
            // 方法一:                           
            preNode =tail.get();              
            while (!tail.compareAndSet(preNode
                preNode = tail.get();         
            }                                 
            // 方法二: 此方法内部就自旋设值                
            preNode = tail.getAndSet(curNode);
            
            // 设置当前节点的前驱节点
            curNode.setPreNode(preNode);
            // 监听前驱节点的locked变量,直到其值为false
            // 若前驱节点的locked状态为true,则表示前一线程还在抢占或者占有锁
            while (curNode.getPreNode().locked){
                // 让出CPU时间片,提高性能
                Thread.yield();
            }
            // 能执行到这里,说明当前线程已经获取到锁了
            // 设置在线程本地变量中,用于释放锁
            curNodeLocal.set(curNode);
        }
    ​
        @Override
        public void lockInterruptibly() throws InterruptedException {
    ​
        }
    ​
        @Override
        public boolean tryLock() {
            return false;
        }
    ​
        @Override
        public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
            return false;
        }
    ​
        @Override
        public void unlock() {
            Node curNode = curNodeLocal.get();
            curNode.setPreNode(null);
            curNodeLocal.set(null);
            curNode.setLocked(false);
        }
    ​
        @Override
        public Condition newCondition() {
            return null;
        }
    ​
        @Data
        static class Node{
    ​
            public static final Node EMPTY = new Node(false, null);
    ​
            private boolean locked; // 锁状态标识:true-当前线程正在抢锁或者已经占有锁,false-当前线程已经释放锁,下一个线程可以占有锁了
    ​
            private Node preNode; // 节点
    ​
            public Node(boolean locked, Node prevNode){
                this.locked = locked;
                this.preNode = prevNode;
            }
    ​
    ​
        }
    }
    ​
    
四、CLH分析

CLH队列锁的优点是空间复杂度低(如果有n个线程,L个锁,每个线程每次只获取一个锁,那么需要的存储空间O(L+n),n个线程有n个Node节点,L个锁有L个tail),CLH的一种变体了Java并发框架中(AbstractQueueSynchronnizer.Node)。CLHzaiSMP体系结构下将大打折扣,一种解决NUMA系统结构的思路是MCS队列锁

五、MCS

MSC与CLH最大不同的并不是链表是显示还是隐式,而是线程自旋的规则不同:CLH是前驱节点的locked上自旋等待,而MCS是在自己的节点的locked上自旋等待。 正因为如此,它解决了CLH在NUMA系统架构中获取locked的状态内存过远的问题。

MCS队列锁的具体实现如下:

  1. 队列初始化时没有节点,tail=null
  2. 线程A想要获取锁,于是将自己置于队尾,由于它是第一个节点,它的locked域为false
  3. 线程B和C相继加入队列,a->next=b, b->next=c 。且B和C现在没有获取锁,处于等待状态,所以它们的locked域为true,尾指针指向线程C对应的节点
  4. 线程A释放锁后,顺着它的next指针找到了线程B,并把B的locked域设置为false。 这一动作会触发线程B获取锁
六、代码实现
package com.lvyuanj.workspace.locks;
​
import lombok.Data;
​
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
​
/**
 * @author yuanjlv
 * @date 2022年03月06日 21:39
 * @see 类描述:
 */
public class MCSLock implements Lock {
​
    private AtomicReference<Node> tail = new AtomicReference<>(null);
​
    // 每个线程都初始化一个节点
    private ThreadLocal<Node> curThreadLocal = ThreadLocal.withInitial(()-> new Node(false,null));
​
    public MCSLock(){
​
    }
​
    @Override
    public void lock() {
        Node curNode = curThreadLocal.get();
        Node preNode = tail.getAndSet(curNode);
        if (Objects.nonNull(preNode)) {
            curNode.locked = true;
            preNode.next = curNode;
            // 等当前节点获取锁后不自旋,否则当前节点自旋
            while (curNode.locked);
        }
    }
​
    @Override
    public void lockInterruptibly() throws InterruptedException {
​
    }
​
    @Override
    public boolean tryLock() {
        return false;
    }
​
    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return false;
    }
​
    @Override
    public void unlock() {
        Node curNode = curThreadLocal.get();
        if (curNode.next == null) {
            if (tail.compareAndSet(curNode, null)) {
                return;
            }
            // 如果当前节点是最后一个节点则自旋等待下一节点出现则不自旋
            while (curNode.next == null);
        }
        curNode.next.locked = false;
        curNode.next = null;
    }
​
    @Override
    public Condition newCondition() {
        return null;
    }
​
    @Data
    static class Node {
​
        private static final Node EMPTY = new Node(false, null);
​
        private volatile boolean locked;
​
        private volatile Node next;
​
        public Node(boolean locked, Node node) {
            this.locked = locked;
            this.next = node;
        }
    }
}
互斥锁
package com.lvyuanj.workspace.locks;
​
import cn.hutool.core.util.NumberUtil;
import com.fasterxml.jackson.databind.ser.std.NumberSerializers;
import org.springframework.ui.context.Theme;
​
import java.util.Objects;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.LockSupport;
​
/**
 * @author yuanjlv
 * @date 2022年03月06日 20:32
 * @see 类描述:
 */
public class MutexLock implements Lock{
​
    private static Thread owner;
​
    private final AtomicInteger state = new AtomicInteger(0);
​
    private final ConcurrentLinkedDeque<Thread> waitersQueue = new ConcurrentLinkedDeque<Thread>();
​
​
    @Override
    public void lock() {
        Thread curThread = Thread.currentThread();
        if(Objects.nonNull(curThread) && (owner == curThread)){
            return;
        }
​
        try{
            waitersQueue.add(curThread);
            while (true){
                boolean succeed = state.compareAndSet(0, 1);
                if(succeed){
                    owner = curThread;
                    break;
                }
                LockSupport.park();
            }
        }finally {
            waitersQueue.remove(curThread);
        }
​
    }
​
    @Override
    public void lockInterruptibly() throws InterruptedException {
​
    }
​
    @Override
    public boolean tryLock() {
        return false;
    }
​
    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return false;
    }
​
    @Override
    public void unlock() {
        if(Objects.nonNull(owner) && owner != Thread.currentThread()){
            System.out.println("Wrong state, this thread don't own this lock. owner:"+ owner.getName());
        }
        // 自旋当前线程获取锁的状态
        while (true){
            if(state.compareAndSet(1, 0)){
                break;
            }
        }
​
        owner = null;
        if(!waitersQueue.isEmpty()){
            for (Thread thread : waitersQueue) {
                LockSupport.unpark(thread);
            }
        }
​
    }
​
    @Override
    public Condition newCondition() {
        return null;
    }
}
​

测试:

package com.lvyuanj.workspace.locks;
​
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;
​
/**
 * @author yuanjlv
 * @date 2022年03月06日 20:53
 * @see 类描述:
 */
public class Test {
​
    public static void main(String[] args) throws InterruptedException {
        CLHLock clhLock = new CLHLock("clhLock-01");
        MCSLock mcsLock = new MCSLock();
​
        ExecutorService executorService = Executors.newFixedThreadPool(10);
​
        List<Future> futures = new CopyOnWriteArrayList<>();
​
        for (int i = 0; i < 10; i++) {
            Future<?> future = executorService.submit(() -> {
                try {
                    System.out.println(Thread.currentThread().getName() + " --> start...");
                    //clhLock.lock();
                    mcsLock.lock();
                    System.out.println(Thread.currentThread().getName() + " --> execute");
                    Thread.sleep(ThreadLocalRandom.current().nextInt(5)*1000);
                    System.out.println(Thread.currentThread().getName() + " --> end...");
                    //clhLock.unlock();
                    mcsLock.unlock();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
            futures.add(future);
        }
​
        while (true){
            if(futures.size()==0){
                System.out.println("线程任务全部完成......");
                break;
            }
            for (Future future : futures) {
                if(future.isDone() &&  !future.isCancelled()){
                    futures.remove(future);
                }
            }
​
        }
    }
}
​