什么是自旋锁
说道自旋锁就要从多线程下的锁机制说起,由于在多处理器系统环境中有些资源因为其有限性,有时需要互斥访问(mutual exclusion),这时会引入锁的机制,只有获取了锁的进程才能获取资源访问。即每次只能有且只有一个进程能获取锁,才能进入自己的临界区,同一时间不能两个或两个以上进程进入临界区,当退出临界区时释放锁。
设计互斥算法时总是会面临一种情况,即没有获得锁的进程怎么办?
通常有2种处理方式:
一种是没有获得锁的调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,这就是本文的重点——自旋锁。他不用将线城阻塞起来(NON-BLOCKING)。
另一种是没有获得锁的进程就阻塞(BLOCKING)自己,继续执行线程上的其他任务,这就是 ——互斥锁(包括内置锁Synchronized还有ReentrantLock等等)。
引言
CAS(Compare and swap),即比较并交换,也是实现我们平时所说的自旋锁或乐观锁的核心操作。
它的实现很简单,就是用一个预期的值和内存值进行比较,如果两个值相等,就用预期的值替换内存值,并返回 true。否则,返回 false。
保证原子操作
任何技术的出现都是为了解决某些特定的问题, CAS 要解决的问题就是保证原子操作。原子操作是什么,原子就是最小不可拆分的,原子操作就是最小不可拆分的操作,也就是说操作一旦开始,就不能被打断,知道操作完成。在多线程环境下,原子操作是保证线程安全的重要手段。举个例子来说,假设有两个线程在工作,都想对某个值做修改,就拿自增操作来说吧,要对一个整数 i 进行自增操作,需要基本的三个步骤:
1、读取 i 的当前值;
2、对 i 值进行加 1 操作;
3、将 i 值写回内存;
假设两个进程都读取了 i 的当前值,假设是 0,这时候 A 线程对 i 加 1 了,B 线程也 加 1,最后 i 的是 1 ,而不是 2。这就是因为自增操作不是原子操作,分成的这三个步骤可以被干扰。如下面这个例子,10个线程,每个线程都执行 10000 次 i++ 操作,我们期望的值是 100,000,但是很遗憾,结果总是小于 100,000 的。
static int i = 0; public static void add(){ i++; } private static class Plus implements Runnable{ @Override public void run(){ for(int k = 0;k<10000;k++){ add(); } } } public static void main(String[] args) throws InterruptedException{ Thread[] threads = new Thread[10]; for(int i = 0;i<10;i++){ threads[i] = new Thread(new Plus()); threads[i].start(); } for(int i = 0;i<10;i++){ threads[i].join(); } System.out.println(i); }
既然这样,那怎么办。没错,也许你已经想到了,可以加锁或者利用 synchronized 实现,例如,将 add() 方法修改为如下这样:
public synchronized static void add(){ i++; }
或者,加锁操作,例如下面使用 ReentrantLock (可重入锁)实现。
private static Lock lock = new ReentrantLock(); public static void add(){ lock.lock(); i++; lock.unlock(); }
CAS 实现自旋锁
既然用锁或 synchronized 关键字可以实现原子操作,那么为什么还要用 CAS 呢,因为加锁或使用 synchronized 关键字带来的性能损耗较大,而用 CAS 可以实现乐观锁,它实际上是直接利用了 CPU 层面的指令,所以性能很高。
上面也说了,CAS 是实现自旋锁的基础,CAS 利用 CPU 指令保证了操作的原子性,以达到锁的效果,至于自旋呢,看字面意思也很明白,自己旋转,翻译成人话就是循环,一般是用一个无限循环实现。这样一来,一个无限循环中,执行一个 CAS 操作,当操作成功,返回 true 时,循环结束;当返回 false 时,接着执行循环,继续尝试 CAS 操作,直到返回 true。
其实 JDK 中有好多地方用到了 CAS ,尤其是java.util.concurrent包下,比如 CountDownLatch、Semaphore、ReentrantLock 中,再比如 java.util.concurrent.atomic 包下,相信大家都用到过 Atomic* ,比如 AtomicBoolean、AtomicInteger 等。
这里拿 AtomicBoolean 来举个例子,因为它足够简单。
public class AtomicBoolean implements java.io.Serializable { private static final long serialVersionUID = 4654671469794556979L; // setup to use Unsafe.compareAndSwapInt for updates private static final Unsafe unsafe = Unsafe.getUnsafe(); private static final long valueOffset; static { try { valueOffset = unsafe.objectFieldOffset (AtomicBoolean.class.getDeclaredField("value")); } catch (Exception ex) { throw new Error(ex); } } private volatile int value; public final boolean get() { return value != 0; } public final boolean compareAndSet(boolean expect, boolean update) { int e = expect ? 1 : 0; int u = update ? 1 : 0; return unsafe.compareAndSwapInt(this, valueOffset, e, u); } }
这是 AtomicBoolean 的部分代码,我们看到这里面又几个关键方法和属性。
1、使用了 sun.misc.Unsafe 对象,这个类提供了一系列直接操作内存对象的方法,只是在 jdk 内部使用,不建议开发者使用;
2、value 表示实际值,可以看到 get 方法实际是根据 value 是否等于0来判断布尔值的,这里的 value 定义为 volatile,因为 volatile 可以保证内存可见性,也就是 value 值只要发生变化,其他线程是马上可以看到变化后的值的;下一篇会讲一下 volatile 可见性问题,欢迎关注
3、valueOffset 是 value 值的内存偏移量,用 unsafe.objectFieldOffset 方法获得,用作后面的 compareAndSet 方法;
4、compareAndSet 方法,这就是实现 CAS 的核心方法了,在使用 AtomicBoolean 的这个方法时,只需要传递期望值和待更新的值即可,而它里面调用了 unsafe.compareAndSwapInt(this, valueOffset, e, u) 方法,它是个 native 方法,用 c++++ 实现,具体的代码就不贴了,总之是利用了 CPU 的 cmpxchg 指令完成比较并替换,当然根据具体的系统版本不同,实现起来也有所区别,感兴趣的可以自行搜一下相关文章。
使用场景
比如 AtomicBoolean 可以用在这样一个场景下,系统需要根据一个布尔变量的状态属性来判断是否需要执行一些初始化操作,如果是多线程的环境下,避免多次重复执行,可以使用 AtomicBoolean 来实现,伪代码如下:
private final static AtomicBoolean flag = new AtomicBoolean(); if(flag.compareAndSet(false,true)){ init(); }
比如 AtomicInteger 可以用在计数器中,多线程环境中,保证计数准确。
ABA问题
CAS 存在一个问题,就是一个值从 A 变为 B ,又从 B 变回了 A,这种情况下,CAS 会认为值没有发生过变化,但实际上是有变化的。对此,并发包下倒是有 AtomicStampedReference 提供了根据版本号判断的实现,可以解决一部分问题。
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对小牛知识库的支持。
本文向大家介绍mysql中的锁机制深入讲解,包括了mysql中的锁机制深入讲解的使用技巧和注意事项,需要的朋友参考一下 前言 为了保证数据的一致完整性,任何一个数据库都存在锁定机制。锁定机制的优劣直接应想到一个数据库系统的并发处理能力和性能,所以锁定机制的实现也就成为了各种数据库的核心技术之一。 大概几个月之前项目中用到事务,需要保证数据的强一致性,期间也用到了mysql的锁,但当时对mysql的
问题内容: 在linux内核中,为什么在按住自旋锁的同时无法入睡? 问题答案: 示例:您的驱动程序正在执行,并且刚拿出一个锁来控制对其设备的访问。保持锁定状态时,设备会发出一个中断,这会使您的中断处理程序运行。在访问设备之前,中断处理程序还必须获得锁。在中断处理程序中取出自旋锁是一件合法的事。这是自旋锁操作无法hibernate的原因之一。但是,如果中断例程与最初取出锁的代码在同一处理器中执行,会
本文向大家介绍MyBatis中$和#的深入讲解,包括了MyBatis中$和#的深入讲解的使用技巧和注意事项,需要的朋友参考一下 这是一次代码优化过程中发现的问题,在功能优化后发现部分数据查不到出来了,问题就在于一条sql上的#和$。 下图为两条sql: 从图上可以看出 wwlr.LabelId in(${showLabels}) 和 wwlr.LabelId in(#{showLabels}),其
本文向大家介绍Spring中@Autowire注入的深入讲解,包括了Spring中@Autowire注入的深入讲解的使用技巧和注意事项,需要的朋友参考一下 一直在思考spring的@Autowire注入属性时到底是按类型注入还是按名称注入,今天写了一个测试来证明一下。 定义接口TestService 定义接口实现:TestServiceImpl1和TestServiceImpl2 定义一个bean
本文向大家介绍python3解析库pyquery的深入讲解,包括了python3解析库pyquery的深入讲解的使用技巧和注意事项,需要的朋友参考一下 前言 pyquery是一个类似jquery的python库,它实现能够在xml文档中进行jQuery查询,pyquery使用lxml解析器进行快速在xml和html文档上操作,它提供了和jQuery类似的语法来解析HTML文档,支持CSS选择器,使
大家最近肯定有这么一个感受——只要一上网,ChatGPT就高频率地映入你的眼帘,人们会忍不住了解这款人工智能有多神、有多火。那对有的人来说,它是款“神器”,标志着人工智能向前迈进了“历史级”的一大步,对有的人来说,它是个“幽灵”,让人恐慌、担忧,一说就是害怕自己被机器取代。