在本课时我们主要讲解什么是自旋锁?以及使用自旋锁的好处和后果分别是什么呢?

什么是自旋

首先,我们了解什么叫自旋?“自旋”可以理解为“自我旋转”,这里的“旋转”指“循环”,比如 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() {@Overridepublic 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://tech.meituan.com/2018/11/15/java-lock.html

自旋锁的实现的代码来自 https://www.fatalerrors.org/a/java-implementation-of-spin-lock.html

Java并发编程78讲--27 第27讲:什么是自旋锁?自旋的好处和后果是什么呢?相关推荐

  1. Java并发编程78讲--29 第29讲:HahMap 为什么是线程不安全的?

    本课时我们主要讲解为什么 HashMap 是线程不安全的?而对于 HashMap,相信你一定并不陌生,HashMap 是我们平时工作和学习中用得非常非常多的一个容器,也是 Map 最主要的实现类之一, ...

  2. Java并发编程(1):可重入内置锁

    每个Java对象都可以用做一个实现同步的锁,这些锁被称为内置锁或监视器锁.线程在进入同步代码块之前会自动获取锁,并且在退出同步代码块时会自动释放锁.获得内置锁的唯一途径就是进入由这个锁保护的同步代码块 ...

  3. Java并发编程之阻塞队列

    Java并发编程之阻塞队列 目录 阻塞队列概述 为什么用?有什么好处? BlockingQueue的核心方法 SynchronousQueue的用法 用在哪里 1. 阻塞队列概述 阻塞队列,顾名思义, ...

  4. Java并发编程71道面试题及答案

    Java并发编程71道面试题及答案 1.在java中守护线程和本地线程区别? java中的线程分为两种:守护线程(Daemon)和用户线程(User). 任何线程都可以设置为守护线程和用户线程,通过方 ...

  5. Java并发编程75道面试题及答案

    1.在java中守护线程和本地线程区别?java中的线程分为两种:守护线程(Daemon)和用户线程(User).任何线程都可以设置为守护线程和用户线程,通过方法Thread.setDaemon(bo ...

  6. java并发实战编程pdf_「原创」Java并发编程系列25 | 交换器Exchanger

    2020年Java面试题库连载中 [000期]Java最全面试题库思维导图 [001期]JavaSE面试题(一):面向对象 [002期]JavaSE面试题(二):基本数据类型与访问修饰符 [003期] ...

  7. 15、Java并发编程:Callable、Future和FutureTask

    Java并发编程:Callable.Future和FutureTask 在前面的文章中我们讲述了创建线程的2种方式,一种是直接继承Thread,另外一种就是实现Runnable接口. 这2种方式都有一 ...

  8. JAVA并发编程: CAS和AQS

    说起JAVA并发编程,就不得不聊聊CAS(Compare And Swap)和AQS了(AbstractQueuedSynchronizer). CAS(Compare And Swap) 什么是CA ...

  9. [转]Java并发编程:线程池的使用

    Java并发编程:线程池的使用 在前面的文章中,我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题: 如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了, ...

最新文章

  1. Overload Overwrite Override
  2. Consul 入门指南
  3. python 停止工作 scapy_常见问题 - Scapy 中文文档_教程_Python开发社区
  4. hadoop安装部署(伪分布及集群)
  5. 使用ubuntu的新立德下载和安装的Eclipse无法在其help菜单中连接并安装ADT
  6. mysql 时间序列可视化工具_mysql – 从from到to条目创建时间序列
  7. SQL Server商业智能中ODS表和Dim表必须有的字段
  8. JS 字符串编码函数(解决URL特殊字符传递问题):escape()、encodeURI()、encodeURIComponent()区别详解
  9. ceph纠删码性能测试
  10. i5 1230u怎么样 i51230u相当于什么水平级别
  11. 【微信小程序】-- 页面导航 -- 编程式导航(二十三)
  12. setoolkit的钓鱼攻击
  13. “Ballerina”可能成为集成的编程语言
  14. linux识别不了u盘格式,Linux识别不了u盘怎么办
  15. 手动实现表格分页功能
  16. 迅为IMX6Q开发板 Buildroot文件系统mqtt测试
  17. html5渐变的两种方式,HTML5的渐变色 渐变的两种类型 createLinearGradient 和createRadialGradient...
  18. Eclipse安装DSS会出现的一点问题
  19. 仓库物资管理系统(C#+SQL)
  20. java实现oss文件存储

热门文章

  1. 递推法:斐波那契数列java
  2. 李阳疯狂英语突破对话(37)-多多益善
  3. 语音识别,语义理解一站式解决之二(android,olami)
  4. 【转】努力赚钱,是为了不让自己的爱情受到别人金钱的考验
  5. 【C语言笔记】struct结构体变量的用法
  6. SpringBoot 开局篇
  7. 跨文化交际学概论详细笔记必备
  8. 为什么字节跳动的前端面试需要那么难的算法题?
  9. 相濡以沫非真性----读《庄子》有感
  10. 2023年大学开学典礼讲话稿