手写ReentrantLock
我们在项目中或多或少都是用过ReentrantLock,它同syncronized关键字一样都是悲观锁,只不过syncronized是由jdk内部自己去实现的,有兴趣的可以打开opne jdk学习下,由于我自身水平有限就不做扩展。我们需要明白的是前人已经帮我们铺好了路,也就是syncronized内部实现由偏向锁—>轻量级锁—>重量级锁。既然它已经这么优秀了为何我们还要用ReentrantLock,想要了解为何我们就必须熟悉其内部实现。
手写前提
在我们不看源码的情况下,要你去实现你会如何实现。首先要明白ReentrankLock的几个特性:
可重入:就是说你获得锁之后可以再次获得锁,并且你释放锁的次数必须和你重入得次数一致,否则锁不予释放
public static void main(String[] args){ReentrantLock reentrantLock = new ReentrantLock();new Thread(() -> {reentrantLock.lock();reentrantLock.lock();reentrantLock.unlock();}).start();LockSupport.parkNanos(1000*1000*1000*1L);new Thread(() -> {reentrantLock.lock();System.out.println("无法获取锁");reentrantLock.unlock();}).start();}
悲观锁:它是一把独占锁,别人占据着你是无法获取的,同时你在没拿到锁的情况下去解锁也是不可行的
public static void main(String[] args){ReentrantLock reentrantLock = new ReentrantLock();new Thread(() -> {reentrantLock.lock();LockSupport.parkNanos(1000*1000*1000*1L);reentrantLock.unlock();}).start();LockSupport.parkNanos(1000*1000*100*1L);new Thread(() -> {reentrantLock.unlock();}).start();}抛出IllegalMonitorStateException
所以总结下来就是一下几点:
- 重入多次,解锁一次,锁不会释放,但是重入次数会-1;
- 没拿到锁的情况下去解锁是不可行的会抛出IllegalMonitorStateException
tryLock()是会尝试占据锁的,tryUnLock也是可以解锁的。
知道这两点以后那我们就可以开始手写reentrantLock了,首先我们仿照reentrantLock的几个方法lock()、unlock()、tryLock()、tryUnLock()。当然最后一个方法是在reentrantLock不存在的,但实际是在AbstractQueuedSynchronizer中存在的,也就是下面这个方法,至于为什么我们稍后解释。
protected boolean tryRelease(int arg) {throw new UnsupportedOperationException();}
手写思路
- 要实现独占并且其它没拿到锁的线程要处于阻塞,我们肯定会联想到线程通信LockSupport.park()
- 要实现锁是否被占用我们可以用AtomicReference
来表名那个线程占据着这把锁,而处于等待池中的线程也就是LockSupport.park()挂起的线程则可以联想到阻塞队列,将其放入到队列中不就解决了,锁释放了,从队列头部取出即可。 重入次数用一个int类型表示就行,但因为必须实现院子操作,所以这里我们用原子类AtomicInteger
所以我们大致的思路就是,当众多线程争抢锁时,先抢到的则重入次数count+1,没抢到的则按照抢锁顺序放入到阻塞队列尾部,当锁释放时,也就是抢到锁的线程会去唤醒LockSupport.unPark(thread)队列头部阻塞的线程,并从队列中移除。<br />
```java /**
- @author heian
- @create 2020-03-05-10:04 下午
- @备注 不是公平锁,在调用lock方法中第二个tryLock方法时候,没加判断,可能存在锁释放时唤醒了队列中的某个线程,
外来线程调用又tryLock()方法此时也正好来抢锁,那此时存在非公平现象 */ public class MyReentrantLock {
//想获取锁的线程 private LinkedBlockingQueue
queue = new LinkedBlockingQueue<>(); //被引用的线程 private AtomicReference reference = new AtomicReference<>(); //计算重入得次数 private AtomicInteger count = new AtomicInteger(0); /**
- 尝试获取锁(不是真的去拿锁,所以不用加入到阻塞队列)
- 修改count 和 reference
*/
public boolean tryLock(){
if (count.get() != 0){
}else {//锁被占用(可能是自己)if (Thread.currentThread() == reference.get()){count.set(count.get()+1);//单线程 无需CAS}
} return false; }if (count.compareAndSet(count.get(),count.get()+1)){reference.set(Thread.currentThread());return true;}
/**
- 抢锁(存在非公平现象)
修改 queue */ public void lock(){ //锁被占用,则CAS自旋不断地去抢锁 if (!tryLock()){
queue.offer(Thread.currentThread());//lock 是不死不休所以得用for循环,既然CAS拿不到则由轻量级锁转为重量级锁(挂起阻塞)再一次去拿锁for (;;){Thread hreadThread = queue.peek();//队列可能一个线程,所以offer进来的或者说唤醒进来的,都会去判断是不是头部,是头部则再一次去抢锁if (hreadThread == Thread.currentThread()){if (tryLock()){queue.poll();break;}else {//是头部线程元素,但是在此在队列并挂起LockSupport.park();}}else {//不是头部线程,在队列并挂起LockSupport.park();}}
} }
/**
- 释放锁
- 修改 queue
*/
public void unlock(){
if (tryUnlock()){
} }Thread peek = queue.peek();//存在队列为空可能,比如就一个抢锁的不会去加入到阻塞队列if (peek != null){LockSupport.unpark(peek);}
/**
- 尝试去解锁
- 修改count 和 reference
*/
public boolean tryUnlock(){
if (reference.get() != Thread.currentThread()){
}else {throw new IllegalMonitorStateException("未能获取到锁,无法释放锁");
} }//只有是拿到锁的线程才有解锁的资格,所以此处是单线程int value = count.get()- 1;count.set(value);//当你lock多次,但是unlock一次,此时是不会释放锁,只是不阻塞罢了if (value == 0){reference.set(null);return true;}else {return false;}
}
至此手写ReentrantLock完成,为了方便大家记忆和理解我这里画了一些流程图如下:<br /><br /><br />但是我们仔细想想看这把锁是不是公平锁呢?其实聪明的同学已经发现了,就是当我们在调用lock方法的时候,如果占得锁的线程释放了锁了,它会去唤醒队列头部的线程,所以队列头部线程会for循环进来进行一次tryLock()抢锁,而此时如果外来线程直接调用了tryLock()方法,则有可能是外来线程(不是从队列中唤醒的线程)获得锁,所以稍加修改下就行```javapublic boolean tryLock(){if (count.get() != 0){//锁被占用(可能是自己)if (Thread.currentThread() == reference.get()){count.set(count.get()+1);//单线程 无需CAS}}else {//为了实现公平锁,需要判断进来的线程是不是队列头部线程,不是则直接返回false(不让外来线程可乘之机)if (queue != null){if (queue.peek() == Thread.currentThread()&& count.compareAndSet(count.get(),count.get()+1)){reference.set(Thread.currentThread());return true;}}else{if (count.compareAndSet(count.get(),count.get()+1)){reference.set(Thread.currentThread());return true;}}}return false;}
