`

自旋锁、排队自旋锁、MCS锁、CLH锁(转)

 
阅读更多

自旋锁(Spin lock)

自旋锁是指当一个线程尝试获取某个锁时,如果该锁已被其他线程占用,就一直循环检测锁是否被释放,而不是进入线程挂起或睡眠状态。

自旋锁适用于锁保护的临界区很小的情况,临界区很小的话,锁占用的时间就很短。

简单的实现

import java.util.concurrent.atomic.AtomicReference;

public class SpinLock {
   private AtomicReference<Thread> owner = new AtomicReference<Thread>();

   public void lock() {
       Thread currentThread = Thread.currentThread();

              // 如果锁未被占用,则设置当前线程为锁的拥有者
       while (owner.compareAndSet(null, currentThread)) {
       }
   }

   public void unlock() {
       Thread currentThread = Thread.currentThread();

              // 只有锁的拥有者才能释放锁
       owner.compareAndSet(currentThread, null);
   }
}

SimpleSpinLock里有一个owner属性持有锁当前拥有者的线程的引用,如果该引用为null,则表示锁未被占用,不为null则被占用。

这里用AtomicReference是为了使用它的原子性的compareAndSet方法(CAS操作),解决了多线程并发操作导致数据不一致的问题,确保其他线程可以看到锁的真实状态。

缺点

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

Ticket Lock

Ticket Lock 是为了解决上面的公平性问题,类似于现实中银行柜台的排队叫号:锁拥有一个服务号,表示正在服务的线程,还有一个排队号;每个线程尝试获取锁之前先拿一个排队号,然后不断轮询锁的当前服务号是否是自己的排队号,如果是,则表示自己拥有了锁,不是则继续轮询。

当线程释放锁时,将服务号加1,这样下一个线程看到这个变化,就退出自旋。

简单的实现

import java.util.concurrent.atomic.AtomicInteger;

public class TicketLock {
   private AtomicInteger serviceNum = new AtomicInteger(); // 服务号
   private AtomicInteger ticketNum = new AtomicInteger(); // 排队号

   public int lock() {
         // 首先原子性地获得一个排队号
         int myTicketNum = ticketNum.getAndIncrement();

              // 只要当前服务号不是自己的就不断轮询
       while (serviceNum.get() != myTicketNum) {
       }

       return myTicketNum;
    }

    public void unlock(int myTicket) {
        // 只有当前线程拥有者才能释放锁
        int next = myTicket + 1;
        serviceNum.compareAndSet(myTicket, next);
    }
}

缺点

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

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

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

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

MCS锁

MCS Spinlock 是一种基于链表的可扩展、高性能、公平的自旋锁,申请线程只在本地变量上自旋,直接前驱负责通知其结束自旋,从而极大地减少了不必要的处理器缓存同步的次数,降低了总线和内存的开销。

import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;

public class MCSLock {
    public static class MCSNode {
        MCSNode next;
        boolean isLocked = true; // 默认是在等待锁
    }

    volatile MCSNode queue ;// 指向最后一个申请锁的MCSNode
    private static final AtomicReferenceFieldUpdater<MCSLock, MCSNode> UPDATER = AtomicReferenceFieldUpdater
                  . newUpdater(MCSLock.class, MCSNode. class, "queue" );

    public void lock(MCSNode currentThread) {
        MCSNode predecessor = UPDATER.getAndSet(this, currentThread);// step 1
        if (predecessor != null) {
            predecessor.next = currentThread;// step 2

            while (currentThread.isLocked ) {// step 3
            }
        }
    }

    public void unlock(MCSNode currentThread) {
        if ( UPDATER.get( this ) == currentThread) {// 锁拥有者进行释放锁才有意义
            if (currentThread.next == null) {// 检查是否有人排在自己后面
                if (UPDATER.compareAndSet(this, currentThread, null)) {// step 4
                    // compareAndSet返回true表示确实没有人排在自己后面
                    return;
                } else {
                    // 突然有人排在自己后面了,可能还不知道是谁,下面是等待后续者
                    // 这里之所以要忙等是因为:step 1执行完后,step 2可能还没执行完
                    while (currentThread.next == null) { // step 5
                    }
                }
            }

            currentThread.next.isLocked = false;
            currentThread.next = null;// for GC
        }
    }
}

CLH锁

CLH锁也是一种基于链表的可扩展、高性能、公平的自旋锁,申请线程只在本地变量上自旋,它不断轮询前驱的状态,如果发现前驱释放了锁就结束自旋。

import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;

public class CLHLock {
    public static class CLHNode {
        private boolean isLocked = true; // 默认是在等待锁
    }

    @SuppressWarnings("unused" )
    private volatile CLHNode tail ;
    private static final AtomicReferenceFieldUpdater<CLHLock, CLHNode> UPDATER = AtomicReferenceFieldUpdater
                  . newUpdater(CLHLock.class, CLHNode .class , "tail" );

    public void lock(CLHNode currentThread) {
        CLHNode preNode = UPDATER.getAndSet( this, currentThread);
        if(preNode != null) {//已有线程占用了锁,进入自旋
            while(preNode.isLocked ) {
            }
        }
    }

    public void unlock(CLHNode currentThread) {
        // 如果队列里只有当前线程,则释放对当前线程的引用(for GC)。
        if (!UPDATER .compareAndSet(this, currentThread, null)) {
            // 还有后续线程
            currentThread. isLocked = false ;// 改变状态,让后续线程结束自旋
        }
    }
}

CLH锁 与 MCS锁 的比较

下图是CLH锁和MCS锁队列图示:
CLH-MCS-SpinLock

差异:

  1. 从代码实现来看,CLH比MCS要简单得多。
  2. 从自旋的条件来看,CLH是在本地变量上自旋,MCS是自旋在其他对象的属性。
  3. 从链表队列来看,CLH的队列是隐式的,CLHNode并不实际持有下一个节点;MCS的队列是物理存在的。
  4. CLH锁释放时只需要改变自己的属性,MCS锁释放则需要改变后继节点的属性。

注意:这里实现的锁都是独占的,且不能重入的

分享到:
评论

相关推荐

    基于SMP的Linux内核自旋锁分析.pdf

    为了实现自旋锁,Linux内核使用了多种技术,包括Ticket Lock、MCS Lock和CLH Lock等。 Ticket Lock是一种简单的自旋锁机制,它使用一个 ticket 变量来记录当前获取自旋锁的处理器的ID。当一个处理器尝试获取自旋锁...

    一种Linux内核自旋锁死锁检测机制的设计与实现.pdf

    在 Linux 内核中,自旋锁的种类有很多,包括基本型、读写自旋、排队锁和 MCS 自旋锁等。每种自旋锁都有其特点和使用场景,需要根据不同的情况选择合适的自旋锁。 在设计自旋锁死锁检测机制时,需要考虑到自旋锁的...

    信号量、互斥体和自旋锁的区别

    ### 信号量、互斥体和自旋锁的区别详解 #### 一、基本概念与应用场景 **信号量**、**互斥体**和**自旋锁**是操作系统中三种常用的同步机制,主要用于解决多线程或多进程环境中资源的并发访问问题。这三种机制虽然...

    无锁编程之自旋锁的C++实现

    4. `5_queuelock.cpp`:队列锁(也称为自旋队列锁)是一种更复杂的自旋锁,它通过维护一个请求锁的线程队列来减少竞争。 5. `6_threadlocal.cpp`:可能涉及到线程局部存储,这种技术可以为每个线程提供独立的数据,...

    Windows驱动编程视频教程 提升IRQ与自旋锁

    在Windows驱动程序开发中,理解和掌握中断请求级别(IRQ)以及自旋锁是至关重要的。本视频教程将深入探讨这两个核心概念,旨在帮助开发者提升驱动程序的性能和稳定性。 首先,我们来了解一下中断请求级别(IRQ)。...

    Linux系统内核的同步机制-自旋锁

    自旋锁的设计理念在于,当一个线程尝试获取已被其他线程持有的自旋锁时,它不会进入睡眠状态,而是会不断地进行忙循环,检查锁的状态,直到锁被释放。这种行为被称为“自旋”,因此得名自旋锁。 自旋锁的特点在于其...

    自旋锁操作 spin_lock

    自旋锁的基本思想是当一个线程试图获取已被其他线程持有的锁时,它不会立即阻塞,而是不断地循环检查锁的状态,直到锁变为可用状态为止,这个过程称为“自旋”。在Linux内核中,自旋锁是实现内核级并发的重要工具。 ...

    信号量与自旋锁

    ### 信号量与自旋锁:并发控制的关键技术 在多任务操作系统中,尤其是在像Linux这样的高度并发系统中,管理共享资源的访问是一项至关重要的任务。为了确保数据的一致性和完整性,开发人员需要采取措施来避免竞态...

    SQL server 自旋锁争用专题

    SQL Server自旋锁争用是一个高级数据库管理问题,通常出现在高性能、高并发的系统中。自旋锁是操作系统中的一个同步机制,用于控制对共享资源的访问。在数据库系统中,自旋锁用于保护数据结构在并发访问时的完整性。...

    linux系统中基于自旋锁的进程调度的实现

    本文将深入探讨Linux系统中基于自旋锁的进程调度实现,以及自旋锁、共享内存和汇编语言在其中的作用。 自旋锁(Spinlock)是一种简单的互斥机制。当一个进程持有一个自旋锁时,其他试图获取该锁的进程将进入“自旋...

    Java并发篇乐观锁,悲观锁,自旋锁

    本文主要讨论了四种锁类型:乐观锁、悲观锁、自旋锁以及Java中的synchronized同步锁,并深入解析了synchronized锁的内部机制,包括其核心组件、实现方式以及锁的状态。 1. **乐观锁**:乐观锁假设在多线程环境下,...

    深入讲解我们说的CAS自旋锁到底是什么

    深入讲解CAS自旋锁的实现机理和原理 CAS(Compare and Swap)是实现自旋锁或乐观锁的核心操作,它的出现是为了解决原子操作的问题。在多线程环境下,原子操作是保证线程安全的重要手段。CAS操作的实现很简单,就是...

    自旋锁公平性的三种实现代码下载

    `code`文件可能会包含CLHLock的源代码,你可以研究其如何通过节点间的交互来实现自旋等待和锁的释放。 4. **MCSLock(Mellor-Crummey and Scott Lock)**:与CLHLock类似,也是一种链表型自旋锁。MCSLock使用单向...

    golang实现Redis分布式自旋锁+本地自旋锁

    在Golang中,实现分布式自旋锁和本地自旋锁是一种常见的并发控制策略,用于解决多线程或分布式系统中的资源竞争问题。自旋锁的基本思想是,当一个线程试图获取锁时,如果锁已被其他线程持有,它会不断地检查锁的状态...

    redis实现分布式锁,自旋式加锁,lua原子性解锁

    本文将深入探讨如何使用Redis实现分布式锁,以及如何利用自旋式加锁和Lua脚本实现原子性解锁。 首先,我们来理解分布式锁的基本概念。分布式锁是在多节点之间共享资源时,用于协调各个节点的访问控制机制。在分布式...

    linux下自旋锁程序源码.zip

    自旋锁的原理是,当一个线程试图获取一个已经被其他线程持有的锁时,它不会像普通互斥锁那样进入睡眠状态,而是会不断地循环检查锁的状态,直至锁变为可用状态。这种机制在处理器等待时间短且锁持有时间也很短的情况...

    并发控制之自旋锁.pdf

    自旋锁的设计理念是,在等待锁释放的过程中,持有锁的任务会不断地检查锁的状态,即“自旋”,直到锁变为可用状态。这种方式在锁的持有时间极短的情况下是非常高效的,因为减少了上下文切换的开销。然而,如果锁被长...

    zynq的linux驱动6-使用自旋锁实现竞争保护

    在这个场景下,“zynq的linux驱动6-使用自旋锁实现竞争保护”是一个关于如何在Zynq SoC的Linux驱动中使用自旋锁来避免数据竞争的重要教程。 自旋锁(Spinlock)是一种同步原语,用于在多线程环境下保护共享资源。在...

    用原子自旋读写锁代替互斥锁提高多线程访问公共资源效率

    在自旋锁的状态下,线程并不像互斥锁那样被挂起,而是持续检查锁的状态,一旦锁变为可用状态,线程就会立即获取锁并继续执行。由于自旋锁的线程不会进入睡眠状态,因此减少了上下文切换的开销,特别适用于锁的持有...

Global site tag (gtag.js) - Google Analytics