手写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 />![屏幕快照 2020-03-05 下午11.30.34.png](https://cdn.nlark.com/yuque/0/2020/png/771792/1583422291364-5698a024-d62c-4d8a-ac85-0f65571cd50e.png#align=left&display=inline&height=158&margin=%5Bobject%20Object%5D&name=%E5%B1%8F%E5%B9%95%E5%BF%AB%E7%85%A7%202020-03-05%20%E4%B8%8B%E5%8D%8811.30.34.png&originHeight=316&originWidth=1184&size=56511&status=done&style=none&width=592)<br />![屏幕快照 2020-03-05 下午11.29.46.png](https://cdn.nlark.com/yuque/0/2020/png/771792/1583422261578-e7379a87-6b68-4429-b151-1534a7d21c5b.png#align=left&display=inline&height=1152&margin=%5Bobject%20Object%5D&name=%E5%B1%8F%E5%B9%95%E5%BF%AB%E7%85%A7%202020-03-05%20%E4%B8%8B%E5%8D%8811.29.46.png&originHeight=1152&originWidth=1888&size=354954&status=done&style=none&width=1888)<br />![屏幕快照 2020-03-05 下午11.30.08.png](https://cdn.nlark.com/yuque/0/2020/png/771792/1583422273555-ba05eaef-5bd1-4146-8dc7-3279e70d45cf.png#align=left&display=inline&height=578&margin=%5Bobject%20Object%5D&name=%E5%B1%8F%E5%B9%95%E5%BF%AB%E7%85%A7%202020-03-05%20%E4%B8%8B%E5%8D%8811.30.08.png&originHeight=578&originWidth=1794&size=160952&status=done&style=none&width=1794)
但是我们仔细想想看这把锁是不是公平锁呢?其实聪明的同学已经发现了,就是当我们在调用lock方法的时候,如果占得锁的线程释放了锁了,它会去唤醒队列头部的线程,所以队列头部线程会for循环进来进行一次tryLock()抢锁,而此时如果外来线程直接调用了tryLock()方法,则有可能是外来线程(不是从队列中唤醒的线程)获得锁,所以稍加修改下就行
```java
public 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;
}