什么叫自旋锁?

2021-03-18 16:53发布

25条回答
云鹰的指尖故事
2楼 · 2021-03-18 19:01

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

一个Ai
3楼 · 2021-03-18 19:22
Linux内核中最常见的锁是自旋锁。一个自旋锁就是一个互斥设备,它只能有两个值:"锁定"和"解锁"。如果锁可用,则"锁定"位被设置,而代码继续进入临界区;相反,如果锁被其他进程争用,则代码进入忙循环并重复检查这个锁,直到锁可用为止。
  这个循环就是自旋锁的"自旋"。自旋锁最多只能被一个可执行的线程持有。如果一个执行线程试图获得一个被争用的自旋锁,那么该线程就会一直进行忙循环-旋转-等待锁重新可用。注意,同一个锁可以用在多个位置。
自旋锁是专为防止多处理器并发而引入的一种锁,它在内核中大量应用于中断处理等部分(对于单处理器来说,防止中断处理中的并发可简单采用关闭中断的方式,即在标志寄存器中关闭/打开中断标志位,不需要自旋锁)。
小小人
5楼 · 2021-03-19 09:14

什么是自旋锁

自旋锁的定义:当一个线程尝试去获取某一把锁的时候,如果这个锁已经被另外一个线程占有了,那么此线程就无法获取这把锁,该线程会等待,间隔一段时间后再次尝试获取。这种采用循环加锁,等待锁释放的机制就称为自旋锁(spinlock)


为什么需要自旋锁

由于在多处理器的环境中某些资源的有限性,有时需要互斥访问,这时候就需要引入锁的概念,只有获取到锁的线程才能对临界资源进行访问,由于多线程的核心是CPU的时分片,所以同一时刻只能又一个线程获取到锁。但是那些没有获取到锁的线程该怎么办呢?


通常有两种做法:一种是没有获取到锁的线程就一直等待判断该资源是否已经释放了锁,这种锁叫做自旋锁,它不会引起线程阻塞(本质上是一种忙等待机制,避免线程切换带来的系统开销)。还有一种是,没有获取到锁的线程把自己阻塞起来,重新等待CPU的调度,这种锁称为互斥锁


自旋锁的原理

自旋锁实现的原理比较简单,当那些不能立马获取到锁资源的线程,它们不会像互斥锁那样直接将自己挂起进入阻塞状态,而是等一会(自旋)不判断的去判断锁资源是否被释放了,如果释放了那么就去获取锁资源。这样做避免了那些锁竞争不激烈的情况下,从核心态到用户态的切换,避免了系统上下文切换的开销。


因为自旋锁避免了操作系统进程调度的和线程的切换,所以自旋锁通常适用于在时间比较短的情况下。但是如果长时间上锁的话,自旋锁是非常消耗性能的,因为它阻止了其他线程调度。如果线程持有锁的时间很长,那么其他线程将一直保持旋转状态(不断的去判断锁资源是否被释放了,并没有让出CPU)。


为了解决上面的问题,可以给自旋锁加一个自旋的时间,等到时间一到立即释放自旋锁。自旋锁的目的是占有CPU资源,等到获取锁立即进行处理,但是如何去选择自旋的时间呢。如果自旋时间太长,会有大量的线程占用CPU,导致系统性能降低。在JDK1.6之后,引入了适应性自旋锁,意味这自旋的时间是不固定的,而是由前一次在同一个锁上自旋的时间以及锁拥有的状态来决定,基本认为线程上下文切换的时间是最佳的一个自旋时间。


自旋锁的优缺点

在线程竞争不激烈的时候,自旋锁是需要等待前面一个获的锁的线程释放锁后就可以在很短的时间内获取到锁从而继续执行,避免了直接将线程阻塞再去重新等待CPU调度所浪费的两次上下文切换的系统开销,这可以极大的提升系统的性能。


在线程竞争很激烈的时候,自旋锁就显得有一点笨拙了。因为自旋锁在获取到锁资源之前CPU是一直在做无用功,同时大量的线程去竞争一个锁资源,会导致获取锁的时间很长。这种情况下,就白白的浪费了许多CPU资源。


LERRR
6楼 · 2021-03-19 10:40

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

为了换取性能,JVM在内置锁上做了非常多的优化,膨胀式的锁分配策略就是其一。理解偏向锁、轻量级锁、重量级锁的要解决的基本问题,几种锁的分配和膨胀过程,有助于编写并优化基于锁的并发程序。

内置锁的分配和膨胀过程较为复杂,限于时间和精力,文中该部分内容是根据网上的多方资料整合而来;仅为方便查阅,后面继续分析JVM源码的时候也有个参考。如果对各级锁已经有了基本了解,读者大可跳过此文。

隐藏在内置锁下的基本问题

内置锁是JVM提供的最便捷的线程同步工具,在代码块或方法声明上添加synchronized关键字即可使用内置锁。使用内置锁能够简化并发模型;随着JVM的升级,几乎不需要修改代码,就可以直接享受JVM在内置锁上的优化成果。从简单的重量级锁,到逐渐膨胀的锁分配策略,使用了多种优化手段解决隐藏在内置锁下的基本问题。

重量级锁

内置锁在Java中被抽象为监视器锁(monitor)。在JDK 1.6之前,监视器锁可以认为直接对应底层操作系统中的互斥量(mutex)。这种同步方式的成本非常高,包括系统调用引起的内核态与用户态切换、线程阻塞造成的线程切换等。因此,后来称这种锁为“重量级锁”。

自旋锁

首先,内核态与用户态的切换上不容易优化。但通过自旋锁,可以减少线程阻塞造成的线程切换(包括挂起线程和恢复线程)。

如果锁的粒度小,那么锁的持有时间比较短(尽管具体的持有时间无法得知,但可以认为,通常有一部分锁能满足上述性质)。那么,对于竞争这些锁的而言,因为锁阻塞造成线程切换的时间与锁持有的时间相当,减少线程阻塞造成的线程切换,能得到较大的性能提升。具体如下:

  • 当前线程竞争锁失败时,打算阻塞自己

  • 不直接阻塞自己,而是自旋(空等待,比如一个空的有限for循环)一会

  • 在自旋的同时重新竞争锁

  • 如果自旋结束前获得了锁,那么锁获取成功;否则,自旋结束后阻塞自己

如果在自旋的时间内,锁就被旧owner释放了,那么当前线程就不需要阻塞自己(也不需要在未来锁释放时恢复),减少了一次线程切换。

“锁的持有时间比较短”这一条件可以放宽。实际上,只要锁竞争的时间比较短(比如线程1快释放锁的时候,线程2才会来竞争锁),就能够提高自旋获得锁的概率。这通常发生在锁持有时间长,但竞争不激烈的场景中。

缺点

  • 单核处理器上,不存在实际的并行,当前线程不阻塞自己的话,旧owner就不能执行,锁永远不会释放,此时不管自旋多久都是浪费;进而,如果线程多而处理器少,自旋也会造成不少无谓的浪费。

  • 自旋锁要占用CPU,如果是计算密集型任务,这一优化通常得不偿失,减少锁的使用是更好的选择。

  • 如果锁竞争的时间比较长,那么自旋通常不能获得锁,白白浪费了自旋占用的CPU时间。这通常发生在锁持有时间长,且竞争激烈的场景中,此时应主动禁用自旋锁。

使用-XX:-UseSpinning参数关闭自旋锁优化;-XX:PreBlockSpin参数修改默认的自旋次数。

自适应自旋锁

自适应意味着自旋的时间不再固定了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定:

  • 如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能再次成功,进而它将允许自旋等待持续相对更长的时间,比如100个循环。

  • 相反的,如果对于某个锁,自旋很少成功获得过,那在以后要获取这个锁时将可能减少自旋时间甚至省略自旋过程,以避免浪费处理器资源。

自适应自旋解决的是“锁竞争时间不确定”的问题。JVM很难感知到确切的锁竞争时间,而交给用户分析就违反了JVM的设计初衷。自适应自旋假定不同线程持有同一个锁对象的时间基本相当,竞争程度趋于稳定,因此,可以根据上一次自旋的时间与结果调整下一次自旋的时间

缺点

然而,自适应自旋也没能彻底解决该问题,如果默认的自旋次数设置不合理(过高或过低),那么自适应的过程将很难收敛到合适的值

轻量级锁

自旋锁的目标是降低线程切换的成本。如果锁竞争激烈,我们不得不依赖于重量级锁,让竞争失败的线程阻塞;如果完全没有实际的锁竞争,那么申请重量级锁都是浪费的。轻量级锁的目标是,减少无实际竞争情况下,使用重量级锁产生的性能消耗,包括系统调用引起的内核态与用户态切换、线程阻塞造成的线程切换等。

顾名思义,轻量级锁是相对于重量级锁而言的。使用轻量级锁时,不需要申请互斥量,仅仅将Mark Word中的部分字节CAS更新指向线程栈中的Lock Record,如果更新成功,则轻量级锁获取成功,记录锁状态为轻量级锁;否则,说明已经有线程获得了轻量级锁,目前发生了锁竞争(不适合继续使用轻量级锁),接下来膨胀为重量级锁

Mark Word是对象头的一部分;每个线程都拥有自己的线程栈(虚拟机栈),记录线程和函数调用的基本信息。二者属于JVM的基础内容,此处不做介绍。

当然,由于轻量级锁天然瞄准不存在锁竞争的场景,如果存在锁竞争但不激烈,仍然可以用自旋锁优化,自旋失败后再膨胀为重量级锁

缺点

同自旋锁相似:

  • 如果锁竞争激烈,那么轻量级将很快膨胀为重量级锁,那么维持轻量级锁的过程就成了浪费。

偏向锁

在没有实际竞争的情况下,还能够针对部分场景继续优化。如果不仅仅没有实际竞争,自始至终,使用锁的线程都只有一个,那么,维护轻量级锁都是浪费的。偏向锁的目标是,减少无竞争且只有一个线程使用锁的情况下,使用轻量级锁产生的性能消耗。轻量级锁每次申请、释放锁都至少需要一次CAS,但偏向锁只有初始化时需要一次CAS。

“偏向”的意思是,偏向锁假定将来只有第一个申请锁的线程会使用锁(不会有任何线程再来申请锁),因此,只需要在Mark Word中CAS记录owner(本质上也是更新,但初始值为空),如果记录成功,则偏向锁获取成功,记录锁状态为偏向锁,以后当前线程等于owner就可以零成本的直接获得锁;否则,说明有其他线程竞争,膨胀为轻量级锁

偏向锁无法使用自旋锁优化,因为一旦有其他线程申请锁,就破坏了偏向锁的假定。

缺点

同样的,如果明显存在其他线程申请锁,那么偏向锁将很快膨胀为轻量级锁。

不过这个副作用已经小的多。

如果需要,使用参数-XX:-UseBiasedLocking禁止偏向锁优化(默认打开)。

小结

偏向锁、轻量级锁、重量级锁分配和膨胀的详细过程见后。会涉及一些Mark Word与CAS的知识。

偏向锁、轻量级锁、重量级锁适用于不同的并发场景:

  • 偏向锁:无实际竞争,且将来只有第一个申请锁的线程会使用锁。

  • 轻量级锁:无实际竞争,多个线程交替使用锁;允许短时间的锁竞争。

  • 重量级锁:有实际竞争,且锁竞争时间长。

另外,如果锁竞争时间短,可以使用自旋锁进一步优化轻量级锁、重量级锁的性能,减少线程切换。

如果锁竞争程度逐渐提高(缓慢),那么从偏向锁逐步膨胀到重量锁,能够提高系统的整体性能。

锁分配和膨胀过程

重申,这部分主要是根据网上的多方资料整理。核心是这位巨巨整理的流程图,相当详细,基本符合逻辑。

前面讲述了内置锁在使用过程中的一些基本问题和解决方案,实现原理一笔带过。详细的锁分配和膨胀过程如下:

image.png

图中有一处疑问:

按照图中流程,如果发现锁已经膨胀为重量级锁,就直接使用互斥量mutex阻塞当前线程

然而,自旋锁的一大好处就是减少线程切换的开销。在这里没有必要直接阻塞当前线程,大可以像轻量级锁一样,自旋一会,失败了再阻塞。

特别说明两点:

  • CAS记录owner时,expected == nullnewValue == ownerThreadId,因此,只有第一个申请偏向锁的线程能够返回成功,后续线程都必然失败(部分线程检测到可偏向,同时尝试CAS记录owner)。

  • 内置锁只能沿着偏向锁、轻量级锁、重量级锁的顺序逐渐膨胀,不能“收缩”。这基于JVM的另一个假定,“一旦破坏了上一级锁的假定,就认为该假定以后也必不成立”。

另外,当重量级锁被解除后,需要唤醒一个被阻塞的线程,这部分逻辑与ReentrantLock基本相同,详见源码|并发一枝花之ReentrantLock与AQS(1):lock、unlock。

简化版

上图记载的很详细,也有Mark Word的图解。看懂上图后,再来看《深入理解Java虚拟机:JVM高级特性与最佳实践(第2版)》中的简化版流程图就能看懂了:



8楼 · 2021-03-19 17:39

Linux内核中最常见的锁是自旋锁。一个自旋锁就是一个互斥设备,它只能有两个值:"锁定"和"解锁"。如果锁可用,则"锁定"位被设置,而代码继续进入临界区;相反,如果锁被其他进程争用,则代码进入忙循环并重复检查这个锁,直到锁可用为止。

我的网名不再改
9楼 · 2021-03-20 12:58

简单回顾一下CAS算法

CAS算法 即compare and swap(比较与交换),是一种有名的无锁算法。无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。CAS算法涉及到三个操作数

  • 需要读写的内存值 V

  • 进行比较的值 A

  • 拟写入的新值 B

当且仅当 V 的值等于 A时,CAS通过原子方式用新值B来更新V的值,否则不会执行任何操作(比较和替换是一个原子操作)。一般情况下是一个自旋操作,即不断的重试。

什么是自旋锁?

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

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

它是为实现保护共享资源而提出一种锁机制。其实,自旋锁与互斥锁比较类似,它们都是为了解决对某项资源的互斥使用。无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个保持者,也就说,在任何时刻最多只能有一个执行单元获得锁。但是两者在调度机制上略有不同。对于互斥锁,如果资源已经被占用,资源申请者只能进入睡眠状态。但是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,”自旋”一词就是因此而得名。

Java如何实现自旋锁?

下面是个简单的例子:

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);
    }
}

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

自旋锁存在的问题

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

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

自旋锁的优点

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

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

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

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

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

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


相关问题推荐

  • 回答 156

    对于每一位才开始接触JAVA的新手来说,先不要管算法和数据结构,大多数简单的程序不需要用到算法和数据结构,所以当你真正需要时再去学习。编程一段时间以后,你就会知道在哪些地方用到他们。这时知道算法的名字并了解它们的功能,然后动手去实践。当我们在去...

  • 回答 93

    2个都很好就业,更关键的是要学得到东西

  • 回答 12
    已采纳

    获取Map集合中所有的key可以通过map集合的keySet()方法获取例如:    Map map = new HashMap();    map.put(xx,xx); //存放数据    //.... 省略    Set set = map.keySet();    //可以通过迭代器进行测试    Iterator iter = set.iter...

  • 回答 56
    已采纳

    不同年龄,不同掌握程度,学历,找工作城市,面试能力这是一个多方面影响的结果,如果是平均值的话,全国平均薪资14k左右

  • 回答 38

    具体学多久,根据自己的学习力,自律性、解决问题能力来决定若系统性学习,跟着讲师的节奏走,大概半年左右,有专业的讲师把课程进行规划,尽心系统学习,有问题,讲师会帮忙解决,学习的效率很高,避免了自学中出现各种问题解决不了,而耽误很多时间,可能会...

  • BIO与NIO、AIO的区别2020-05-19 15:59
    回答 4
    已采纳

    IO的方式通常分为几种,同步阻塞的BIO、同步非阻塞的NIO、异步非阻塞的AIO。一、BIO     在JDK1.4出来之前,我们建立网络连接的时候采用BIO模式,需要先在服务端启动一个ServerSocket,然后在客户端启动Socket来对服务端进行通信,默认情况下服务端需要...

  • 回答 23
    已采纳

    (1)idea启动时会有两个快捷方式,安装完后默认生成在桌面的是32位的idea的快捷方式,如果我们使用这个快捷方式运行大项目,一般都会很卡。解决方法是找到idea的安装目录,然后进入bin文件夹,找到名称为idea64的应用程序,右键他生成桌面快捷方式。以后每次...

  • Java方法的命名规则2021-04-06 19:07
    回答 31

    ava是一种区分字母的大小写的语言,所以我们在定义变量名的时候应该注意区分大小写的使用和一些规范,接下来我们简单的来讲讲Java语言中包、类、变量等的命名规范。(一)Package(包)的命名Package的名字应该都是由一个小写单词组成,例如com、xuetang9、compan...

  • 回答 2

    public class Point {    private int x;    private int y;    public int getX() {        return x;    }    public void setX(int x) {        this.x = x;    }    public int getY() {        return y;    } ...

  • 回答 6

    经典版单例模式public class Singleton {        private static Singleton uniqueInstance;//利用一个静态常量来记录singleton类的唯一实例。     private Singleton() {     }     public static  Singleton getInstance()...

  • 回答 3

    哈希表的长度一般是定长的,在存储数据之前我们应该知道我们存储的数据规模是多大,应该尽可能地避免频繁地让哈希表扩容。但是如果设计的太大,那么就会浪费空间,因为我们跟不用不到那么大的空间来存储我们当前的数据规模;如果设计的太小,那么就会很容易发...

  • 回答 14

    1. DOM(Document Object Model)        DOM是用与平台和语言无关的方式表示XML文档的官方W3C标准。DOM是以层次结构组织的节点或信息片断的集合。这个层次结构允许开发人员在树中寻找特定信息。分析该结构通常需要加载整个文档和构造层次结构,然后才...

  • 回答 19

    1)作用不同: throw用于程序员自行产生并抛出异常; throws用于声明在该方法内抛出了异常2) 使用的位置不同: throw位于方法体内部,可以作为单独语句使用; throws必须跟在方法参数列表的后面,不能单独使用。3)内容不同: throw抛出一个异常对象,且只能是...

  • 回答 11

    基本执行过程如下:1)程序首先执行可能发生异常的try语句块。2)如果try语句没有出现异常则执行完后跳至finally语句块执行;3)如果try语句出现异常,则中断执行并根据发生的异常类型跳至相应的catch语句块执行处理。4)catch语句块可以有多个,分别捕获不同类型...

  • 回答 20

    100-199 用于指定客户端应相应的某些动作。 200-299 用于表示请求成功。 300-399 用于已经移动的文件并且常被包含在定位头信息中指定新的地址信息。 400-499 用于指出客户端的错误。 400 语义有误,当前请求无法被服务器理解。 401 当前请求需要用户验证...

  • 回答 16

    异常表示程序运行过程中可能出现的非正常状态,运行时异常表示虚拟机的通常操作中可能遇到的异常,是一种常见运行错误,只要程序设计得没有问题通常就不会发生。受检异常跟程序运行的上下文环境有关,即使程序设计无误,仍然可能因使用的问题而引发。Java编译...

没有解决我的问题,去提问