J.U.C剖析与解读1(Lock的实现) (2)

tryLock:尝试获取锁,成功返回true,失败返回false。首先是获取锁的行为,可以通过CAS操作实现,或者更简单一些,通过Atomic包实现(其底层也还是CAS)。另外,由于是可重入锁,所以在尝试获取锁时,需要判断尝试获取锁的线程是否为当前锁的持有者线程。

lock:尝试获取锁,直到成功获得锁。看到这种不成功便成仁的精神,我第一个想法是循环调用tryLock。但是这实在太浪费资源了(毕竟长时间的忙循环是非常消耗CPU资源的)。所以就是手动通过LockSupport.park()将当前线程挂起,然后置入等待队列waiters中,直到释放锁操作来调用。

tryUnlock:尝试解锁,成功返回true,失败返回false。首先就是在释放锁前,需要判断尝试解锁的线程与锁的持有者是否为同一个线程(总不能线程A把线程B持有的锁给释放了吧)。其次,需要判断可重入次数count是否为0,从而决定是否将锁的持有owner设置为null。最后,就是为了避免在count=0时,其他线程同时进行加锁操作,造成的count>0,owner=null的情况,所以count必须是Atomic,并此处必须采用CAS操作(这里有些难理解,可以看代码,有相关注释)。

unlock:解锁操作。这里尝试进行解锁,如果解锁成功,需要从等待队列waiters中唤醒一个线程(唤醒后的线程,由于在循环中,所以会继续进行竞争锁操作。但是切记该线程不一定竞争锁成功,因为可能有新来的线程,抢先一步。那么该线程会重新进入队列。所以,此时的JarryReentrantLock只支持不公平锁)。

JarryReentrantLock实现

那么接下来,就根据之前的信息,进行编码吧。

package tech.jarry.learning.netease; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.LockSupport; /** * @Description: 仿ReentrantLock,实现其基本功能及特性 * @Author: jarry */ public class JarryReentrantLock implements Lock { // 加锁计数器 private AtomicInteger count = new AtomicInteger(0); // 锁持有者 private AtomicReference<Thread> owner = new AtomicReference<>(); // 等待池 private LinkedBlockingQueue<Thread> waiters = new LinkedBlockingQueue<>(); @Override public boolean tryLock() { // 判断当前count是否为0 int countValue = count.get(); if (countValue != 0){ // countValue不为0,意味着锁被线程持有 // 进而判断锁的持有者owner是否为当前线程 if (Thread.currentThread() == owner.get()){ // 锁的持有者为当前线程,那么就重入加锁 // 既然锁已经被当前线程占有,那么就不用担心count被其他线程修改,即不需要使用CAS count.set(countValue+1); // 执行重入锁,表示当前线程获得了锁 return true; }else{ // 如果当前线程不是锁的持有者,返回false(该方法是tryLock,即浅尝辄止) return false; } }else { // countValue为0,意味着当前锁不被任何线程持有 // 通过CAS操作将count修改为1 if (count.compareAndSet(countValue,countValue+1)){ // count修改成功,意味着该线程获得了锁(只有一个CAS成功修改count,那么这个CAS的线程就是锁的持有者) // 至于这里为什么不用担心可见性,其实一开始我也比较担心其发生类似doubleCheck中重排序造成的问题(tryUnlock是会设置null的) // 看了下源码,AtomicReference中的value是volatile的 owner.set(Thread.currentThread()); return true; } else { // CAS操作失败,表示当前线程没有成功修改count,即获取锁失败 return false; } } } @Override public void lock() { // lock()【不死不休型】就等于执行tryLock()失败后,仍然不断尝试获取锁 if (!tryLock()){ // 尝试获取锁失败后,就只能进入等待队列waiers,等待机会,继续tryLock() waiters.offer(Thread.currentThread()); // 通过自旋,不断尝试获取锁 // 其实我一开始也不是很理解为什么这样写,就可以确保每个执行lock()的线程就在一直竞争锁。其实,想一想执行lock()的线程都有这个循环。 // 每次unlock,都会将等待队列的头部唤醒(unpark),那么处在等待队列头部的线程就会继续尝试获取锁,等待队列的其它线程仍然,继续阻塞(park) // 这也是为什么需要在循环体中执行一个检测当前线程是否为等待队列头元素等一系列操作。 // 另外,还有就是:处于等待状态的线程可能收到错误警报和伪唤醒,如果不在循环中检测等待条件,程序就会在没有满足结束条件的情况下退出。反正最后无论那个分支,都return,结束方法了。 // 即使没有伪唤醒问题,while还是需要的,因为线程需要二次尝试获得锁 while (true){ // 获取等待队列waiters的头元素(peek表示获取头元素,但不删除。poll表示获取头元素,并删除其在队列中的位置) Thread head = waiters.peek(); // 如果当前线程就是等待队列中的头元素head,说明当前等待队列就刚刚加入的元素。 if (head == Thread.currentThread()){ // 尝试再次获得锁 if (!tryLock()){ // 再次尝试获取锁失败,即将该线程(即当前线程)挂起, LockSupport.park(); } else { // 获取锁成功,即将该线程(等待队列的头元素)从等待队列waiters中移除 waiters.poll(); return; } } else { // 如果等待队列的头元素head,不是当前线程,表示等待队列在当前线程加入前,就还有别的线程在等待 LockSupport.park(); } } } } private boolean tryUnlock() { // 首先确定当前线程是否为锁持有者 if (Thread.currentThread() != owner.get()){ // 如果当前线程不是锁的持有者,就抛出一个异常 throw new IllegalMonitorStateException(); } else { // 如果当前线程是锁的持有者,就先count-1 // 另外,同一时间执行解锁的只可能是锁的持有者线程,故不用担心原子性问题(原子性问题只有在多线程情况下讨论,才有意义) int countValue = count.get(); int countNextValue = countValue - 1; count.compareAndSet(countValue,countNextValue); if (countNextValue == 0){ // 如果当前count为0,意味着锁的持有者已经完全解锁成功,故应当失去锁的持有(即设置owner为null) // 其实我一开始挺纠结的,这里为什么需要使用CAS操作呢。反正只有当前线程才可以走到程序这里。 // 首先,为什么使用CAS。由于count已经设置为0,其它线程已经可以修改count,修改owner了。所以不用CAS就可能将owner=otherThread设置为owner=null了,最终的结果就是彻底卡死 //TODO_FINISHED 但是unlock()中的unpark未执行,根本就不会有其它线程啊。囧 // 这里代码还是为了体现源码的一些特性。实际源码是将这些所的特性,抽象到了更高的层次,形成一个AQS。 // 虽然tryUnlock是由实现子类实现,但countNextValue是来自countValue(而放在JarryReadWriteLock中就是writeCount),在AQS源码中,则是通过state实现 // 其次,有没有ABA问题。由于ABA需要将CAS的expect值修改为currentThread,而当前线程只能单线程执行,所以不会。 // 最后,这里owner设置为null的操作到底需不需要。实际源码可能是需要的,但是这里貌似真的不需要。 owner.compareAndSet(Thread.currentThread(),null); // 解锁成功 return true; } else { // count不为0,解锁尚未完全完成 return false; } } } @Override public void unlock() { if (tryUnlock()){ // 如果当前线程成功tryUnlock,就表示当前锁被空置出来了。那就需要从备胎中,啊呸,从waiters中“放“出来一个 Thread head = waiters.peek(); // 这里需要做一个简单的判断,防止waiters为空时,抛出异常 if (head != null){ LockSupport.unpark(head); } } } // 非核心功能就不实现了,起码现在不实现了。 @Override public void lockInterruptibly() throws InterruptedException { } @Override public boolean tryLock(long time, TimeUnit unit) throws InterruptedException { return false; } @Override public Condition newCondition() { return null; } }

这里就不进行一些解释了。因为需要的解释,在注释中都写的很明确了,包括我踩的一些坑。

如果依旧有一些看不懂的地方,或者错误的地方,欢迎@我,或者私信我。

三,手写ReentrantReadWriteLock 获取需求

与ReentrantLock一样,首先第一步操作,我们需要确定我们要做什么。

我们要做一个锁,这里姑且命名为JarryReadWriteLock。

这个锁,需要具备以下特性:读写锁,可重入锁,悲观锁。

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/zyxdzg.html