自旋锁

几种自旋锁的实现原理

自定义接口

 interface Lock {
        void lock();

        void unLock();
    }

1. SpinLock(自旋锁)

    static class SpinLock implements Lock {

        /**
         * currentHoldLockThread == null,无线程占用锁
         */
        private AtomicReference currentHoldLockThread =
                new AtomicReference<>();

        @Override
        public void lock() {
            Thread current = Thread.currentThread();

            /**
             * CAS 更新,当前值不是null 说明锁被占用,自旋
             */
            while (currentHoldLockThread.compareAndSet(null,current))
            {

            }
        }

        @Override
        public void unLock() {
            Thread current = Thread.currentThread();
            //只有持有锁的线程才能释放锁
            if (currentHoldLockThread.get() == Thread.currentThread()) {
                currentHoldLockThread.compareAndSet(current, null);
            }

        }
    } 

缺点:

  1. CAS操作需要硬件的配合
  2. 保证各个CPU的缓存(L1、L2、L3、跨CPU Socket、主存)的数据一致性,通讯开销很大,在多处理器系统上更严重
  3. 没法保证公平性,不保证等待进程/线程按照FIFO顺序获得锁

2.TicketLock(公平性spinLock)

    /**
     * 保证公平性的spinLock,实现类似银行叫号,服务号=被呼叫号 获得锁
     * != 则自旋
     * 票号
     * 排位号
     * 服务号
     */
    static class TicketLock implements Lock {

        private AtomicInteger serviceNum = new AtomicInteger(1);

        private AtomicInteger ticketNum = new AtomicInteger(0);

        private final static ThreadLocal ownNum = new ThreadLocal<>();

        @Override
        public void lock() {
            //获取自己的排队号
            int ownCalledNum = ticketNum.incrementAndGet();
            //保存在当前线程里
            ownNum.set(ownCalledNum);
            //排队号不等于服务号  自旋
            while (ownCalledNum != serviceNum.get()) {

            }
        }

        @Override
        public void unLock() {
            //服务号=自己的排队号才能释放锁
            //保证只有获得锁的线程才能释放锁
            serviceNum.compareAndSet(ownNum.get(),serviceNum.get()+1);
        }
    }

缺点

Ticket Lock 虽然解决了公平性的问题,但是多处理器系统上,每个进程/线程占用的处理器都在读写同一个变量serviceNum ,每次读写操作都必须在多个处理器缓存之间进行缓存同步,这会导致繁重的系统总线和内存的流量,大大降低系统整体的性能。

下面介绍的CLH锁和MCS锁都是为了解决这个问题的。

MCS 来自于其发明人名字的首字母: John Mellor-Crummey和Michael Scott。

CLH的发明人是:Craig,Landin and Hagersten。

3.CLH自旋锁

实现原理

线程检查自己的前驱结点是否被锁定,锁定则自旋,未锁定则执行
下面已三个线程在同一锁上竞争说明。

第一个线程locked,A前驱节点false,B尾节点true,unlock后B节点false

graph LR
A-->B

线程2,前驱节点B,true,尾节点true,现在在节点B上自旋,线程1释放锁后,B节点false,线程2开始执行

graph LR
B-->C

线程有两个本地变量,一个前驱一个尾节点。前驱节点判定线程是否自旋,tail节点关联下个线程的状态

graph LR
C-->D
CLH优点:

1)公平,FIFO,先来后到的顺序进入锁,在前驱自旋,隐式指针(指向下个节点)

2)而且没有竞争同一个变量,因为每个线程只要等待自己的前继释放就好了
3)空间复杂度低,如果有n个线程,L个锁,每个线程每次只获取一个锁,那么需要O(n+L),n个线程有n个node,L个锁有L个tail

  static class CLHLock implements Lock
    {
        static class CLHNode
        {
            private boolean isLocked = false;
        }

        private AtomicReference tail = new AtomicReference<>(new CLHNode());

        /**
         * 前一节点
         */
        private ThreadLocal predNode;

        /**
         * 当前节点
         */
        private ThreadLocal currentNode;

        public CLHLock()
        {
            currentNode = new ThreadLocal()
            {
                @Override
                protected CLHNode initialValue() {
                    return new CLHNode();
                }
            };

            predNode = new ThreadLocal()
            {
                @Override
                protected CLHNode initialValue() {
                    return null;
                }
            };
        }


        @Override
        public void lock() {
            //对于新线程 node = new CLHNode();
            CLHNode node = currentNode.get();
            //进行自旋
            node.isLocked = true;
            CLHNode pre = tail.getAndSet(node);
            predNode.set(pre);
            while (pre.isLocked)
            {
            }
        }

        @Override
        public void unLock() {
            CLHNode node = currentNode.get();
            node.isLocked=false; //currentNode.set(predNode.get());
        }
    }

4. MCS队列锁

在自己loclNode上自旋,持有锁的线程释放锁时把next指向的node并 设置false,线程开始运行

   static class MCSLock implements Lock
    {
        static class Node
        {
             volatile boolean isLocked=false;

             Node next;
        }

        private ThreadLocal local;

        private AtomicReference tail = new AtomicReference<>(null);

        public MCSLock()
        {
            local = new ThreadLocal()
            {
                @Override
                protected Node initialValue() {
                    return new Node();
                }
            };
        }
        @Override
        public void lock() {
            //获取当前线程的Node节点
            Node node = local.get();
            Node pre = tail.getAndSet(node);
            if(pre != null)
            {
                node.isLocked = true;
                pre.next = node;
                //在自己node上自旋
                while (node.isLocked)
                {

                }
            }

        }

        @Override
        public void unLock() {
            Node node = local.get();

            //node是最后一个节点
            if(node.next == null)
            {
                if(tail.compareAndSet(node,null))
                {
                    return;
                }

                while (node.next == null)
                {

                }
            }

            node.next.isLocked = false;
            node.next = null;
        }
    }

从上面的实现可以看出,MSC与CLH最大的不同并不是链表是显示还是隐式,而是线程自旋的规则不同,CLH是在前趋结点的locked域上自旋等待,而MSC是在自己的结点的locked域上自旋等待。

lock方法:
若要获得锁,线程会把自己的结点放到队列的尾部,如果队列中开始有结点,就将前一个结点的next结点指向当前结点;

然后就在自己的locked域上自旋等待,直到它的前趋结点把自己的locked设置为false为止。

unlock方法:

若要释放锁,先检查自己的next域是否为null,如果为null,要么当前结点是尾结点,要么还有其他线程正在争用锁。不管是哪种情况都可以采用compareAndSet(q,null)来判断,其中q为当前结点,如果调用成功,则没有其他线程在争用锁,于是将tail设置为null返回;如果调用失败,说明另一个比较慢的线程正在试图获得锁,于是自旋等待它结束。在以上任一种情况,一旦出现有后继结点就将后续结点的locked域设置为false,然后返回。

你可能感兴趣的:(Java并发编程)