ReentrantReadWriteLock读写锁的lock、unlock源码分析

先说一下结论,再分析源码,ReentrantReadWriteLock拆分了两把锁,一把读锁ReadLock,一把写锁WriteLock,读锁是共享的,写锁是独占的;也就是说,如果有线程在读,其他线程也可以读,但是不能写;如果有线程在写,则其他线程不能读也不能写。

lock

ReadLock

一、tryAcquireShared 获取共享锁

先来看第一个方法tryAcquireShared获取共享锁

主要分四部分,介绍这四部分之前先来介绍一点东西,AQS里边只有一个state来记录锁的状态,那怎么区分读锁和写锁呢,这里用到了二进制。将state转化为二进制,有32位,左边高16位记录读锁数量,包括重入的读锁数量,右边低16位记录写锁数量,包括重入的写锁数量。

下边来分析这四步

1、判断是否有写锁,若有,且不是自己,则直接返回,加入队列去排队。

2、readerShouldBlock是否要阻塞读锁请求,这里主要分公平锁和非公平锁,若不需要阻塞,且读锁数量没有达到上限,则通过CAS尝试获取锁,获取失败就走第4步(其实也是要加入对列,后续分析到第四步再说)

       readerShouldBlock在公平锁和非公平锁中有两种实现,下边看一下这个源码

 

 可以看出,公平锁中判断是否阻塞读锁请求,就是看队列中是否有阻塞线程(hasQueuedPredecessors这个方法就是判断队列中是否有阻塞线程的,我在ReentrantLock中有分析这段代码,就不在这里展开了,有兴趣的可以直接跳转 ReentrantLock的lock、unlock源码分析) 

非公平锁中判断是否阻塞读锁请求,就是看队列中是否有等待的写锁线程

3、第三步看起来很复杂,其实很简单,就是获取到锁之后的一些操作,这里包括了锁的重入,只是区分了一下本次进入的是不是第一次进入那个线程(至于这里为什么把第一个线程和后续线程区分开我们在文章末尾时讨论)

4、fullTryAcquireShared其实就是判断一下是不是锁重入,不是直接去排队,是的话竞争一下锁,竞争到就改一下重入次数等信息,和上边方法很多重复的,就不把代码展开了

 4.1:判断是否有写入锁,有的话再判断是不是当前线程,不是的话直接返回去排队(因为是多线程操作,方法外虽然已经判断过了,但是执行到这里还是要再判断一下,防止这期间有新的写锁抢到锁进入)

4.2:判断是否阻塞读锁线程(readShouldBlock这个方法在前边有分析,这里不再分析),如果需要阻塞,就进入,这里主要判断了下是不是锁重入,不是重入的话就返回去排队;是锁重入的话就走下边去获取锁

4.3:如果获取锁成功,就改一下cacheHoldCounter(cacheHoldCounter缓存最近一次获取锁的线程及获取锁的次数),如果是锁重入就修改一下重入次数。

 二、doAcquireShared 加入队列

1:给当前队列创建node节点加入到队列尾部

2:判断node节点的上一节点是不是head节点

        2.1:是的话重新获取一下锁,针对这个方法前边已经展开分析过。

        2.2:获取成功就把当前节点设置为head节点(这里边还涉及到很多东西,后边展开分析)

3:上一节点不是头节点的话,修改上一节点的waitStatus 为-1,就是告诉上一节点,锁释放后通知我。

4:调用线程的park方法,将自己阻塞掉。

1、3、4方法我在ReentrantLock中有做详细剖析,方法一模一样的,这里留个传送门,就不展开分析了。

来看一下2.2中的setHeadAndPropagate方法

上边是将自己设置为头节点,没什么好说的,主要是下边这一块了,如果head的下一个节点为空,或者下一个节点是共享锁,就把后续的线程唤醒。

 这里就比较简单,先自己的waitStatus的值,一直改,直到改成功为止,如果原来是-1,表明需要唤醒后续线程,就去做唤醒操作。

 唤醒操作就是如果head的下一个节点为空,或者waitStatus>0 (表明线程被取消了)就从队伍末尾开始往前遍历,找到离head节点最近的一个阻塞线程,调用unpark方法将线程唤醒。

unlock

 

释放锁的过程相对简单,第二步中的doReleaseShared唤醒后续线程上边已经分析过,不再分析。

tryReleaseShared的源码

上边一部分就是将锁的数量减1,只是区分了一下是不是第一个线程,因为第一个线程的重入次数和后续线程的重入次数是分开统计的;减完以后就通过CAS方式修改锁的数量,循环直到修改成功为止,修改成功后,如果锁的数量等于0了,表明锁全部被释放,可以去唤醒后续节点线程。不等于0表明锁还没有被全部释放(涉及到重入,加几次锁就需要释放几次),那么就不去唤醒后续线程。 

这里提一下 SHARED_UNIT这个东西,SHARED_UNIT是 1左移16位获取的值,因为我们的读锁是放在高16位的,所以要减去这个值才能获取到真正的锁的数量。

最后来分析一下前边提到的为什么要单独记录一下第一个进入的线程?

这个东西是ReentrantReadWriteLock作者后续单独加上的代码,之前都是通过ThreadLocal计数的,加这个东西猜测是为了提高性能,因为大多时候是单线程操作的,单线程操作我还要放入ThreadLocal中,每次都要去ThreadLocal中查找,比较影响性能。关于这个问题,感兴趣的同学可以看下这篇文章:关于ReentrantReadWriteLock,首个获取读锁的线程单独记录问题讨论

总结:

ReadLock

获取锁前先判断是否有线程在写,有的话则直接加入队列,等待被唤醒。没有写锁的话就尝试获取锁,获取锁的方式分公平锁和非公平锁。

公平锁:先判断队列中是否有阻塞线程,若有,则加入后续队列,若没有则尝试获取锁,获取失败就加入队列。

非公平锁:先判断队列中是否有写锁线程,若有,则加入后续队列,等待被唤醒。若没有则尝试获取锁,获取失败就加入队列。

WriteLock

获取锁前先判断是否有锁(读锁或写锁),有的话再判断是不是自己在写,是的话将锁数量加1(就是写锁的重入),不是自己在写的话则直接加入队列,等待被唤醒。如果没有锁的话就尝试获取锁,获取锁的方式分公平锁和非公平锁。

公平锁:先判断队列中是否有阻塞线程,若有则加入队列,等待被唤醒,没有阻塞线程就尝试获取锁,获取失败则加入队列,等待被唤醒。

非公平锁:不管队列中是否有阻塞线程,只要锁是可用的,就直接去抢,抢到就用,抢不到就加入队列,等待被唤醒。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
在 Java 中,你可以使用 `java.util.concurrent.locks` 包中的 `ReadWriteLock` 接口来实现。下面是一个使用的示例代码: ```java import java.sql.*; import java.util.concurrent.locks.*; public class SQLiteTest { private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock(); private final Lock readLock = rwLock.readLock(); private final Lock writeLock = rwLock.writeLock(); public void readFromDatabase() throws Exception { readLock.lock(); try { Class.forName("org.sqlite.JDBC"); Connection connection = DriverManager.getConnection("jdbc:sqlite:/path/to/your/database"); Statement statement = connection.createStatement(); ResultSet resultSet = statement.executeQuery("SELECT * FROM your_table"); // do something with the result set... } finally { readLock.unlock(); } } public void writeToDatabase() throws Exception { writeLock.lock(); try { Class.forName("org.sqlite.JDBC"); Connection connection = DriverManager.getConnection("jdbc:sqlite:/path/to/your/database"); Statement statement = connection.createStatement(); statement.executeUpdate("INSERT INTO your_table (column1, column2) VALUES ('value1', 'value2')"); } finally { writeLock.unlock(); } } } ``` 在这个示例代码中,我们使用 `ReentrantReadWriteLock` 类来创建。`readLock` 用于操作,`writeLock` 用于操作。在操作中,我们首先获取,然后执行查询操作。在操作中,我们首先获取,然后执行插入操作。 使用可以确保在同一时间只有一个线程可以进行操作,而多个线程可以同时进行操作。这可以提高程序的并发性能和稳定性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值