详解 —— 常见锁策略及ABA问题
目录
一、常见锁策略
一般来说,锁策略和普通的程序员基本没啥关系,跟实现锁的人有紧密的联系。这里提到的锁策略也不是局限于Java这门语言的,是适用于所有跟锁相关的情况的。
1、悲观锁VS乐观锁
悲观锁:预期锁冲突的概率很高
乐观锁:预期锁冲突的概率很低
悲观锁需要做的事情更多,那么效率也越低。乐观锁则刚好相反,做的事更少,效率更高
2、读写锁VS普通互斥锁
普通互斥锁:只有两个操作,加锁和解锁;只要两个线程针对同一个对象加锁,就会产生互斥。
读写锁:分成了三个操作:加读锁,加写锁,解锁;其中读锁和读锁之间是不会产生互斥关系的,而读锁和写锁以及写锁和写锁之间才会产生互斥关系。
3、重量级锁VS轻量级锁
重量级锁和轻量级锁的概念跟悲观乐观锁是有一定的重叠的,我们可以这样认为:乐观悲观锁是看待问题的态度,而这个态度决定的结果是重量级和轻量级锁。
重量级锁就是做了更多的东西,开销更大
轻量级锁做的事更少,开销更少
一般悲观锁都是重量级锁,乐观锁一般都是轻量级锁(当然这也不是绝对的)
在使用的锁当中,如果该锁是基于内核的一些功能来实现的,此时一般认为该锁是重量级锁;如果该锁是纯用户态(用户态代码更可控,也更高效)实现的,一般认为这把锁是轻量级锁。
4.挂起等待锁VS自旋锁
挂起等待锁往往是通过内核的一些机制来实现的,一般都比较重(重量级锁的典型实现);而自旋锁往往是通过用户态代码来实现的,一般较轻(轻量级锁的经典实现)。
5、公平锁VS非公平锁
公平锁:遵循先来后到的原则
非公平锁:不讲究先来后到,不管是不是先来的,获取这把锁的概率是相同的
6、可重入锁VS不可重入锁
同一个线程针对同一把锁连续加锁两次,如果不会造成死锁的就是可重入锁;反之就是不可重入锁。
小结
以synchronized来举例:
1.既是一个乐观锁也是一个悲观锁(根据锁冲突的激烈程度自适应)
2.不是读写锁,只是一个普通的互斥锁
3.既是重量级锁也是轻量级锁(同1)
4.轻量级的部分基于自旋锁,重量级部分基于挂起等待锁
5.是非公平锁
6.是可重入锁
二、CAS
CAS是compre and swap的缩写,我们假设内从中的原数据V,旧的预期值A,需要修改的值B:
1.比较A与V是否相同
2.如果相同则将B写入V
3.返回操作是否成功
此处的CAS是CPU提供的一条指令,通过这一条指令(原子操作)就完成了上述的三个步骤。
1、CAS的作用
1.基于CAS可以实现一些原子类:Java标准库中提供了一组原子类,针对常用的例如:int、long....进行了封装,可以基于CAS进行修改,并且线程安全
2.基于CAS实现自旋锁
2、ABA问题
CAS中的关键就是先比较再交换。这里的比较其实是在比较当前值和旧值是否相同。如果这两个值相同就会认为中间并没有发生过变化。但是这里其实是有bug的,当前值和旧值相同,可能中间并没有发生变化,但还有一种极端情况就是中间变过,然后又变回来了。
举个例子:
假设滑稽去取款机取钱,账户上有100,滑稽想要取出50.但是当他按下取款按钮时取款机卡了一下,于是他又按了一下取款(一次取钱操作执行了两次,我们预期的效果是取走50还剩50),下面我们来分析一下执行的过程:
1. t1线程将账户余额读取至寄存器与原值100比较
2. t1发现原值与账户余额相等
3. 执行CAS操作将账户余额修改为50,返回取款成功
4. t2线程将账户余额读取至寄存器与原值100比较
5. t2发现并不相同,于是啥也不干返回了FALSE
以上过程是滑稽正常取钱的情况下,但是当滑稽取钱的一刹那有一个新情况,有人给他的账户打了50块钱,这个时候问题就来了。
1. t1线程将账户余额读取至寄存器与原值100比较
2. t1发现原值与账户余额相等
3. 执行CAS操作将账户余额修改为50,返回取款成功
4. 账户被汇款50(具体就不展开了),账户余额又变成了100
5. t2线程将账户余额读取至寄存器与原值100比较
6. t2发现比较结果相同
7. 执行CAS操作将账户余额修改为50,返回取款成功
实际上这里滑稽老铁是只先要取50的,但是却执行了两次取钱操作,这就是CAS的ABA问题
那么我们该怎么去解决这个问题呢?
做法就是引入一个版本号,这个版本号只能增长不能减小,而每次比较我们比较的不再是原值而是版本号。因为版本号是不可逆的,因此也就避免了ABA问题。
三、synchronized的锁优化机制
1.锁膨胀/锁升级
这个机制体现了synchronized自适应的能力
偏向锁:一开始是处于无锁状态的,当第一个线程加锁时就会进入偏向锁的状态。但偏向锁并不是真正的加锁,而是添加了一个标记(加锁解锁也是有消耗的)
自旋锁:如果有其他线程进入了锁竞争(竞争不激烈)就会进入轻量级锁的状态
重量级锁:如果锁竞争非常激烈,那么synchronized就会进入重量级锁状态
2、锁粗化
这里的粗细是锁的粒度。加锁代码的范围越大那么锁的粒度就越粗,反之粒度就越细。
如果锁粒度粗,那么加锁解锁的开销就更小。如果锁越细,那么多个线程之间的并发性就更高。
通常编译器自动会有一个优化,如果某个地方粒度太细了那么它就会进行优化,让锁的粒度更粗一些,使代码执行更有效率。
3、锁消除
有些代码是不需要加锁的,但是你给加上了锁。那么编译器会自动将这个锁去掉,减小开销,这就叫锁消除。