1. 我们在项目中或多或少都是用过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();
              // 此线程陷入阻塞,无法获取锁
              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. 重入多次,解锁一次,锁不会释放,但是重入次数会-1;
  2. 没拿到锁的情况下去解锁是不可行的会抛出IllegalMonitorStateException
  3. 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){
        //锁被占用,判断是不是自己占着,是自己则更新重入值,无需更新引用
        if (reference.get() == Thread.currentThread()){
            if (count.compareAndSet(count.get(),count.get() + 1))
                return true;
        }
    }else {
        //锁没被占用
        count.compareAndSet(count.get(),1);//count
        reference.compareAndSet(null,Thread.currentThread());
        return true;
    }
    return false;
}

/**
 * 抢锁(存在非公平现象)
 * 修改 queue
 */
public void lock(){
    //锁被占用,则CAS自旋不断地去抢锁
    if (!tryLock()){
        queue.offer(Thread.currentThread());
        //lock 是不死不休所以得用for循环,既然CAS拿不到则由轻量级锁转为重量级锁(挂起阻塞)再一次去拿锁
        for (;;){
            Thread hreadThread = queue.peek();
            //队列可能一个线程,所以offer进来的或者说唤醒进来的,都会去判断是不是头部,是头部则再一次去抢锁
            // 之所以不直接park,是因为下一次你可能被unpark时,需要去抢一次锁,当然期间,也有可能trylock的时候,非队列线程可能抢到锁,所以没抢到就继续park
            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()){
        throw new IllegalMonitorStateException("未能获取到锁,无法释放锁");
    }else {
        //只有是拿到锁的线程才有解锁的资格,所以此处是单线程
        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#crop=0&crop=0&crop=1&crop=1&height=158&id=kKjAC&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&originalType=binary&ratio=1&rotation=0&showTitle=false&size=56511&status=done&style=none&title=&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#crop=0&crop=0&crop=1&crop=1&height=1152&id=oXjLD&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&originalType=binary&ratio=1&rotation=0&showTitle=false&size=354954&status=done&style=none&title=&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#crop=0&crop=0&crop=1&crop=1&height=578&id=K47py&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&originalType=binary&ratio=1&rotation=0&showTitle=false&size=160952&status=done&style=none&title=&width=1794)


      但是我们仔细想想看这把锁是不是公平锁呢?其实聪明的同学已经发现了,就是当我们在调用lock方法的时候,如果占得锁的线程释放了锁了,它会去唤醒队列头部的线程,所以队列头部线程会for循环进来进行一次tryLock()抢锁,而此时如果外来线程直接调用了tryLock()方法,则有可能是外来线程(不是从队列中唤醒的线程)获得锁,所以稍加修改下就行

```java
 public boolean tryLock(){
        if (count.get() != 0){
            //锁被占用(可能是自己)
            if (Thread.currentThread() == reference.get()){
                count.set(count.get()+1);
                return true;
            }
        }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;
    }