精华内容
下载资源
问答
  • 什么是自旋锁

    2020-05-28 12:10:30
    多线程中,对共享资源进行访问,为了防止并发引起的相关问题,通常都引入的机制来处理并发问题。 获取到资源的线程A对这个资源加锁,其他线程比如B要访问这个资源首先要获得,而此时A持有这个资源的,只有...

    多线程中,对共享资源进行访问,为了防止并发引起的相关问题,通常都是引入锁的机制来处理并发问题。

    获取到资源的线程A对这个资源加锁,其他线程比如B要访问这个资源首先要获得锁,而此时A持有这个资源的锁,只有等待线程A逻辑执行完,释放锁,这个时候B才能获取到资源的锁进而获取到该资源。

    这个过程中,A一直持有着资源的锁,那么没有获取到锁的其他线程比如B怎么办?通常就会有两种方式:

    1. 一种是没有获得锁的进程就直接进入阻塞(BLOCKING),这种就是互斥锁

    2. 另外一种就是没有获得锁的进程,不进入阻塞,而是一直循环着,看是否能够等到A释放了资源的锁。

    上述的两种方式,学术上,就有几种不同的定义方式,大学的时候 学习的是C++, 《C++ 11》中就有这样的描述:

    自旋锁(spin lock)是一种非阻塞锁,也就是说,如果某线程需要获取锁,但该锁已经被其他线程占用时,该线程不会被挂起,而是在不断的消耗CPU的时间,不停的试图获取锁。

    互斥量(mutex)是阻塞锁,当某线程无法获取锁时,该线程会被直接挂起,该线程不再消耗CPU时间,当其他线程释放锁后,操作系统会激活那个被挂起的线程,让其投入运行。

    而《linux内核设计与实现》经常提到两种态,一种是内核态,一种是用户态,对于自旋锁来说,自旋锁使线程处于用户态,而互斥锁需要重新分配,进入到内核态。这里大家对内核态和用户态有个初步的认知就行了,用户态比较轻,内核态比较重。用户态和内核态这个也是linux中必备的知识基础,借鉴这个,可以进行很多程序设计语言API上的优化,就比如说javaio的部分,操作io的时候,先是要从用户态,进入内核态,再用内核态去操作输入输出设备的抽象,这里减少用户态到内核态的转换就是新io的一部分优化,后面再聊。

    wiki中的定义如下:

    自旋锁是计算机科学用于多线程同步的一种锁,线程反复检查锁变量是否可用。由于线程在这一过程中保持执行,因此是一种忙等待。

    自旋锁避免了进程上下文的调度开销,因此对于线程只会阻塞很短时间的场合是有效的。因此操作系统的实现在很多地方往往用自旋锁。Windows操作系统提供的轻型读写锁(SRW Lock)内部就用了自旋锁。显然,单核CPU不适于使用自旋锁,这里的单核CPU指的是单核单线程的CPU,因为,在同一时间只有一个线程是处在运行状态,假设运行线程A发现无法获取锁,只能等待解锁,但因为A自身不挂起,所以那个持有锁的线程B没有办法进入运行状态,只能等到操作系统分给A的时间片用完,才能有机会被调度。这种情况下使用自旋锁的代价很高。(红字部分是我给wiki编辑的词条,单核CPU不适合自旋锁,这个也只是针对单核单线程的情况,现在的技术基本单核都是支持多线程的)

    为什么要使用自旋锁
    互斥锁有一个缺点,他的执行流程是这样的 托管代码  - 用户态代码 - 内核态代码、上下文切换开销与损耗,假如获取到资源锁的线程A立马处理完逻辑释放掉资源锁,如果是采取互斥的方式,那么线程B从没有获取锁到获取锁这个过程中,就要用户态和内核态调度、上下文切换的开销和损耗。所以就有了自旋锁的模式,让线程B就在用户态循环等着,减少消耗。

    自旋锁比较适用于锁使用者保持锁时间比较短的情况,这种情况下自旋锁的效率要远高于互斥锁。

    自旋锁可能潜在的问题
    过多占用CPU的资源,如果锁持有者线程A一直长时间的持有锁处理自己的逻辑,那么这个线程B就会一直循环等待过度占用cpu资源
    递归使用可能会造成死锁,不过这种场景一般写不出来。

     

    总结:自旋的意思就是while()循环这样的等待,这样是会一直消耗cpu的,不会造成任务切换,而且只有多核才能用,单核不是真正的并行,所以不挂起是不行的。

    展开全文
  • 文章目录什么是自旋锁参考链接自旋锁和互斥锁的区别参考链接 什么是自旋锁 多线程对共享资源访问, 为防止并发引起的相关问题, 常引入锁的机制来处理并发问题。 \newline \newline 获取到资源的...

    什么是自旋锁

    • 多线程对共享资源访问,
      • 为防止并发引起的相关问题,
      • 常引入锁的机制来处理并发问题。

    \newline
    \newline

    • 获取到资源的线程A对这个资源加锁,
    • 其他线程如B要访问这个资源首先要获得锁,
    • 而此时A持有这个资源的锁,只有等待线程A逻辑执行完,释放锁,
    • 这个时候B才能获取到资源的锁进而获取到该资源。
    • 这个过程中,A一直持有着资源的锁,那么没有获取到锁的其他线程比如B怎么办?通常就会有两种方式:
      1. 没有获得锁的进程就直接进入阻塞(BLOCKING),这种就是互斥锁
      1. 没获得锁的进程,不进入阻塞,而一直循环着,看是否能够等到A释放了资源的锁。

    \newline
    \newline

    • 《C++ 11》中就有这样的描述:
    • spin lock是非阻塞锁,
      • 如果某线程需要获取锁,但该锁已经被其他线程占用时,
      • 该线程不会被挂起,而是在不断的消耗CPU的时间,不停的试图获取锁。
    • 互斥量(mutex)是阻塞锁,
      • 当某线程无法获取锁时,
      • 该线程会被直接挂起,
      • 该线程不再消耗CPU时间,
      • 当其他线程释放锁后,操作系统会激活那个被挂起的线程,让其运行。

    \newline
    \newline

    • 《linux内核设计与实现》内核态,用户态,
    • 对自旋锁来说,自旋锁使线程处于用户态,
    • 而互斥锁需要重新分配,进入到内核态。
    • 用户态比较轻,内核态比较重。
    • 用户态和内核态这个也是linux中必备的知识基础,
    • 借鉴这个,可进行很多程序设计语言API上的优化,
    • 比如说javaio的部分,操作io的时候,先从用户态,进入内核态,
    • 再用内核态去操作输入输出设备的抽象,
    • 这里减少用户态到内核态的转换就是新io的一部分优化,后面聊

    \newline
    \newline

    • wiki中的定义:
    • 自旋锁是计算机科学
      • 用于多线程同步的一种锁,
      • 线程反复检查锁变量是否可用。
      • 由于线程在这一过程中保持执行,
      • 因此是一种忙等待。

    \newline
    \newline

    • 自旋锁避免了进程上下文的调度开销,因此对于线程只会阻塞很短时间的场合是有效的。
    • 因此OS的实现在很多地方用自旋锁。
    • Windows提供的轻型读写锁(SRW Lock)内部就用了自旋锁。
    • 单核CPU不适用自旋锁,这里单核指的是单核单线程的CPU
    • 因为,在同一时间只有一个线程是处在运行状态,假设运行线程A发现无法获取锁,只能等待解锁,但因为A自身不挂起,所以那个持有锁的线程B没有办法进入运行态,只能等到操作系统分给A的时间片用完,才有机会被调度。
    • 这种情况下使用自旋锁的代价很高。
    • (单核CPU不适合自旋锁,这个也只是针对单核单线程的情况,现在的技术基本单核都是支持多线程的)

    \newline
    \newline

    • 为什么要用自旋锁
    • 互斥锁有个缺点,
    • 他的执行流程是这样的 托管代码 - 用户态代码 - 内核态代码、上下文切换开销与损耗,
    • 若获取到资源锁的线程A立马处理完逻辑释放掉资源锁,
    • 如果是采取互斥的方式,则线程B从没有获取锁到获取锁这个过程中,就要用户态和内核态调度、上下文切换的开销和损耗。
    • 所以就有了自旋锁的模式,让线程B就在用户态循环等着,减少消耗。

    \newline
    \newline

    • 自旋锁适用于锁使用者保持锁时间比较短
    • 这种情况下自旋锁的效率要远高于互斥锁。
      \newline
      \newline
    • 自旋锁潜在的问题
    • 过多占用CPU的资源,
    • 如果锁持有者线程A一直长时间的持有锁处理自己的逻辑,
    • 那么线程B就会一直循环等待过度占用cpu资源
    • 递归使用可能会造成死锁,不过这种场景一般写不出来

    \newline
    \newline

    • CAS
    • 不写术语定义了,
    • 简单的理解就是这个CAS是由操作系统定义的,
    • 由若干指令组成的,这个操作具有原子性,这些指令如果执行,就会全部执行完,不会被中断。
    • CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。
    • 当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。

    \newline
    \newline

    • CAS的问题
    • 经典的CAS的ABA问题,
    • 上面提到了CAS操作的时候,要检测值有没有变化,
    • 如果一个值原来是A,后来变成了B, 后来又变成了A,
    • CAS会认为没有发生变化。
    • 解决方案:
    • 加版本号 1A - 2B - 3A
    • jdk1.5提供了AtomicStampedReference来解决这个问题

    \newline
    \newline

    • 只能保证一个共享变量的原子操作
    • CAS通常是对一个变量来进行原子操作的,所以如果对多个变量进行原子操作就会有问题了。
    • 解决方案
    • 简单粗暴,加锁,反而加入了复杂性,最low的方式
    • 跟上面的加版本号的道理一样,就是将多个变量拼成一个变量(可以拼成一个字符串)
    • jdk1.5 提供了AtomicStampedReference,这个reference 就是个对象引用,把多个变量放在这个对象里即可

    \newline
    \newline

    • JAVA CAS封装
    • sun.misc.Unsafe是JDK里面的一个内部类,这个类当中有三个CAS的操作

    \newline
    \newline

    • JAVA自旋锁应用
    • Jdk1.5后,提供java.util.concurrent.atomic包,里面提供一组原子类。
    • 基本上就是当前获取锁的线程,执行更新的方法,其他线程自旋等待,
    • 比如atomicInteger类中的getAndAdd方法内部实际上使用的就是Unsafe的方法。
      /**
    • Atomically adds the given value to the current value.
    • @param delta the value to add
    • @return the previous value
      */
      public final int getAndAdd(int delta) {
      return unsafe.getAndAddInt(this, valueOffset, delta);
      }

    \newline
    \newline

    • java中的syncronized,在1.5中有大优化,加入了偏隙锁也叫偏向锁,
    • 实现方式:在对象头markword中打上线程的信息,这样资源上的锁的获取就偏向了这个线程,
    • 后面,会涉及一系列的锁升级的问题,
    • 间隙锁 - 轻量锁 - 重量级锁 ,锁升级后面单独抽出来写一篇,这个轻量锁实际上就是使用的也是自旋锁的实现方式。

    参考链接

    自旋锁和互斥锁的区别

    • 自旋锁是一种互斥锁的实现方式,一般的互斥锁会在等待期间放弃cpu,
    • spinlock是不断循环并测试锁的状态,这样就一直占着cpu。

    \newline
    \newline

    • 互斥锁:用于保护临界区,确保同一时间只有一个线程访问数据。
    • 对共享资源的访问,先对互斥量加锁,如果互斥量已经上锁,调用线程会阻塞,直到互斥量被解锁。
    • 在完成了对共享资源的访问后,要对互斥量进行解锁。

    \newline
    \newline

    • 临界区:每个进程中访问临界资源的那段程序称为临界区,每次只允许一个进程进入临界区,进入后不允许其他进程进入。
    • 自旋锁:与互斥量类似,它不是通过休眠使进程阻塞,而是在获取锁之前一直处于忙等(自旋)阻塞状态。
    • 用在以下情况:
      • 锁持有的时间短,且线程不希望在重新调度上花太多的成本。
      • “原地打转”。

    \newline
    \newline

    • 自旋锁与互斥锁的区别:
      • 线程在申请自旋锁时,
      • 线程不被挂起,而是处于忙等的状态。

    \newline
    \newline

    • 信号量:是个计数器,来控制多个进程对共享资源的访问。
    • 常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。
    • 主要作为进程间以及同一进程内不同线程之间的同步手段。

    参考链接

    展开全文
  • 什么是自旋锁?自旋锁(spinlock):是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。获取锁的线程一直处于活跃状态...

    什么是自旋锁?

    自旋锁(spinlock):是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。

    获取锁的线程一直处于活跃状态,但是并没有执行任何有效的任务,使用这种锁会造成busy-waiting。

    Java如何实现自旋锁?

    下面是个简单的例子:

    /**

    * Date: 2016年1月4日 下午4:41:50

    *

    * @author medusar

    */

    public class SpinLock {

    private AtomicReference cas = new AtomicReference();

    public void lock() {

    Thread current = Thread.currentThread();

    // 利用CAS

    while (!cas.compareAndSet(null, current)) {

    // DO nothing

    }

    }

    public void unlock() {

    Thread current = Thread.currentThread();

    cas.compareAndSet(current, null);

    }

    }

    ock()方法利用的CAS,当第一个线程A获取锁的时候,能够成功获取到,不会进入while循环,如果此时线程A没有释放锁,另一个线程B又来获取锁,此时由于不满足CAS,所以就会进入while循环,不断判断是否满足CAS,直到A线程调用unlock方法释放了该锁。

    自旋锁存在的问题

    使用自旋锁会有以下一个问题:

    1. 如果某个线程持有锁的时间过长,就会导致其它等待获取锁的线程进入循环等待,消耗CPU。使用不当会造成CPU使用率极高。

    2. 上面Java实现的自旋锁不是公平的,即无法满足等待时间最长的线程优先获取锁。不公平的锁就会存在“线程饥饿”问题。

    自旋锁的优点

    自旋锁不会使线程状态发生切换,一直处于用户态,即线程一直都是active的;不会使线程进入阻塞状态,减少了不必要的上下文切换,执行速度快

    非自旋锁在获取不到锁的时候会进入阻塞状态,从而进入内核态,当获取到锁的时候需要从内核态恢复,需要线程上下文切换。 (线程被阻塞后便进入内核(Linux)调度状态,这个会导致系统在用户态与内核态之间来回切换,严重影响锁的性能)

    可重入的自旋锁和不可重入的自旋锁

    文章开始的时候的那段代码,仔细分析一下就可以看出,它是不支持重入的,即当一个线程第一次已经获取到了该锁,在锁释放之前又一次重新获取该锁,第二次就不能成功获取到。由于不满足CAS,所以第二次获取会进入while循环等待,而如果是可重入锁,第二次也是应该能够成功获取到的。

    而且,即使第二次能够成功获取,那么当第一次释放锁的时候,第二次获取到的锁也会被释放,而这是不合理的。

    为了实现可重入锁,我们需要引入一个计数器,用来记录获取锁的线程数。

    /**

    * Date: 2016年1月4日 下午5:21:23

    *

    * @author medusar

    */

    public class ReentrantSpinLock {

    private AtomicReference cas = new AtomicReference();

    private int count;

    public void lock() {

    Thread current = Thread.currentThread();

    if (current == cas.get()) { // 如果当前线程已经获取到了锁,线程数增加一,然后返回

    count++;

    return;

    }

    // 如果没获取到锁,则通过CAS自旋

    while (!cas.compareAndSet(null, current)) {

    // DO nothing

    }

    }

    public void unlock() {

    Thread cur = Thread.currentThread();

    if (cur == cas.get()) {

    if (count > 0) {// 如果大于0,表示当前线程多次获取了该锁,释放锁通过count减一来模拟

    count--;

    } else {// 如果count==0,可以将锁释放,这样就能保证获取锁的次数与释放锁的次数是一致的了。

    cas.compareAndSet(cur, null);

    }

    }

    }

    }

    自旋锁的其他变种

    1. TicketLock

    TicketLock主要解决的是公平性的问题。

    思路:每当有线程获取锁的时候,就给该线程分配一个递增的id,我们称之为排队号,同时,锁对应一个服务号,每当有线程释放锁,服务号就会递增,此时如果服务号与某个线程排队号一致,那么该线程就获得锁,由于排队号是递增的,所以就保证了最先请求获取锁的线程可以最先获取到锁,就实现了公平性。

    可以想象成银行办理业务排队,排队的每一个顾客都代表一个需要请求锁的线程,而银行服务窗口表示锁,每当有窗口服务完成就把自己的服务号加一,此时在排队的所有顾客中,只有自己的排队号与服务号一致的才可以得到服务。

    实现代码:

    /**

    *

    * date: 2016年1月4日 下午6:09:16

    *

    * @author Medusar

    */

    public class TicketLock {

    /**

    * 服务号

    */

    private AtomicInteger serviceNum = new AtomicInteger();

    /**

    * 排队号

    */

    private AtomicInteger ticketNum = new AtomicInteger();

    /**

    * lock:获取锁,如果获取成功,返回当前线程的排队号,获取排队号用于释放锁.

    *

    * @return

    */

    public int lock() {

    int currentTicketNum = ticketNum.incrementAndGet();

    while (currentTicketNum != serviceNum.get()) {

    // Do nothing

    }

    return currentTicketNum;

    }

    /**

    * unlock:释放锁,传入当前持有锁的线程的排队号

    *

    * @param ticketnum

    */

    public void unlock(int ticketnum) {

    serviceNum.compareAndSet(ticketnum, ticketnum + 1);

    }

    }

    上面的实现方式是,线程获取锁之后,将它的排队号返回,等该线程释放锁的时候,需要将该排队号传入。但这样是有风险的,因为这个排队号是可以被修改的,一旦排队号被不小心修改了,那么锁将不能被正确释放。一种更好的实现方式如下:

    /**

    * Date: 2016年1月4日 下午6:11:50

    *

    * @author medusar

    */

    public class TicketLockV2 {

    /**

    * 服务号

    */

    private AtomicInteger serviceNum = new AtomicInteger();

    /**

    * 排队号

    */

    private AtomicInteger ticketNum = new AtomicInteger();

    /**

    * 新增一个ThreadLocal,用于存储每个线程的排队号

    */

    private ThreadLocal ticketNumHolder = new ThreadLocal();

    public void lock() {

    int currentTicketNum = ticketNum.incrementAndGet();

    // 获取锁的时候,将当前线程的排队号保存起来

    ticketNumHolder.set(currentTicketNum);

    while (currentTicketNum != serviceNum.get()) {

    // Do nothing

    }

    }

    public void unlock() {

    // 释放锁,从ThreadLocal中获取当前线程的排队号

    Integer currentTickNum = ticketNumHolder.get();

    serviceNum.compareAndSet(currentTickNum, currentTickNum + 1);

    }

    }

    上面的实现方式是将每个线程的排队号放到了ThreadLocal中。

    TicketLock存在的问题

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

    下面介绍的MCSLock和CLHLock就是解决这个问题的。

    2. CLHLock

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

    实现代码如下:

    import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;

    /**

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

    * 代码来源:http://ifeve.com/java_lock_see2/

    */

    public class CLHLock {

    /**

    * 定义一个节点,默认的lock状态为true

    */

    public static class CLHNode {

    private volatile boolean isLocked = true;

    }

    /**

    * 尾部节点,只用一个节点即可

    */

    private volatile CLHNode tail;

    private static final ThreadLocal LOCAL = new ThreadLocal();

    private static final AtomicReferenceFieldUpdater UPDATER = AtomicReferenceFieldUpdater.newUpdater(CLHLock.class, CLHNode.class,

    "tail");

    public void lock() {

    // 新建节点并将节点与当前线程保存起来

    CLHNode node = new CLHNode();

    LOCAL.set(node);

    // 将新建的节点设置为尾部节点,并返回旧的节点(原子操作),这里旧的节点实际上就是当前节点的前驱节点

    CLHNode preNode = UPDATER.getAndSet(this, node);

    if (preNode != null) {

    // 前驱节点不为null表示当锁被其他线程占用,通过不断轮询判断前驱节点的锁标志位等待前驱节点释放锁

    while (preNode.isLocked) {

    }

    preNode = null;

    LOCAL.set(node);

    }

    // 如果不存在前驱节点,表示该锁没有被其他线程占用,则当前线程获得锁

    }

    public void unlock() {

    // 获取当前线程对应的节点

    CLHNode node = LOCAL.get();

    // 如果tail节点等于node,则将tail节点更新为null,同时将node的lock状态职位false,表示当前线程释放了锁

    if (!UPDATER.compareAndSet(this, node, null)) {

    node.isLocked = false;

    }

    node = null;

    }

    }

    3. MCSLock

    MCSLock则是对本地变量的节点进行循环。

    /**

    * MCS:发明人名字John Mellor-Crummey和Michael Scott

    * 代码来源:http://ifeve.com/java_lock_see2/

    */

    public class MCSLock {

    /**

    * 节点,记录当前节点的锁状态以及后驱节点

    */

    public static class MCSNode {

    volatile MCSNode next;

    volatile boolean isLocked = true;

    }

    private static final ThreadLocal NODE = new ThreadLocal();

    // 队列

    @SuppressWarnings("unused")

    private volatile MCSNode queue;

    // queue更新器

    private static final AtomicReferenceFieldUpdater UPDATER = AtomicReferenceFieldUpdater.newUpdater(MCSLock.class, MCSNode.class,

    "queue");

    public void lock() {

    // 创建节点并保存到ThreadLocal中

    MCSNode currentNode = new MCSNode();

    NODE.set(currentNode);

    // 将queue设置为当前节点,并且返回之前的节点

    MCSNode preNode = UPDATER.getAndSet(this, currentNode);

    if (preNode != null) {

    // 如果之前节点不为null,表示锁已经被其他线程持有

    preNode.next = currentNode;

    // 循环判断,直到当前节点的锁标志位为false

    while (currentNode.isLocked) {

    }

    }

    }

    public void unlock() {

    MCSNode currentNode = NODE.get();

    // next为null表示没有正在等待获取锁的线程

    if (currentNode.next == null) {

    // 更新状态并设置queue为null

    if (UPDATER.compareAndSet(this, currentNode, null)) {

    // 如果成功了,表示queue==currentNode,即当前节点后面没有节点了

    return;

    } else {

    // 如果不成功,表示queue!=currentNode,即当前节点后面多了一个节点,表示有线程在等待

    // 如果当前节点的后续节点为null,则需要等待其不为null(参考加锁方法)

    while (currentNode.next == null) {

    }

    }

    } else {

    // 如果不为null,表示有线程在等待获取锁,此时将等待线程对应的节点锁状态更新为false,同时将当前线程的后继节点设为null

    currentNode.next.isLocked = false;

    currentNode.next = null;

    }

    }

    }

    4. CLHLock 和 MCSLock

    都是基于链表,不同的是CLHLock是基于隐式链表,没有真正的后续节点属性,MCSLock是显示链表,有一个指向后续节点的属性。

    将获取锁的线程状态借助节点(node)保存,每个线程都有一份独立的节点,这样就解决了TicketLock多处理器缓存同步的问题。

    自旋锁与互斥锁

    自旋锁与互斥锁都是为了实现保护资源共享的机制。

    无论是自旋锁还是互斥锁,在任意时刻,都最多只能有一个保持者。

    获取互斥锁的线程,如果锁已经被占用,则该线程将进入睡眠状态;获取自旋锁的线程则不会睡眠,而是一直循环等待锁释放。

    总结

    自旋锁:线程获取锁的时候,如果锁被其他线程持有,则当前线程将循环等待,直到获取到锁。

    自旋锁等待期间,线程的状态不会改变,线程一直是用户态并且是活动的(active)。

    自旋锁如果持有锁的时间太长,则会导致其它等待获取锁的线程耗尽CPU。

    自旋锁本身无法保证公平性,同时也无法保证可重入性。

    基于自旋锁,可以实现具备公平性和可重入性质的锁。

    TicketLock:采用类似银行排号叫好的方式实现自旋锁的公平性,但是由于不停的读取serviceNum,每次读写操作都必须在多个处理器缓存之间进行缓存同步,这会导致繁重的系统总线和内存的流量,大大降低系统整体的性能。

    CLHLock和MCSLock通过链表的方式避免了减少了处理器缓存同步,极大的提高了性能,区别在于CLHLock是通过轮询其前驱节点的状态,而MCS则是查看当前节点的锁状态。

    CLHLock在NUMA架构下使用会存在问题。在没有cache的NUMA系统架构中,由于CLHLock是在当前节点的前一个节点上自旋,NUMA架构中处理器访问本地内存的速度高于通过网络访问其他节点的内存,所以CLHLock在NUMA架构上不是最优的自旋锁。

    写在最后:欢迎留言讨论,私信:“Java”或“架构资料”有惊喜哟!加关注,持续更新!!!

    展开全文
  • 什么是自旋锁?以及使用自旋锁的好处和后果分别是什么呢? 什么是自旋 首先,我们了解什么叫自旋?“自旋”可以理解为“自我旋转”,这里的“旋转”指“循环”,比如 while 循环或者 for 循环。“自旋”就是自己...

    什么是自旋锁?以及使用自旋锁的好处和后果分别是什么呢?

     

    什么是自旋
    首先,我们了解什么叫自旋?“自旋”可以理解为“自我旋转”,这里的“旋转”指“循环”,比如 while 循环或者 for 循环。“自旋”就是自己在这里不停地循环,直到目标达成。而不像普通的锁那样,如果获取不到锁就进入阻塞。

    对比自旋和非自旋的获取锁的流程


    下面我们用这样一张流程图来对比一下自旋锁和非自旋锁的获取锁的过程。

    首先,我们来看自旋锁,它并不会放弃  CPU  时间片,而是通过自旋等待锁的释放,也就是说,它会不停地再次地尝试获取锁,如果失败就再次尝试,直到成功为止。

    我们再来看下非自旋锁,非自旋锁和自旋锁是完全不一样的,如果它发现此时获取不到锁,它就把自己的线程切换状态,让线程休眠,然后 CPU 就可以在这段时间去做很多其他的事情,直到之前持有这把锁的线程释放了锁,于是 CPU 再把之前的线程恢复回来,让这个线程再去尝试获取这把锁。如果再次失败,就再次让线程休眠,如果成功,一样可以成功获取到同步资源的锁。

    可以看出,非自旋锁和自旋锁最大的区别,就是如果它遇到拿不到锁的情况,它会把线程阻塞,直到被唤醒。而自旋锁会不停地尝试。那么,自旋锁这样不停尝试的好处是什么呢?

    自旋锁的好处
    首先,阻塞和唤醒线程都是需要高昂的开销的,如果同步代码块中的内容不复杂,那么可能转换线程带来的开销比实际业务代码执行的开销还要大。

    在很多场景下,可能我们的同步代码块的内容并不多,所以需要的执行时间也很短,如果我们仅仅为了这点时间就去切换线程状态,那么其实不如让线程不切换状态,而是让它自旋地尝试获取锁,等待其他线程释放锁,有时我只需要稍等一下,就可以避免上下文切换等开销,提高了效率。

    用一句话总结自旋锁的好处,那就是自旋锁用循环去不停地尝试获取锁,让线程始终处于 Runnable 状态,节省了线程状态切换带来的开销。

    AtomicLong 的实现
    在 Java 1.5 版本及以上的并发包中,也就是 java.util.concurrent 的包中,里面的原子类基本都是自旋锁的实现。

    比如我们看一个 AtomicLong 的实现,里面有一个 getAndIncrement 方法,源码如下:

    复制代码
    public final long getAndIncrement() {
        return unsafe.getAndAddLong(this, valueOffset, 1L);
    }
    可以看到它调用了一个 unsafe.getAndAddLong,所以我们再来看这个方法:

    复制代码
    public final long getAndAddLong (Object var1,long var2, long var4){
        long var6;
        do {
            var6 = this.getLongVolatile(var1, var2);
        } while (!this.compareAndSwapLong(var1, var2, var6, var6 + var4));


        return var6;
    }
    在这个方法中,它用了一个 do while 循环。这里就很明显了:

    复制代码
    do {
        var6 = this.getLongVolatile(var1, var2);

    while (!this.compareAndSwapLong(var1, var2, var6, var6 + var4));
    这里的 do-while 循环就是一个自旋操作,如果在修改过程中遇到了其他线程竞争导致没修改成功的情况,就会 while 循环里进行死循环,直到修改成功为止。

    自己实现一个可重入的自旋锁
    下面我们来看一个自己实现可重入的自旋锁。

    代码如下所示:

    复制代码
    package lesson27;
     
    import java.util.concurrent.atomic.AtomicReference;
    import java.util.concurrent.locks.Lock;
     
    /**
     * 描述:     实现一个可重入的自旋锁
     */
    public class ReentrantSpinLock  {
     
        private AtomicReference<Thread> owner = new AtomicReference<>();
     
        //重入次数
        private int count = 0;
     
        public void lock() {
            Thread t = Thread.currentThread();
            if (t == owner.get()) {
                ++count;
                return;
            }
            //自旋获取锁
            while (!owner.compareAndSet(null, t)) {
                System.out.println("自旋了");
            }
        }
     
        public void unlock() {
            Thread t = Thread.currentThread();
            //只有持有锁的线程才能解锁
            if (t == owner.get()) {
                if (count > 0) {
                    --count;
                } else {
                    //此处无需CAS操作,因为没有竞争,因为只有线程持有者才能解锁
                    owner.set(null);
                }
            }
        }
     
        public static void main(String[] args) {
            ReentrantSpinLock spinLock = new ReentrantSpinLock();
            Runnable runnable = new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() + "开始尝试获取自旋锁");
                    spinLock.lock();
                    try {
                        System.out.println(Thread.currentThread().getName() + "获取到了自旋锁");
                        Thread.sleep(4000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } finally {
                        spinLock.unlock();
                        System.out.println(Thread.currentThread().getName() + "释放了了自旋锁");
                    }
                }
            };
            Thread thread1 = new Thread(runnable);
            Thread thread2 = new Thread(runnable);
            thread1.start();
            thread2.start();
        }
    }
    这段代码的运行结果是:

    复制代码
    ...
    自旋了
    自旋了
    自旋了
    自旋了
    自旋了
    自旋了
    自旋了
    自旋了
    Thread-0释放了了自旋锁
    Thread-1获取到了自旋锁
    前面会打印出很多“自旋了”,说明自旋期间,CPU依然在不停运转。

    缺点
    那么自旋锁有没有缺点呢?其实自旋锁是有缺点的。它最大的缺点就在于虽然避免了线程切换的开销,但是它在避免线程切换开销的同时也带来了新的开销,因为它需要不停得去尝试获取锁。如果这把锁一直不能被释放,那么这种尝试只是无用的尝试,会白白浪费处理器资源。也就是说,虽然一开始自旋锁的开销低于线程切换,但是随着时间的增加,这种开销也是水涨船高,后期甚至会超过线程切换的开销,得不偿失。

    适用场景
    所以我们就要看一下自旋锁的适用场景。首先,自旋锁适用于并发度不是特别高的场景,以及临界区比较短小的情况,这样我们可以利用避免线程切换来提高效率。

    可是如果临界区很大,线程一旦拿到锁,很久才会释放的话,那就不合适用自旋锁,因为自旋会一直占用 CPU 却无法拿到锁,白白消耗资源。

    引用:https://kaiwu.lagou.com/course/courseInfo.htm?courseId=16#/detail/pc?id=267

    展开全文
  • 什么是自旋 “自旋”可以理解为“自我旋转”,这里的“旋转”指“循环”,比如 while 循环或者 for 循环。“自旋”就是自己在这里不停地循环,直到目标达成。而不像普通的那样,如果获取不到就进入阻塞 自旋和...
  • 什么是自旋 我们了解什么叫自旋?“自旋”可以理解为“自我旋转”,这里的“旋转”指“循环”,比如 while 循环或者 for 循环。“自旋”就是自己在这里不停地循环,直到目标达成。而不像普通的那样,如果获取不到...
  • 自旋锁什么是自旋?字面意思是 "自我旋转" 。在 Java 中也就是循环的意思,比如 for 循环,while 循环等等。那自旋锁顾名思义就是「线程循环地去获取锁」。非自旋锁,也就是普通锁。获取不到锁,线程就进入阻塞...
  • 什么是自旋 首先,我们了解什么叫自旋?“自旋”可以理解为“自我旋转”,这里的“旋转”指“循环”,比如 while 循环或者 for 循环。“自旋”就是自己在这里不停地循环,直到目标达成。而不像普通的那样,如果...
  • 什么是自旋锁--spinlock

    千次阅读 2018-04-04 15:24:27
    原文地址:https://blog.csdn.net/jeffasd/article/details/51321743自旋锁与互斥锁有点类似,只是... 由于自旋锁使用者一般保持锁时间非常短,因此选择自旋而不是睡眠非常必要的,自旋锁的效率远高于互斥锁。 ...
  • Linux内核最常见的锁是自旋锁(spin lock)。自旋锁最多只能被一个可执行线程持有。 如果一个线程试图获得一个被已经持有的自旋锁,那么该线程就会一直进行忙 循环---旋转---等待锁重新可用。 要是锁未被争用,请求...
  • 什么是自旋锁

    千次阅读 2019-06-05 15:26:33
    无锁编程,即不使用的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。CAS算法涉及到三个操作数 需要读写的内存值 V ...
  • 自旋锁简介 Linux 内核开发中,最常使用的锁是自旋锁。如果线程 A 获得了自旋锁,其他线程再请求锁的时候就无法获得,必须等待线程 A 释放自旋锁。也就是说,一个自旋锁同时只能被一个线程持有,用其保护共享资源就...
  • 文章目录多线程中的升级原理是什么? 多线程中的升级原理是什么
  • 自旋锁以及Java中的自旋锁的实现

    万次阅读 多人点赞 2018-08-07 00:45:18
    什么是自旋锁 多线程中,对共享资源进行访问,为了防止并发引起的相关问题,通常都是引入锁的机制来处理并发问题。 获取到资源的线程A对这个资源加锁,其他线程比如B要访问这个资源首先要获得锁,而此时A持有这个...
  • 自旋锁到底是什么

    2019-08-21 21:32:11
    什么是自旋锁? 自旋锁(spinlock):是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。 获取锁的线程一直处于...
  • 一、什么是自旋锁一直以为自旋锁也是用于多线程互斥的一种锁,原来不是!自旋锁是专为防止多处理器并发(实现保护共享资源)而引入的一种锁机制。自旋锁与互斥锁比较类似,它们都是为了解决对某项资源的互斥使用。无论...
  • 自旋锁是专为防止多处理器并发而引入的一种锁,它在内核中大量应用于中断处理等部分(对于单处理器来说,防止中断处理中的并发可简单采用关闭中断的方式,即在标志寄存器中关闭/打开中断标志位,不需要自旋锁)。...
  • 1、什么是自旋 所谓自旋,就是指当有另外一个线程来竞争时,这个线程会在原地循环等待,而不是把该线程给阻塞,直到那个获得的线程释放之后,这个线程就可以马上获得的。在原地循环的时候,是会消耗cpu的...
  • 自旋锁

    2019-06-05 10:57:08
    1. 什么是自旋锁 自旋锁与互斥锁有点类似,但是自旋锁不会引起调用者阻塞,如果自旋锁已经被别的执行单元保持,调用者会一直循环检查该自旋锁的保持者是否已经释放了锁,所以才叫自旋。 2. 自旋锁的特点 同时自旋锁...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 1,035
精华内容 414
关键字:

什么是自旋锁