大家好呀,今天想跟大家聊聊Java里的AQS以及相关的锁机制,都是些平时开发中可能会用到的知识点。

        首先,什么是AQS呢?其实它就是AbstractQueuedSynchronizer的缩写,是Java里一个很核心的线程同步框架。像我们常用的Lock、Semaphore、ReentrantLock这些线程同步的类,底层都是基于AQS实现的。它主要负责原子式地管理同步状态,还能处理线程的阻塞和唤醒,并且提供了等待队列的模型,帮我们搞定线程同步的问题。

        接下来聊聊ReentrantLock,它是可重入锁,意思就是一个线程能对一个临界资源重复加锁。那它和我们更熟悉的Synchronized有啥不一样呢?

        从锁的实现机制来说,ReentrantLock是基于AQS的,而Synchronized是基于监视器Monitor。获取锁的时候,ReentrantLock可以用tryLock()方法尝试获取,更灵活;Synchronized就是线程抢占的模式。释放锁方面,ReentrantLock必须显式地用unlock()释放,Synchronized则是自动释放的。锁类型上,ReentrantLock支持公平锁和非公平锁,Synchronized就只是非公平锁。不过它们有个共同点,都是可重入的。

         ReentrantLock的公平锁和非公平锁底层都是基于AQS实现的。先看非公平锁的加锁流程,它会先尝试用CAS把同步状态State从0改成1,如果成功了,就把当前线程设为独占线程,也就是获取锁成功;要是失败了,就进入acquire(1)方法接着处理。而公平锁呢,就直接进入acquire()方法。不管是公平锁还是非公平锁,加锁流程虽然有差异,但都会用到acquire()方法。这个方法是AQS里的核心方法,能提供排队等候机制,让没拿到锁的线程继续等待,还有尝试获取锁的机制。ReentrantLock里的公平锁和非公平锁通过父类Sync间接继承了AQS,所以才能基于AQS实现锁的功能。

         再深入了解下AQS的原理。AQS的核心思想是,如果请求的共享资源空闲,就把当前请求资源的线程设为有效工作线程,把共享资源设为锁定状态;要是共享资源被占用了,就通过等待机制来分配锁。这个等待机制主要靠CLH队列实现,这是一种虚拟双向队列,没拿到锁的线程会被封装成队列的节点,排队等着。AQS里用一个被volatile修饰的int类型变量state表示同步状态,修改这个状态的时候会用CAS来保证原子性。

        AQS的框架大概分五层,自定义同步器接入的时候,只要重写第一层需要的部分方法就行,不用管底层实现。当进行加锁或解锁操作时,会先经过第一层的API进入AQS内部方法,再到第二层获取锁,要是获取失败,就进入第三层和第四层处理等待队列,这些都依赖第五层的基础数据。

         AQS里最基本的数据结构是Node,也就是CLH变体队列里的节点。Node里有waitStatus(节点在队列中的状态)、thread(节点对应的线程)、prev(前驱指针)、predecessor(返回前驱节点)、nextWaiter(指向下一个CONDITION状态的节点)、next(后继指针)这些属性。waitStatus有不同的值,0是初始化的默认值,1表示线程获取锁的请求取消了(CANCELLED),-2表示节点在等待队列中(CONDITION),-3在SHARED模式下使用(PROPAGATE),-1表示线程准备好了,就等资源释放(SIGNAL)。线程的锁模式有两种,SHARED是共享模式等待锁,EXCLUSIVE是独占模式等待锁。

        AQS里的state字段很重要,它表示同步状态,用volatile修饰保证线程可见性。有三个常用方法,getState()获取状态值,setState()设置状态值,compareAndSetState()用CAS方式更新状态。我们就是通过修改这个状态来实现多线程的独占模式和共享模式下的加锁。

        自定义同步器实现时,AQS提供了很多Protected方法。ReentrantLock作为独占锁,实现了tryAcquire(独占方式尝试获取资源)、tryRelease(独占方式尝试释放资源)等方法。一般来说,自定义同步器要么是独占方式,要么是共享方式,选一种实现对应的方法就行。

         ReentrantLock的加锁过程是这样的:通过lock()方法加锁,会调用内部类Sync的Lock方法,因为Sync的lock是抽象方法,所以会根据选择的公平锁或非公平锁,执行相应内部类的Lock方法,本质上都会执行AQS的Acquire方法。AQS的Acquire方法会调用tryAcquire方法,这个方法由ReentrantLock里的公平锁和非公平锁内部类实现,所以会根据锁类型执行不同的tryAcquire。要是获取锁失败了,后面的逻辑就由AQS框架处理,和ReentrantLock的自定义同步器无关了。

         解锁过程呢,通过unlock()方法,会调用内部类Sync的Release方法,这个方法继承自AQS。Release里会调用tryRelease方法,这个方法在ReentrantLock的Sync里实现,所以释放锁不区分公平锁和非公平锁。释放成功后,后续处理也由AQS框架完成。

         当执行acquire(1)时,如果tryAcquire获取锁失败,就会调用addWaiter把线程加入等待队列。addWaiter方法会用当前线程和锁模式新建一个节点,然后把节点加入队列。具体是让节点的prev指针指向尾节点,再用compareAndSetTail方法设置尾节点,如果失败了就调用enq方法。

        加入等待队列后,acquireQueued方法会让队列里的线程不断尝试获取锁,直到成功或者被中断。这个方法里有个自旋的过程,线程会检查自己的前驱节点是不是头节点,如果是,就尝试获取锁,成功了就把自己设为头节点;如果不是,就判断是否要阻塞,避免无限循环浪费资源。

        总结几个关于AQS和ReentrantLock的问题吧。某个线程获取锁失败后,会进入排队等候机制,继续等待获取锁的机会。排队的队列是CLH变体的FIFO双端队列。队列里的线程会通过acquireQueued方法不断尝试获取锁,直到成功或中断。要是一直拿不到锁,线程所在节点的状态会变成取消状态,然后从队列中释放。Lock函数通过Acquire方法加锁,Acquire会调用由自定义同步器实现的tryAcquire方法来完成加锁。

        最后再说说ReadWriteLock。ReentrantLock保证只有一个线程能执行临界区代码,但有时候这种保护有点过头。比如有些场景下,我们希望多个线程能同时读,只有写的时候才独占。ReadWriteLock就能解决这个问题,它保证只允许一个线程写入(其他线程不能写也不能读),没有写入时,多个线程可以同时读。

        用ReentrantReadWriteLock的话,读操作加读锁,写操作加写锁。读锁之间是允许同时获取的,读和写、写和写之间都是互斥的。这样在并发读的场景下,能提高不少性能。

         比如有个Counter类,用ReentrantReadWriteLock的话,inc方法(写操作)加写锁,get方法(读操作)加读锁。这样多个线程可以同时调用get方法读取数据,只要没有线程在调用inc方法写数据就行;而当有线程调用inc方法时,其他线程无论是读还是写,都得等着。

        以上就是关于AQS、ReentrantLock和ReadWriteLock的一些知识点整理,希望对大家有帮助~

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐