什么是自旋锁?
自旋锁是一种非阻塞的线程同步机制,它采用“忙等待”的方式,通过不停地循环检查锁的状态来实现对临界区的访问控制。在没有被其他线程占有的情况下,当前线程可以获得锁并进入临界区;否则,它将在循环中等待其他线程释放锁。
忙等待并不是真正的等待,它实际上属于一种主动轮询的行为。在自旋锁中,线程会反复检查共享变量以确定是否获得了锁。如果获得了锁就直接返回继续执行,否则就继续重试,使用 CPU 时间来进行自旋操作。
与传统的阻塞方式相比,忙等待的锁并不需要切换线程的上下文或者阻塞休眠,因此没有进入等待阻塞的状态,这也是为什么说自旋锁是一种非阻塞式的锁的原因。
举个例子理解:
具体来说,每当有一个人走到洗手间门口时,他会先尝试去推门,看看门是否能打开(类比获取锁)。如果门已经打开了,则表示没有其他人在使用该洗手间,他便可以直接进入洗手间完成自己的任务。如果门没打开,代表当前有人占用了洗手间,那么他就不能进去,而是需要在门口等待。此时他不会离开门口,而是一直在转圈等待,检查有没有人出来并释放锁(类比检查锁状态),以期争取更快地进入洗手间。等到某个人使用完洗手间后,会将门打开并通过告诉门口的等待者(类比释放锁)可以进入了。
与传统的互斥锁等机制不同,自旋锁不使用阻塞和唤醒线程进行同步,而是采用“忙等待”的方式等待其他线程释放锁。假如要进入的人太多,就会出现竞争激烈、洗手间拥堵、人们在门口转圈等待的情况,这时候自旋锁可能并不是最适合的同步机制,因为其中大量耗费在“忙等待”的时间上会浪费 CPU 资源。
代码理解:
public class SpinLock {
private AtomicBoolean locked = new AtomicBoolean(false);
public void lock() {
while (!locked.compareAndSet(false, true)) {
// 自旋等待,直到获得锁
}
}
public void unlock() {
locked.set(false); // 释放锁
}
}
在上面的代码中,lock()
方法使用了 atomic 的 compareAndSet() 操作来实现对自旋锁的操作。这里利用了原子类型的 volatile 变量进行操作,利用循环检测被占用情况,如果没有被占用,就通过 compareAndSet() 方法获取锁,如果被其他线程占用,则继续自旋(循环等待),以此避免了线程阻塞带来的额外开销也避免了线程上下文切换和调度开销,可以提高锁的性能
读到这里难免有点迷惑自旋锁和互斥锁的区别在于啥啊?
实现方式区别:
自旋锁是采用忙等待的方式来保证线程独占共享资源,而互斥锁是通过阻塞和唤醒线程的方式来实现对共享资源的访问控制。
效率区别:
自旋锁由于不会让线程挂起,因此通过忙等待的方式来获取锁,相比较互斥锁可以提供更高的并发性和响应速度。但是当共享资源被其他线程持有时,自旋锁需要进行一定的自旋次数,这样可能会浪费 CPU 时间,因此自旋锁适用于轻量级的同步需求,而互斥锁适用于较重量级的同步处理。
简单的说就是我拿着资源时刻准备着,时机一到,我直接飞出去进行,实时性高,并发性也高。但是我拿着资源的过程是浪费cpu的,所以自旋锁适用于轻量级的同步需求
适用场景区别:
自旋锁通常应用于加锁时间短,且线程竞争较少或共享资源只被少量线程频繁使用的情况下,比如任务队列或共享缓存区等;而互斥锁适用于临界区代码块比较长、执行时间较长的情况,例如文件 I/O、网络传输文件、数据库操作等。总体来说,在多线程编程中,采用阻塞等待状态比采用忙等待状态更加高效。因为阻塞等待状态使得线程不会占用 CPU 时间,而忙等待状态则会浪费 CPU 时间,并且可能造成死循环等问题。但是对于一些特定的应用场景来说,如实时性要求较高的情况下,采用忙等待的方式可以更快地响应事件。因此在选择线程等待方式时,需要根据具体的应用场景来综合考虑需要采取哪种方式。
就是说,如果你追求实时性,并且竞争线程数少但是线程频繁使用的时候,你就可以考虑用自旋锁而不是互斥锁,反之就用互斥锁比较好,这样子CPU的利用率更高,而不是占着茅坑不拉屎。
等待方式区别:
当自旋锁等待时,线程会一直处于忙等待状态并占用 CPU 时间,需要考虑等待的时间和锁的竞争状况。而互斥锁在等待时另一个线程获得了锁则会失去时间片而进入阻塞状态,等到获取锁时由系统重新调度唤醒。
实现方法区别:
在 Java 中,自旋锁通常是通过 Atomic 变量来实现的;而互斥锁通常是利用 synchronized 关键字或者 ReentrantLock 等类来实现的。
自旋锁的优点:
-
原子操作:自旋锁采用原子操作,使得多个线程能够规定在同一时间获取到锁。
-
避免阻塞挂起: 自旋锁不会将线程挂起,减少了上下文切换的开销和调度器的工作负担。
-
等待时间短:自旋锁适用于轻量级的同步需求,加锁和解锁的处理时间短,适用于锁竞争不激烈、持有者锁定时间短等应用场景,提高项目效率。
-
易于实现,无死锁风险: 实现简便,使用atomic类型变量即可,避免了死锁问题的发生。
自旋锁的缺点:
-
CPU利用率低:因为自旋锁需要进行忙等待而不释放 CPU 时间,所以在并发度高且持有锁的时间长时,自旋锁的效率可能反而低于其他锁。有种占着茅坑不拉屎的感觉。
-
无法让线程挂起:当共享资源被其它线程占用时,自旋锁会一直进行忙等待,如果锁持有时间较长且竞争激烈,可能会造成很高的 CPU 资源消耗。
-
竞争激烈情况下性能下降:当自旋锁的竞争非常激烈时,也就是有大量线程同时请求同一资源时,可能会出现“活锁”,即所有线程都在不停请求资源,并尝试获取锁,但最终始终无法成功。
-
仅适用单核处理器:当应用程序运行在具有多个物理核心或超线程技术的多处理器架构中时,使用自旋锁可能引起更严重的问题,如缓存竞争、内存屏障抖动等,对实际效率造成不利影响,因此,需要结合具体环境和应用场景以选择更好的数据结构。