抽象同步器AQS、CAS应用之--ReentrantLock,lock和unlock的流程、源码分析

本文深入解析了AQS(AbstractQueuedSynchronizer)和CAS(Compare-and-Swap)的原理,探讨了它们在Java多线程环境中的应用,包括ReentrantLock与synchronized的区别,ReentrantLock的内部结构及工作流程,以及如何预防死锁。

多线程重点:各种锁的区别!

1. AQS和CAS

多线程中经常听到AQS和CAS,他们究竟是什么呢?

1.1 AQS:
        AbstractQueuedSynchronizer(AQS)抽象队列同步器,AQS定义了一套多线程访问共享资源的同步器框架,许多同步类实现都依赖于它,如常用的ReentrantLock/Semaphore/CountDownLatch等。在实现AQS时,一般通过定义内部类Sync继承AQS,将同步器所有调用都映射到Sync对应的方法来处理。

AQS定义两种资源共享方式:

  • ①:独占:只有一个线程能执行,如ReentrantLock

    static final Node EXCLUSIVE = null;
    
  • ②:共享:多个线程可以同时执行,如Semaphore/CountDownLatch

    static final Node SHARED = new Node();  
    

AQS定义两种队列:

  • ①:同步等待队列AQS当中的同步等待队列也称CLH队列,CLH队列是Craig、Landin、Hagersten三人 发明的一种基于双向链表数据结构的队列,是FIFO先入先出线程等待队列

            Node(Thread thread, Node mode) {
         
              // Used by addWaiter
                this.nextWaiter = mode;
                this.thread = thread;
            }
    

同步等待队列的进入时机:

  • 当前线程获取锁失败后,同步器会将线程构建成一个节点,并将其加入同步队列中。

  • 通过signalsignalAll将条件队列中的节点转移到同步队列。(由条件队列转化为同步队列)在这里插入图片描述

  • ②:条件等待队列Condition是一个多线程间协调通信的工具类,使得某个,或者某些线程一起等待某个条件(Condition),只有当该条件具备时,这些等待线程才会被唤醒,从而重新争夺锁

            Node(Thread thread, int waitStatus) {
         
          // Used by Condition
                this.waitStatus = waitStatus;
                this.thread = thread;
            }
    

    在这里插入图片描述

条件等待队列的进入时机:

  • 调用await方法阻塞线程;

注意一个线程只能存在于两个队列中的一个
 

AQS四大核心原理

  • 队列元素自旋获取锁:阻塞的线程通过自旋不断尝试加锁,加锁成功则跳出循环
  • LockSupport:通过LockSupport.park()LockSupport.unpark()
  • CAS:无锁算法,功能类似于Synchronized
  • 队列:保存每个阻塞线程的引用

=========================================================


1.2 CAS

         compare and swap(CAS),比较与交换。作用:不管并发有多高,都能保证操作的原子性,保证锁的互斥性。CAS是保证并发安全性的一条CPU底层原子指令cmpxchgx86框架下),它的功能是判断某个值是否为预期值,如果是的话,就改为新值,在CAS过程中不会被中断。

         但CPU底层原子指令cmpxchg仅仅能保证原子性,如果java中的CAS只有这项功能肯定是不行的!因为并发安全需要原子性、一致性、可见性都得到保证,java中的compareAndSwapXxx方法其实在jvm底层又增加了Lock前缀去保证其他并发安全特性的!所以在java中可以使用CAS达到SychronizedLock同样的功能

        然而在Java发展初期,并不能直接利用硬件提供的并发来提升系统的性能的,随着java的发展,CAS理论成为实现java并发的一种常用手段。

CAS包含三个参数:

  • ①:偏移量:要更新的变量(V)在内存中的偏移量
  • ②:预期值 (A)
  • ③:新值(B)

什么是偏移量:从堆内存中对象头的Mark Word(起始位置)开始数,直到存储该成员变量V的起始值,这个起始值被称为当前对象的成员变量V的偏移量,也就是内存中记录V的位置,用于cpu寻址,进而进行cas中的比较!!

        如果内存位置的值与预期值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该位置的值

        对于某个对象中的变量V,如果想要通过CAS无锁且安全的方式修改这个变量V的值,具体的逻辑如下:

  • 通过cpu寻址找到该变量V在对象中的偏移量,拿到V的原始值。
  • V的原始值和预期值(A)作比较
  • 如果当前值和预期值一样,则修改V的值为新值(B)
  • 如果当前值和预期值不一样,则不能修改V的原始值,可以选择自旋,重新获取V的值,直到V的值等于预期值(A)才可以对V进行修改!

如以上逻辑,通过CAS操作,保证整个操作过程是原子的,任意时刻只有一个线程执行成功!

cas的实现:

主要是使用的Unsafe类的这三个native方法,再底层就是调用的汇编的原子指令cmpxchg(),依赖于硬件完成!

/**
* Object var1:对象
* long var2:对象中某变量的偏移量
* int var4:期望值
* int var5:想要修改的值
*/
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);

public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);

        
        

1.1 CAS存在的bug:ABA问题

        因为CAS需要在操作值的时候,检查值有没有发生变化,如果没有发生变化则更新,但是如果一个值 原来是A,变成了B,然后又变成了A,那么此时使用CAS进行检查时会发现它的值没有发生变化,依旧是A,最终结果没有变。但是实际上,A却经历了A-B-A的过程,这就是CAS存在的ABA问题!

ABA问题示例

//要修改的变量
private static AtomicReference<Integer> atomicReference = new AtomicReference<Integer>(100);

public static void main(String[] args) {
   
   
	//线程一 修改后又恢复原样
	new Thread(() -> {
   
   
		atomicReference.compareAndSet(100, 101);
		atomicReference.compareAndSet(101, 100);
	},"t1").start();
	
	//线程二:还可以修改,无法解决ABA问题
	new Thread(() -> {
   
   
		try {
   
   
			//睡一秒,让线程一先修改
			TimeUnit.SECONDS.sleep(1);
		} catch (InterruptedException e) {
   
   
			e.printStackTrace();
		}
		System.out.println(atomicReference.compareAndSet(100, 2019) + "\t修改后的值:" + atomicReference.get());
	},"t2").start();
}
  • 初始值为100,线程t1将100改成101,然后又将101改回100
  • 线程t2先睡眠1秒,等待t1操作完成,然后t2线程将值改成2019
  • 可以看到,线程2修改成功
  • 输出结果:true 修改后的值:2019

ABA问题的危害

        一般场景下,使用CAS处理一些数值的简单计算,ABA并不会出现什么问题,但是当涉及到引用的时候就会出问题。

比如:使用CAS导致栈结构变化

  • 现有一个用单向链表实现的堆栈,栈顶为A,这时线程T1已经知道A.nextB
    在这里插入图片描述
  • 然后希望用CAS将栈顶替换为B,使栈中元素为B、Bhead.compareAndSet(A,B)
  • T1执行上面这条指令之前,由于T1还没有进入CAS,存在线程争抢!此时线程T2介入,将A、B出栈,再pushD、C、A,此时堆栈结构如下图,而对象B此时处于游离状态:
    在这里插入图片描述
  • 此时轮到线程T1执行CAS操作,检测发现栈顶仍为A,所以CAS成功,栈顶变为B,但实际上B.nextnull,所以此时的情况变为:
    在这里插入图片描述
  • 最后堆栈中只有B一个元素,CD组成的链表不再存在于堆栈中,平白无故就把C、D丢掉了。

ABA解决方案

        使用版本号。在变量前面追加版本号,每次变量更新的时候把版本号+1,那么A->B->A 就会变成1A->2B->3A,在进行CAS比较时,即使值相同,也不应该修改成功,还要比对版本号!从JDK1.5 开始,JDK的Atomic包里提供了一个类AtomicStampedReference来解决ABA问题。AtomicStampedReference除了定义值 ,还可以定义版本号

// AtomicStampedReference定义值为100 ,版本号为1
private static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<Integer>(100,1);

public static void main(String[] args) {
   
   
	//线程一
	new Thread(() -> {
   
   
		System.out.println("t1拿到的初始版本号:" + atomicStampedReference.getStamp());
		
		//睡眠1秒,是为了让t2线程也拿到同样的初始版本号
		TimeUnit.SECONDS.sleep(1);
		
		//干坏事,修改原始值	
		atomicStampedReference.compareAndSet(100, 101,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
		atomicStampedReference.compareAndSet(101, 100,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
	},"t1").start();
	
	//线程二
	new Thread(() -> {
   
   
		int stamp = atomicStampedReference.getStamp();
		System.out.println("t2拿到的初始版本号:" + stamp);
		
		//睡眠3秒,是为了让t1线程完成ABA操作
		TimeUnit.SECONDS.sleep(3);
	
		System.out.println("最新版本号:" + atomicStampedReference.getStamp());
		System.out.println(atomicStampedReference.compareAndSet(100, 2019,stamp,atomicStampedReference.getStamp() + 1) + "\t当前 值:" + atomicStampedReference.getReference());
	},"t2").start();
}
  • 初始值100,初始版本号1
  • 线程t1和t2拿到一样的初始版本号1
  • 线程t1完成ABA操作,版本号递增到3
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值