浅谈AQS锁实现机制(含ReentrantReadWriteLock读写锁加锁解锁相关源码分析)

Posted 默辨

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了浅谈AQS锁实现机制(含ReentrantReadWriteLock读写锁加锁解锁相关源码分析)相关的知识,希望对你有一定的参考价值。





一、分析AQS锁

在Java语言层面,它拥有自己的锁实现机制(JVM层面的是synchronized)。这个锁机制就是AbstractQueuedSynchronizer类,也就是我们常说的AQS。我们常见的ReentrantLock、CountDownLatch、Semaphore、CyclicBarrier等,都是基于AQS来实现的,只是不同的并发工具类使用到的是AQS的不同特性,如ReentrantLock加锁操作使用了互斥特性,Semaphore信号量就是利用了共享的特性,但两者都是在state变量上做文章。



AQS是一个抽象类,他无法直接实例化为一个对象,他只能依附于其他工具类的实现。AQS使用了一个模板方法的设计模式,它给出了我们实现锁的规范,我们只需要按照它的规范来,就可以完成对应的加锁、解锁逻辑。

不懂模板方法设计模式的可以参考我之前的博客:模板方法模式



本文仅讨论尝试加锁(tryAcquire)和尝试解锁(tryRelease)这两个方法。我认为这是比较核心的两个方法。
为什么我说这两个方法是比较核心的方法呢?我们以ReentrantLock为例,调用lock方法为起点。更详细的过程及源码分析,请参考浅谈AQS同步队列(含ReentrantLock加锁和解锁源码分析)此处仅进行流程分析。


1、选择调用ReentrantLock中的公平锁还是非公平锁实现(在创建ReentrantLock对象的时候,就会初始化出我们的锁对象,即可以指定是公平还是非公平的锁),我们以默认的非公平锁为例

2、然后调用当前类的acquire,而该类是AQS中实现的,即会调用父类方法

3、此时会调用对应的tryAcquire方法尝试获取锁

4、该方法在AQS中是一个会报错的实现,即具体实现方法只能是具体子类

5、此时就根据ReentrantLock中的方法逻辑进行获取锁

6、解锁流程逻辑同理



总结:AQS给出了锁的实现规则,具体的锁工具类,只需要实现对应的方法即可完成一把锁的功能





二、自定义一把AQS锁

总结上面的关键点:

1、lock方法为我们加锁的入口

2、tryAcquire为我们获取锁的逻辑


于是我们就可以自定义一把锁,完成锁的基本功能,代码如下:

public class MobianLock extends AbstractQueuedSynchronizer 

	// 用于加锁的入口
	public void lock()
		acquire(1);
	
	// 用于解锁的入口
	public void unlock()
		release(1);
	

	@Override
	protected boolean tryAcquire(int arg) 
		// cas加锁 成功返回true
		if(compareAndSetState(0,1))
			setExclusiveOwnerThread(Thread.currentThread());
			return true;
		
		return false;
	

	@Override
	protected boolean tryRelease(int arg) 
		// 释放锁逻辑
		setExclusiveOwnerThread(null);
		setState(0);
		return true;
	



测试结果:

锁效果成功





三、分析ReentrantReadWriteLock锁源码

ReentrantReadWriteLock锁的使用不再讲解

现实中有这样一种场景:对共享资源有读和写的操作,且写操作没有读操作那么频繁(读多写少)。在没有写操作的时候,多个线程同时读一个资源没有任何问题,所以应该允许多个线程同时读取共享资源(读读可以并发);但是如果一个线程想去写这些共享资源,就不应该允许其他线程对该资源进行读和写操作了(读写,写读,写写互斥)。在读多于写的情况下,读写锁能够提供比排它锁更好的并发性和吞吐量。

针对这种场景,JAVA的并发包提供了读写锁ReentrantReadWriteLock,它内部,维护了一对相关的锁,一个用于只读操作,称为读锁;一个用于写入操作,称为写锁,描述如下:

线程进入读锁的前提条件:

  • 没有其他线程的写锁
  • 没有写请求或者有写请求,但调用线程和持有锁的线程是同一个。

线程进入写锁的前提条件:

  • 没有其他线程的读锁
  • 没有其他线程的写锁

而读写锁有以下三个重要的特性:

  • 公平选择性:支持非公平(默认)和公平的锁获取方式,吞吐量还是非公平优于公平。
  • 可重入:读锁和写锁都支持线程重入。以读写线程为例:读线程获取读锁后,能够再次获取读锁。写线程在获取写锁之后能够再次获取写锁,同时也可以获取读锁。
  • 锁降级:遵循获取写锁、再获取读锁最后释放写锁的次序,写锁能够降级成为读锁。



ReentrantReadWriteLock的类图结构



根据ReentrantReadWriteLock类的情况可以两部分

  1. WriteLock和ReadLock为一部分:关键的读写锁逻辑
  2. Sync为一部分:维护锁的状态信息



1、Sync

直接继承AQS,在AQS的基础上,添加了一些要素

这里涉及了很多的位运算,其原因都要归咎于在ReentrantReadWriteLock锁的内部,它是将一个int类型的state分成16高位+16低位来处理的。

这个状态数字的判断,在后面的读写锁加锁过程中会经常使用到。

总结:高16位为读锁状态标志位,低16位为写锁状态标志位




2、ReadLock

读锁部分,依然可以分为lock和unlock方法,它们分别调用的是acquireShared和releaseShared方法。想都不用想,最终肯定是调用到tryAcquireShared和tryReleaseShared方法

public void lock() 
    sync.acquireShared(1);


public void unlock() 
    sync.releaseShared(1);



加锁逻辑



解锁逻辑

3、WriteLock

同理,会调用到tryAcquire和tryRelease方法

public void lock() 
    sync.acquire(1);


public void unlock() 
    sync.release(1);



加锁逻辑



解锁逻辑

以上是关于浅谈AQS锁实现机制(含ReentrantReadWriteLock读写锁加锁解锁相关源码分析)的主要内容,如果未能解决你的问题,请参考以下文章

浅谈如何使用Redis实现分布式锁

AQS原理与源码

AQS:Java 中悲观锁的底层实现机制

AQS机制

java-基于AQS实现锁

Java锁机制4.0