什么是自旋锁?

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

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

Java如何实现自旋锁?

下面是个简单的例子:

/**

* Date: 2016年1月4日 下午4:41:50 <br/>

*

* @author medusar

*/

public class SpinLock {

private AtomicReference<Thread> cas = new AtomicReference<Thread>();

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实现的自旋锁不是公平的,即无法满足等待时间最长的线程优先获取锁。不公平的锁就会存在“线程饥饿”问题。

自旋锁的优点

  1. 自旋锁不会使线程状态发生切换,一直处于用户态,即线程一直都是active的;不会使线程进入阻塞状态,减少了不必要的上下文切换,执行速度快
  2. 非自旋锁在获取不到锁的时候会进入阻塞状态,从而进入内核态,当获取到锁的时候需要从内核态恢复,需要线程上下文切换。 (线程被阻塞后便进入内核(Linux)调度状态,这个会导致系统在用户态与内核态之间来回切换,严重影响锁的性能)

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

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

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

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

/**

* Date: 2016年1月4日 下午5:21:23 <br/>

*

* @author medusar

*/

public class ReentrantSpinLock {

private AtomicReference<Thread> cas = new AtomicReference<Thread>();

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 <br/>

*

* @author Medusar

*/

public class TicketLock {

/**

* 服务号

*/

private AtomicInteger serviceNum = new AtomicInteger();

/**

* 排队号

*/

private AtomicInteger ticketNum = new AtomicInteger();

/**

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

*

* @return

*/

public int lock() {

int currentTicketNum = ticketNum.incrementAndGet();

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

// Do nothing

}

return currentTicketNum;

}

/**

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

*

* @param ticketnum

*/

public void unlock(int ticketnum) {

serviceNum.compareAndSet(ticketnum, ticketnum + 1);

}

}

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

/**

* Date: 2016年1月4日 下午6:11:50 <br/>

*

* @author medusar

*/

public class TicketLockV2 {

/**

* 服务号

*/

private AtomicInteger serviceNum = new AtomicInteger();

/**

* 排队号

*/

private AtomicInteger ticketNum = new AtomicInteger();

/**

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

*/

private ThreadLocal<Integer> ticketNumHolder = new ThreadLocal<Integer>();

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<CLHNode> LOCAL = new ThreadLocal<CLHNode>();

private static final AtomicReferenceFieldUpdater<CLHLock, CLHNode> 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<MCSNode> NODE = new ThreadLocal<MCSNode>();

// 队列

@SuppressWarnings("unused")

private volatile MCSNode queue;

// queue更新器

private static final AtomicReferenceFieldUpdater<MCSLock, MCSNode> 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”或“架构资料”有惊喜哟!加关注,持续更新!!!

认真的讲一讲:自旋锁到底是什么相关推荐

  1. CAS自旋锁到底是什么?为什么能实现线程安全?

  2. Java锁的种类以及辨析(二):自旋锁的其他种类

    作者:山鸡 锁作为并发共享数据,保证一致性的工具,在JAVA平台有多种实现(如 synchronized 和 ReentrantLock等等 ) .这些已经写好提供的锁为我们开发提供了便利,但是锁的具 ...

  3. 深入理解Linux自旋锁(1.0)

    学习方法论 写作原则 标题括号中的数字代表完成度与完善度 0.0-1.0 代表完成度,1.1-1.5 代表完善度 0.0 :还没开始写 0.1 :写了一个简介 0.3 :写了一小部分内容 0.5 :写 ...

  4. 读写自旋锁,第1部分(来自IBM)

    读写自旋锁简介 什么是读写自旋锁 由于互斥的特点,使用自旋锁的代码毫无线程并发性可言,多处理器系统的性能受到限制.通过观察线程在临界区的访问行为,我们发现有些线程只是简单地读取信息,并不修改任何东西, ...

  5. 读写自旋锁详解:TODO

    林 昊翔 2011 年 7 月 21 日发布 Table of Contents 读写自旋锁简介 什么是读写自旋锁 读写自旋锁的属性 以自动机的观点看读写自旋锁 读写自旋锁的实现细节 读写自旋锁的接口 ...

  6. 高薪程序员面试题精讲系列68之可重入锁、公平锁、自旋锁是怎么回事?

    一. 面试题及剖析 1. 今日面试题 除了synchronized与Lock,你还了解哪些锁? 可重入锁与不可重入锁有什么区别? 你了解公平锁吗? 什么是自旋锁? 2. 题目剖析 壹哥 在上一篇文章中 ...

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

    在本课时我们主要讲解什么是自旋锁?以及使用自旋锁的好处和后果分别是什么呢? 什么是自旋 首先,我们了解什么叫自旋?"自旋"可以理解为"自我旋转",这里的&quo ...

  8. Linux内核中的同步原语:自旋锁,信号量,互斥锁,读写信号量,顺序锁

    Linux内核中的同步原语 自旋锁,信号量,互斥锁,读写信号量,顺序锁 rtoax 2021年3月 在英文原文基础上,针对中文译文增加5.10.13内核源码相关内容. 1. Linux 内核中的同步原 ...

  9. linux 进程调度 运行队列 自旋锁,linux内核进程调度(自旋锁)

    2.1首先让我们了解,操作系统分为两类:一类是实时操作系统,一类是分时操作系统.它们的共同特点是都是多任务的 .多任务操作系统分为两类:非抢占式多任务和抢占式多任务. 非抢占式多任务,就是指进程不断的 ...

最新文章

  1. Quartz框架多个trigger任务执行出现漏执行的问题分析--转
  2. 系列(六)—Linux命令
  3. PHP $_SERVER['HTTP_REFERER'] 获取前一页面的 URL 地址
  4. 五个角度,来梳理下产品经理的分类和职业发展方向
  5. numpy教程:逻辑函数Logic functions
  6. RecyclerView 判断滑到底部 顶部 预加载 更多 分页 MD
  7. python读取大智慧数据_大智慧数据格式
  8. eclipse配置struts2详细介绍
  9. opencv学习(四十四)之图像角点检测Harris
  10. JavaFX Scene Builder的使用
  11. sony相机二次开发sdK C语言,sdk与开放API协议支持二次开发的摄像头
  12. 计算机点击管理无效,win10开始菜单没反应,二种解决办法!
  13. AVFoundation 框架小结
  14. java在控制台用星号打印出圆形
  15. ABM410-ASEMI贴片整流桥ABM410
  16. android framework实战车机手机系统开发环境相关问题补充
  17. QVW Load多个不同目录下的QVD文件
  18. matlab非线性数值方程的求解
  19. zabbix某一个代理服务器下面多个agent出现5分钟数据采集不到的告警的解决过程...
  20. 华为H22M-03服务器ubuntu配置

热门文章

  1. 视频播放器实现技术(一)
  2. JVM虚拟机,也就那么回事!(总结+绘图+流程+代码)
  3. 实习技术员的基本功(八)
  4. 【css】css display属性的值及用法
  5. 一键部署项目到服务器
  6. Android定制出厂默认输入法
  7. 【英语】大学英语CET考试,口语部分2(课程笔记)
  8. Kafka启动异常:kafka.common.InconsistentClusterIdException
  9. FIR滤波器和IIR滤波器区别
  10. 一个管理者,不应该是一个好员工