原子类与自旋锁原理初探
Posted 若曦`
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了原子类与自旋锁原理初探相关的知识,希望对你有一定的参考价值。
1. 原子性
原子性是指在一个操作中就是cpu不可以在中途暂停然后再调度,既不被中断操作,要不执行完成,要不就不执行
也就是一个线程在某个代码块的执行过程中,不能被其他线程抢用cpu资源而导致中断
2. 原子变量
此篇博客为介绍原子类的部分原理为主
有关原子类的详细介绍可以看这位大佬的博客 Java16个原子类介绍-基于JDK8
原子变量属于原子类,本质是对一些基本类型包装类的升级
原子类中的原子变量,采用了以下两点保证可见性和原子性
- volatile 修饰内部的值保证可见性
- 使用CAS算法保证数据的原子性
(1) 内部的CAS
以AtomicInteger原子变量的getAndAdd(int dalta)方法为例
CAS算法是对于并发操作共享数据的支持
CAS算法的3个操作数
- 内存值 (V)
- 预估值 (A)
- 更新值 (B)
仅当V==A的时候,V=B,否则就不进行任何操作
- 比较和替换是一步原子性操作,不能被其他线程抢用
(2) Unsafe类
原子类中的CAS算法本质是调用Unsafe类中的native本地方法去对数据进行操作(直接调用c++对内存进行操作,所以效率也会比较快)
(3) CAS的缺点
CAS算法虽然能实现自旋锁,但是有以下3个缺点
- 循环会耗时
- 一次性只能保证一个共享变量的原子性
- ABA问题
(4) 乐观锁
乐观锁有两种实现:自旋锁和版本比对
自旋锁
自旋锁也就是使用普通的CAS算法,在后序章节会有详解
自旋锁容易出现上述的ABA问题 也就是数据是期望的,但是是被其他线程动过的数据,比如将2020->2021->2020,虽然最后还是2020,但已经被动过了
版本比对
比如一个线程执行任务,就会比对数据的版本号,如果版本号没有被其他线程动过,那么才对数据进行操作
3. 原子引用
原子引用指的也就是AtomicReference,作用是对"对象"进行原子操作。
它提供了一种读和写都是原子性的对象引用变量,同样采用CAS,不过内存值和预估值是比较对象的地址
(1) AtomicReference和AtomicInteger的差异
-
AtomicInteger是对整数的封装,底层采用的是compareAndSwapInt实现CAS,比较的是数值是否相等
-
AtomicReference则对应普通的对象引用,底层使用的是compareAndSwapObject实现CAS,比较的是两个对象的地址是否相等。
综上所述,原子引用能够保证你在修改对象引用时的线程安全性。
(2) 解决ABA问题
解决ABA问题,可以采用自旋锁+版本号来解决
自旋锁+版本(时间戳)比对
源码分析
使用示例
public class TestAtomic {
public static void main(String[] args) {
// 自旋锁+版本号的原子类 构造函数的Stamp 1 代表版本号(或者说是时间戳)
// 注意参数传入值类型的时候会自动装箱 int->Integer 如果Ref的值(第一个参数)设置过大 超出Integer指定范围(-128~127之间) Integer会在堆上new一块新的内存地址,导致比对不正确
AtomicStampedReference<Integer> stampedReference = new AtomicStampedReference(100,1);
new Thread(()->{
//记录当前的num值
int num=100;
//比较并交换值
stampedReference.compareAndSet(num,++num,1,2);
//获取当前版本号:getStamp()
int stamp = stampedReference.getStamp();
if(num==101) {
System.out.println("a=>将值更新为了101,"+"当前版本号为:"+stamp);
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//比较并交换值
stampedReference.compareAndSet(num,++num,2,3);
//获取当前版本号:getStamp()
stamp = stampedReference.getStamp();
if(num==102) {
System.out.println("b=>将值更新为了102,"+"当前版本号为:"+stamp);
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//比较并交换值
stampedReference.compareAndSet(num,++num,3,4);
//获取当前版本号:getStamp()
stamp = stampedReference.getStamp();
if(num==103) {
System.out.println("c=>将值更新为了103,"+"当前版本号为:"+stamp);
}
}).start();
}
}
注意点,也就是当预期值或者说版本号是int类型,且AtomicStampedReference泛型指定为Integer的时候,要注意值的大小
4. 自旋锁
自旋锁本身是靠CAS算法实现的,与Lock和synchronized无关
CAS算法是对于并发操作共享数据的支持,在目录2中有更具体的说明
CAS算法的3个操作数
- 内存值 (V)
- 预估值 (A)
- 更新值 (B)
- 也就是仅当V==A的时候,V=B,否则就不进行任何操作
- 比较和替换是一步原子性操作,不能被其他线程抢用
通过以上两点特性实现的自旋锁
自己实现一个自旋锁
/**
* 自己实现一个自旋锁 (原子引用+CAS)
* @author ruoxi
*/
public class My_CAS_Lock {
/**
* 使用原子引用,指定泛型类型为Thread
*/
static AtomicReference<Thread> atomicReference = new AtomicReference<>();
/**
* CAS算法实现自旋锁的加锁
*/
public void myLock() {
//获取当前线程
Thread thread = Thread.currentThread();
//CAS算法实现 自旋锁
//预期值:thread=null 读取内存中的值:当前线程的引用 thread = this.thread
//如果当前线程不是空,则不能获取锁(说明有线程拿到锁了),开始一直自旋
while(!atomicReference.compareAndSet(null,thread)){
try {
//使线程每次自旋间隔为1毫秒
TimeUnit.MILLISECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(thread.getName()+"=>在等待解锁..自旋中..");
}
//加锁 (其实只是利用CAS算法将其他线程隔离了)
System.out.println(thread.getName()+"拿到了锁");
}
/**
* CAS算法实现自旋锁的解锁
*/
public void unlock(){
Thread thread = Thread.currentThread();
System.out.println(thread.getName()+"释放了锁");
//将当前线程置换为空,也就是释放了锁
atomicReference.compareAndSet(thread,null);
}
/**
* 测试
*/
public static void main(String[] args) {
//实例化一个自己实现的自旋锁
My_CAS_Lock lock = new My_CAS_Lock();
//开启两个线程,都需要获取自旋锁才能执行
for (int i = 0; i < 2; i++) {
new Thread(()->{
//上锁
lock.myLock();
try {
//让线程沉睡10毫秒再解锁
TimeUnit.MILLISECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
//解锁
lock.unlock();
}
},"线程"+(i+1)).start();
}
}
}
以上是关于原子类与自旋锁原理初探的主要内容,如果未能解决你的问题,请参考以下文章
synchronized实现原理及其优化-(自旋锁,偏向锁,轻量锁,重量锁)