赞
踩
ReentrantLock作为Lock的实现类,在JUC中扮演者十分重要的角色.学习Java的最好方式就是深入源码.
通过对ReentrantLock的学习,现在试着手写一遍ReentrantLock. PS:这里仅尝试实现简单的mini版本.重要的是体会 其中的思想…
因为Lock接口中很多方法要实现, 我就自己手写一个极简版本的Lock,用来实现一些简单的方法.
public interface Lock {
void lock();
void unlock();
}
这里仅仅只有 lock和unlock 上锁和解锁两个方法. 简单版本的 ReentrantLock中也主要围绕这两个方法展开.
下面是必要的一些 属性,我都加了注释,方便理解.
这里实现的是独占模式,然后 因为公平锁比非公平锁稍微麻烦一些,所以选择实现公平锁
因为这里用到了阻塞队列.画个图来理解一下
public class MiniReentrantLock implements Lock { /** * status 作为是否上锁的标志位. * status == 0 表示未上锁, status > 0 表示已上锁 */ private int status; /** * exclusiveOwnerThread==>表示 独占锁的线程 */ private Thread exclusiveOwnerThread; /** * head ==>表示 阻塞队列中的头节点 * tail ==> 表示 阻塞队列的尾节点 */ private Node head; private Node tail; /** * 一个内部类 ,作为容器,用来存放阻塞队列中的 线程 */ private static final class Node { //当前节点的前节点 Node prex; //当前节点的后节点 Node next; //当前节点的线程 Thread thread; public Node() { } //方便用来用线程创建节点. public Node(Thread thread) { this.thread = thread; } } @Override public void lock() { } @Override public void unlock() { } }
大体框架已经搭起来了.下面来具体实现 lock方法和unlock 方法.
.首先说一下lock 的思路
我们使用这个类的 lock 方法来进行加锁. 那么 如何加锁呢,就是保证同一时刻,只有一个线程可以拿到我们的一个标识(status),
这样我们可以使用CAS方式来确保只有一个线程能够获取,因为我们这里是重入锁,所以可能会同时拿到好几个锁,这样,我们只要确保status在每次拿到锁的时候进行+1(或者+其他的数字都可以,只要保证作为标识的作用就可以.),这样我们可以设计一个获取锁方法.在lock方法中调用这个方法,来进行操作.具体实现如下:
@Override public void lock() { acquire(1); } /** * 获取锁的方法==> 多个线程进入这个方法来获取锁 * 如果获取锁成功的话,我们就可以直接进入线程的逻辑进行操作. * 如果获取锁失败的话,就需要进入阻塞队列进行排队. * @param i 表示每次获取一个锁,实际的标识status的增量 */ private void acquire(int i) { //如果尝试获取锁失败. 将线程加入到阻塞队列中. if(!tryAcquire(i)){ Node node = addWorker(); acquireQueue(node,i); } //没有进入到if代码块中,表示获取锁成功.就直接结束方法了 } /** * 将传入的节点加入到阻塞队列中 * @param node 传入的节点 * @param i 锁的标识 */ private void acquireQueue(Node node, int i) { } /** * 线程获取锁失败后,将之封装成一个Node节点,方便入队 * @return 封装的node节点 */ private Node addWorker() { return null; } /** * 尝试获取锁的方法 * @param i 锁的标识 * @return true 表示获取锁成功, false 表示失败 */ private boolean tryAcquire(int i) { return false; }
下面来 根据思路 一个个方法实现…首先先来实现tryAcquire()
首先分析一下,怎么才能获取到锁呢.
1.当前没有其他线程来获取锁,也就是阻塞队列中也没有,只有一个线程,就可以直接获取到锁
2.之前的线程已经结束任务,当前尝试获取锁的队列在阻塞队列的首位, 可以拥有去获取锁的权限.
当然这里都有前提,就是当前锁的状态是0,也就是 锁是未被其他线程持有的.
因为要用CAS方式来给锁的标识来+1,那么就需要引入Unsafe类相关API
private static final Unsafe unsafe; private static final long stateOffset; private static final long headOffset; private static final long tailOffset; static { try { Field f = Unsafe.class.getDeclaredField("theUnsafe"); f.setAccessible(true); unsafe = (Unsafe) f.get(null); stateOffset = unsafe.objectFieldOffset (MiniReentrantLock.class.getDeclaredField("state")); headOffset = unsafe.objectFieldOffset (MiniReentrantLock.class.getDeclaredField("head")); tailOffset = unsafe.objectFieldOffset (MiniReentrantLock.class.getDeclaredField("tail")); } catch (Exception ex) { throw new Error(ex); } } private final boolean compareAndSetHead(Node update) { return unsafe.compareAndSwapObject(this, headOffset, null, update); } private final boolean compareAndSetTail(Node expect, Node update) { return unsafe.compareAndSwapObject(this, tailOffset, expect, update); } protected final boolean compareAndSetState(int expect, int update) { // See below for intrinsics setup to support this return unsafe.compareAndSwapInt(this, stateOffset, expect, update); }
将判断当前线程之前是否有等待的线程 的代码提取成方法:
/** * 判断 当前线程前面是否有等待的线程 * * @return true 表示 当前线程前面有等待的线程 false 表示当前线程前面没有等待的线程,可以去尝试获取锁 */ private boolean hasQueuedPredecessor() { //h =>头节点 t=>尾节点 n=>head.next Node h = head; Node t = tail; Node n ; /** * 怎么判断呢? * 1.队列如果没有初始化的话,那么肯定当前线程前面没有 等待队列了! ==> h == t == null ==>true: 无等待队列 ==> 返回false * false: 有等待队列 ==>返回true * 2.因为第一个没有任何竞争拿到锁的线程是没进行任何操作的,也就是说他并没有初始化阻塞队列,第二个线程来竞争锁的时候,就会初始化阻塞队列, * 但是这个时候如果因为多线程进来竞争CAS 改变锁状态,导致第二个线程CAS失败,那么n 就是null * 这个时候,也要告诉其他线程,前面有人了...当然这是一个非常极端的情况 * 3. 如果前面两个条件都成立了,那么就是 有了头节点,且头节点的next == n !=null * 来判断的这个节点 n 的线程 并不是当前的线程,返回 true ==> n.thread != Thread.currentThead() */ return h != t &&((n =head.next) == null || n.thread != Thread.currentThread()); }
/** * 尝试获取锁的方法 * * @param i 锁的标识要增加的数值 * @return true 表示获取锁成功, false 表示失败 */ private boolean tryAcquire(int i) { //先来判断,锁是否没有被其他线程持有! if (status == 0) { /** * 1.首先判断是不是等待队列的首位 * 2.CAS方式给锁标识+1.也就是争抢锁 */ if (!hasQueuedPredecessor() && compareAndSetState(0, i)) { //进入if代码块里面之后,说明当前线程获取锁成功了! //那么我们就将当前线程,设置为 拥有锁的线程 this.exclusiveOwnerThread = Thread.currentThread(); return true; } //如果当前线程正好就是现在获取到锁的线程 else if (this.exclusiveOwnerThread == Thread.currentThread()) { //那么就是重入锁! //需要更新锁的标识 int c = getStatus(); c += i; this.status = c; return true; } } //如果不满足上述条件.,也就是 // 1.锁的状态status >0 ,锁被占有了. // 2.CAS 抢锁没抢过别人~ return false; }
那么回到 最初的acquire()方法中,此时我们完成了判断试图获取锁成功与否,获取锁失败之后,我们需要将失败的线程加入到阻塞队列中,然后
那么来实现具体方法:
addWorker():
/** * 线程获取锁失败后,将之封装成一个Node节点,方便入队 * * @return 封装的node节点 */ private Node addWorker() { //以当前线程创建一个node节点,因为是获取锁失败的,恶搞一下~loser Node loser = new Node(Thread.currentThread()); //然后将之加入到阻塞队列中. //因为他是获取锁失败的,那么必定有成功的,那么他就需要入尾 Node loserPrefix = tail; if(null != loserPrefix){ //将loser 的前引用指向之前的尾 loser.prefix = loserPrefix; //调用CAS方法,将当前线程入尾 if(compareAndSetTail(tail,loser)){ //成功入尾 tail.next = loser; return loser; } } //上述条件不满足的话. //1. 队列就是空的! //2.CAS入尾的时候又被人截胡了.. //这时候就需要再次尝试入尾~ intoQueue(loser); return loser; } /** * 入尾操作,不成功不退出! * @param loser 之前没有入队成功的loser~ */ private void intoQueue(Node loser) { for (;;){ //承上启下~上面有两种情况下来.1.队列是空的.2.CAS入尾失败 if(tail == null){ //说明队列是空的 //就是之前是 第一个线程过来,他直接玩去了,没有初始化 阻塞队列..那么我们只能辛苦给他创建了! if(compareAndSetHead(new Node())){ tail = head; } //这个时候我们就初始化完了阻塞队列,,继续 尝试入队吧~ }else{ //说明队列不是空的,我们是因为CAS入尾失败才来的 //1.先找到要入队的的位置的前面的节点..因为我们是加到最后面的 Node front = tail; if (front != null){ //这里判断是为了防止我们在这里干这个的时候,其他线程把tail给弄成null了... loser.prefix = front; //继续CAS 入队吧,,这次要直到成功才可以 if(compareAndSetTail(tail,loser)){ tail.next =loser; return; } } } } }
acquireQueue()方法如下:
/** * 将传入的节点加入到阻塞队列中 * * @param node 传入的节点 * @param i 锁的标识 */ private void acquireQueue(Node node, int i) { //因为我们已经在阻塞队列中了,那么久只能在这里自旋,直到拿到锁成功 for (;;){ //啥时候我们才能拿到锁呢,,,, 只有我们前面的线程都走了,我们成为head.next了 //当前节点的前面节点 Node n = node.prefix; if(n == head && tryAcquire(i)){ //将当前线程的节点 设置成head setHead(node); //将原来的head节点的引用设置为null,防止内存泄漏 n.next = null; return; } //没满足条件你就搁这自旋吧,直到拿到锁~ } }
lock 方法的逻辑就写完了…那么继续来解锁~
解锁其实就是释放锁嘛.把我们的锁的标识改为0 ,这样其他线程看到0了,就可以去获取锁了
释放锁的逻辑比较简单…我就写在一起了
@Override public void unlock() { //这里写死了.就是1 因为之前加锁的时候 也是加1 release(1); } /** * 释放锁,因为是重入锁,所以需要一层层的来 */ private void release(int i) { if (tryRelease(i)) { Node n = this.head; //如果阻塞队列里面有人等着呢 if (n.next != null) { //唤醒他的后面的那个! unParkNext(head); } } } /** * 唤醒当前节点的下一个等待节点 * @param node 当前节点 */ private void unParkNext(Node node) { Node n = node.next; if (n != null && n.thread != null){ //说明n 确实是一个等待的节点 //借用JUC提供的类去唤醒. LockSupport.unpark(n.thread); } } /** * 尝试 释放锁 * * @param i 锁的标识的减值 * @return true 表示释放成功 false 表示释放失败 */ private boolean tryRelease(int i) { if (getExclusiveOwnerThread() != Thread.currentThread()) { //如果当前线程不是持有所锁的线程. throw new RuntimeException("你没锁,释放啥呢"); } //经过上述判断之后,只有持有锁的线程才能进来这里,就不存在并发安全问题了 int c = getStatus() - i; if (c == 0) { //说明 已经彻底释放干净了 this.exclusiveOwnerThread = null; this.status = 0; return true; } //没进去条件说明 还有锁没释放完. this.status = c; return false; }
以上~
这里万分感谢B站的暴躁小刘讲师.因为他的课才会让我更方便的理解JUC~
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。