手写ReentrantLock

我们在项目中或多或少都是用过ReentrantLock,它同syncronized关键字一样都是悲观锁,只不过syncronized是由jdk内部自己去实现的,有兴趣的可以打开opne jdk学习下,由于我自身水平有限就不做扩展。我们需要明白的是前人已经帮我们铺好了路,也就是syncronized内部实现由偏向锁—>轻量级锁—>重量级锁。既然它已经这么优秀了为何我们还要用ReentrantLock,想要了解为何我们就必须熟悉其内部实现。

手写前提

  1. 在我们不看源码的情况下,要你去实现你会如何实现。首先要明白ReentrankLock的几个特性:
  • 可重入:就是说你获得锁之后可以再次获得锁,并且你释放锁的次数必须和你重入得次数一致,否则锁不予释放

    1. public static void main(String[] args){
    2. ReentrantLock reentrantLock = new ReentrantLock();
    3. new Thread(() -> {
    4. reentrantLock.lock();
    5. reentrantLock.lock();
    6. reentrantLock.unlock();
    7. }).start();
    8. LockSupport.parkNanos(1000*1000*1000*1L);
    9. new Thread(() -> {
    10. reentrantLock.lock();
    11. System.out.println("无法获取锁");
    12. reentrantLock.unlock();
    13. }).start();
    14. }
  • 悲观锁:它是一把独占锁,别人占据着你是无法获取的,同时你在没拿到锁的情况下去解锁也是不可行的

    1. public static void main(String[] args){
    2. ReentrantLock reentrantLock = new ReentrantLock();
    3. new Thread(() -> {
    4. reentrantLock.lock();
    5. LockSupport.parkNanos(1000*1000*1000*1L);
    6. reentrantLock.unlock();
    7. }).start();
    8. LockSupport.parkNanos(1000*1000*100*1L);
    9. new Thread(() -> {
    10. reentrantLock.unlock();
    11. }).start();
    12. }
    13. 抛出IllegalMonitorStateException

    所以总结下来就是一下几点:

  1. 重入多次,解锁一次,锁不会释放,但是重入次数会-1;
  2. 没拿到锁的情况下去解锁是不可行的会抛出IllegalMonitorStateException
  3. tryLock()是会尝试占据锁的,tryUnLock也是可以解锁的。

    知道这两点以后那我们就可以开始手写reentrantLock了,首先我们仿照reentrantLock的几个方法lock()、unlock()、tryLock()、tryUnLock()。当然最后一个方法是在reentrantLock不存在的,但实际是在AbstractQueuedSynchronizer中存在的,也就是下面这个方法,至于为什么我们稍后解释。

    1. protected boolean tryRelease(int arg) {
    2. throw new UnsupportedOperationException();
    3. }

手写思路

  • 要实现独占并且其它没拿到锁的线程要处于阻塞,我们肯定会联想到线程通信LockSupport.park()
  • 要实现锁是否被占用我们可以用AtomicReference来表名那个线程占据着这把锁,而处于等待池中的线程也就是LockSupport.park()挂起的线程则可以联想到阻塞队列,将其放入到队列中不就解决了,锁释放了,从队列头部取出即可。
  • 重入次数用一个int类型表示就行,但因为必须实现院子操作,所以这里我们用原子类AtomicInteger

    1. 所以我们大致的思路就是,当众多线程争抢锁时,先抢到的则重入次数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){
        1. //锁被占用(可能是自己)
        2. if (Thread.currentThread() == reference.get()){
        3. count.set(count.get()+1);//单线程 无需CAS
        4. }
        }else {
        1. if (count.compareAndSet(count.get(),count.get()+1)){
        2. reference.set(Thread.currentThread());
        3. return true;
        4. }
        } return false; }

      /**

      • 抢锁(存在非公平现象)
      • 修改 queue */ public void lock(){ //锁被占用,则CAS自旋不断地去抢锁 if (!tryLock()){

        1. queue.offer(Thread.currentThread());
        2. //lock 是不死不休所以得用for循环,既然CAS拿不到则由轻量级锁转为重量级锁(挂起阻塞)再一次去拿锁
        3. for (;;){
        4. Thread hreadThread = queue.peek();
        5. //队列可能一个线程,所以offer进来的或者说唤醒进来的,都会去判断是不是头部,是头部则再一次去抢锁
        6. if (hreadThread == Thread.currentThread()){
        7. if (tryLock()){
        8. queue.poll();
        9. break;
        10. }else {
        11. //是头部线程元素,但是在此在队列并挂起
        12. LockSupport.park();
        13. }
        14. }else {
        15. //不是头部线程,在队列并挂起
        16. LockSupport.park();
        17. }
        18. }

        } }

      /**

      • 释放锁
      • 修改 queue */ public void unlock(){ if (tryUnlock()){
        1. Thread peek = queue.peek();
        2. //存在队列为空可能,比如就一个抢锁的不会去加入到阻塞队列
        3. if (peek != null){
        4. LockSupport.unpark(peek);
        5. }
        } }

      /**

      • 尝试去解锁
      • 修改count 和 reference */ public boolean tryUnlock(){ if (reference.get() != Thread.currentThread()){
        1. throw new IllegalMonitorStateException("未能获取到锁,无法释放锁");
        }else {
        1. //只有是拿到锁的线程才有解锁的资格,所以此处是单线程
        2. int value = count.get()- 1;
        3. count.set(value);
        4. //当你lock多次,但是unlock一次,此时是不会释放锁,只是不阻塞罢了
        5. if (value == 0){
        6. reference.set(null);
        7. return true;
        8. }else {
        9. return false;
        10. }
        } }

}

  1. 至此手写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)
  2. 但是我们仔细想想看这把锁是不是公平锁呢?其实聪明的同学已经发现了,就是当我们在调用lock方法的时候,如果占得锁的线程释放了锁了,它会去唤醒队列头部的线程,所以队列头部线程会for循环进来进行一次tryLock()抢锁,而此时如果外来线程直接调用了tryLock()方法,则有可能是外来线程(不是从队列中唤醒的线程)获得锁,所以稍加修改下就行
  3. ```java
  4. public boolean tryLock(){
  5. if (count.get() != 0){
  6. //锁被占用(可能是自己)
  7. if (Thread.currentThread() == reference.get()){
  8. count.set(count.get()+1);//单线程 无需CAS
  9. }
  10. }else {
  11. //为了实现公平锁,需要判断进来的线程是不是队列头部线程,不是则直接返回false(不让外来线程可乘之机)
  12. if (queue != null){
  13. if (queue.peek() == Thread.currentThread()
  14. && count.compareAndSet(count.get(),count.get()+1)){
  15. reference.set(Thread.currentThread());
  16. return true;
  17. }
  18. }else{
  19. if (count.compareAndSet(count.get(),count.get()+1)){
  20. reference.set(Thread.currentThread());
  21. return true;
  22. }
  23. }
  24. }
  25. return false;
  26. }