深入理解 ReentrantLock

118 阅读8分钟

深入理解 ReentrantLock

前言

我们都知道JDK中已经有了synchronized 锁,为什么还要提供 ReentrantLock

与 相比 ReentrantLock锁有什么优势?为什么需要提供这个锁?
ReentrantLock 锁 和 synchronized 锁 该怎么选择?

synchronized 锁
  • 隐式锁
  • 可冲入锁
  • 自动释放锁
  • 不能人为控制

synchronized 锁 ,自动释放锁,好处是不需要我们担心释放锁,但也带来一个问题,如果下面代码锁总执行时间很长,也就意味着长时间没释放锁,其他线程等待时间就过长。

synchronized (lock){

//时间很长    
}

业务中,我们一般都会有执行时间限制,但对于synchronized 来说,我们不能去打断锁,除非我们在里面写一写特定的打断锁代码,比如抛个异常。。

这样虽然也能干,但显得我们代码有点臃肿,远远没有 ReentrantLock 那么灵活。

ReentrantLock用法详解

ReentrantLock 相对于 synchronized 而言 reentrantlock 是显示锁

提供了 Lock(),unLock()加锁释放锁,一般配合try…catch…使用

    public  static ReentrantLock reentrantLock = new ReentrantLock();
        new Thread(()->{
  				reentrantLock.lock();
           try {
               System.out.println( Thread.currentThread().getName() + " do some thing");
           }finally {
               reentrantLock.unlock();
           }
        }).start();

也提供了尝试获取锁方法

    public boolean tryLock()

   public boolean tryLock(long timeout, TimeUnit unit)

使用如下


    public static void testtryLock() {
        boolean b = reentrantLock.tryLock();
        if (b) {
            try {
                TimeUnit.MICROSECONDS.sleep(1);
                System.out.println(Thread.currentThread().getName() + " get lock ");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                reentrantLock.unlock();
            }

        } else {
            try {
                TimeUnit.MICROSECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " un lock ");
        }

    }

其中还提供了可以被打断方法,相比 synchronized 很灵活

    public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
    }

使用如下


   public static void lockInterruptibly() throws InterruptedException {
 			//可以被打断的锁
            reentrantLock.lockInterruptibly();
        try {
            while (!Thread.currentThread().isInterrupted()){
                System.out.println(Thread.currentThread().getName() + " get lock ");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            reentrantLock.unlock();
            System.out.println(Thread.currentThread().getName() + " unlock  ");
        }


    }

test

        Thread thread = new Thread(() -> {
         		 lockInterruptibly();
            try {
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        thread.start();
        TimeUnit.SECONDS.sleep(1);
        thread.interrupt();  // 锁 被打断

可以看到,打断后释放锁

在这里插入图片描述

锁的公平性与非公平性

Sync是一个抽象类,它有两个子类FairSync与NonfairSync,
分别对应公平锁和非公平锁。从下面的ReentrantLock构造函数可以看出,会传入一个布尔类型的变量fair指定锁是公平的还是非公平的,默认为非公平的。



    /**
     * Creates an instance of {@code ReentrantLock}.
     * This is equivalent to using {@code ReentrantLock(false)}.
     */
    public ReentrantLock() {
        sync = new NonfairSync();
    }

    /**
     * Creates an instance of {@code ReentrantLock} with the
     * given fairness policy.
     *
     * @param fair {@code true} if this lock should use a fair ordering policy
     */
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

公平就是排队一个一个来
非公平就是你可以插队先来

阻塞队列与唤醒机制

对于 synchronized 来说,我们经常和wait 和 notify 配合使用,可以用来阻塞和唤醒
同样 ReentrantLock 也提供了一个接口配合使用 Condition。

Condition本身也是一个接口,其功能和wait/notify类似,功能比 wait 和 notify 更加精细。

synchronized 搭配 wait 和 notify 来阻塞和唤醒,比如在生产者和消费者模式中,唤醒线程,唤醒的是全部的线程,可能唤醒生产者线程了,此时队列是满的,还得继续阻塞等待,继续唤醒消费者线程。所以这样就存在一个问题。唤醒不够精细,此时需要唤醒消费者线程,而otifyall唤醒全部线程,可能生产者抢到,也可能消费者抢到。

对于synchronized来说,会将所有阻塞的都放在一个阻塞队列中,等待通知时从里面通知,不知道哪个线程会出来抢占锁。

ReentrantLock 支持多个,不同的Condition唤醒和通知不同的线程

对此 Condition 就比较精细化了,分别用不同的Condition对生产者和消费者进行阻塞和通知


    private final static Lock lock = new ReentrantLock();

    private final static Condition produceCond = lock.newCondition();

    private final static Condition comsumerCond = lock.newCondition();

    private final static LinkedList<Long> TIME_POOL = new LinkedList<>();

    private final static int MAX = 100;

    public static void main(String[] args) {

        IntStream.rangeClosed(0,5).forEach(x->{
            new Thread(() -> {
                for (; ; ) {
                    buildData();
                    try {
                        TimeUnit.SECONDS.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }

            },x+":P").start();
        });

        IntStream.rangeClosed(0,10).forEach(x->{
            new Thread(() -> {
                for (; ; ) {
                    consumeData();
                    try {
                        TimeUnit.SECONDS.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }

            },x+":C ").start();
        });

    }


    static void buildData() {
      lock.lock();
        try {
            while (TIME_POOL.size() >= MAX) {
                try {
                    produceCond.await();  // 相当于 wait()
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            Long value = System.currentTimeMillis();
            System.out.println(Thread.currentThread().getName() + " p -> " + value);
            TIME_POOL.addLast(value);
            produceCond.signalAll(); // 相当于   notifyAll();
        } finally {
            lock.unlock();
        }

    }


    static void consumeData() {
                lock.lock();
        try {
            while (TIME_POOL.isEmpty()) {
                try {
                    comsumerCond.await();  // 相当于 wait()
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            Long value = TIME_POOL.removeFirst();
            System.out.println(Thread.currentThread().getName() + " c -> " + value);
            comsumerCond.signalAll(); // 相当于   notifyAll();
        } finally {
            lock.unlock();
        }

    }

Condition必须和Lock一起使用,所以Condition的实现也是Lock的一部分。首先查看互斥锁和
读写锁中Condition的构造方法

public class ReentrantLock implements Lock, java.io.Serializable {
    // ... 
    public Condition newCondition() {
        return sync.newCondition();
    }
}

public class ReentrantReadWriteLock implements ReadWriteLock, java.io.Serializable {
    //...
    private final ReentrantReadWriteLock.ReadLock readerLock;
    private final ReentrantReadWriteLock.WriteLock writerLock;

    // ... 
    public static class ReadLock implements Lock, java.io.Serializable {
        // 读锁不支持Condition
        public Condition newCondition() {
            // 抛异常 
            throw new UnsupportedOperationException();
        }
    }

    public static class WriteLock implements Lock, java.io.Serializable {
        // ... 
        public Condition newCondition() {
            return sync.newCondition();
        }// ...
        // 
    }
// ... 
}

读写锁中的 ReadLock 是不支持 Condition 的,读写锁的写锁和互斥锁都支持Condition。虽
然它们各自调用的是自己的内部类Sync,但内部类Sync都继承自AQS。因此,上面的代码
sync.newCondition最终都调用了AQS中的newCondition:

在这里插入图片描述

每一个Condition对象上面,都阻塞了多个线程。因此,在ConditionObject内部有一个单向链表
组成的队列

public final void await() throws InterruptedException {
 // 刚要执行await()操作,收到中断信号,抛异常
  if (Thread.interrupted()) throw new InterruptedException(); 
  // 加入Condition的等待队列 
  Node node = addConditionWaiter(); 
  // 阻塞在Condition之前必须先释放锁,否则会死锁 
  int savedState = fullyRelease(node); 
  int interruptMode = 0; while (!isOnSyncQueue(node)) {
   // 阻塞当前线程 
   LockSupport.park(this);
    if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) break; 
    }
    // 重新获取锁
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE) interruptMode = REINTERRUPT; 
    if (node.nextWaiter != null) 
    // clean up 
    if cancelled unlinkCancelledWaiters();
     if (interruptMode != 0) 
     // 被中断唤醒,抛中断异常 
     reportInterruptAfterWait(interruptMode);
      }

signal

public final void signal() {
 // 只有持有锁的线程,才有资格调用signal()方法 
 if (!isHeldExclusively()) throw new IllegalMonitorStateException(); 
 Node first = firstWaiter; 
 if (first != null)
  // 发起通知 
  doSignal(first); 
  }
  // 唤醒队列中的第1个线程 
  private void doSignal(Node first) { 
  do {
  if ( (firstWaiter = first.nextWaiter) == null) 
  lastWaiter = null;
   first.nextWaiter = null; 
   } 
   while (!transferForSignal(first) && (first = firstWaiter) != null); 
   }
   final boolean transferForSignal(Node node) {
    if (!node.compareAndSetWaitStatus(Node.CONDITION, 0)) 
    return false; 
    // 先把Node放入互斥锁的同步队列中,再调用unpark方法 
    Node p = enq(node); 
    int ws = p.waitStatus; 
    if (ws > 0 || !p.compareAndSetWaitStatus(ws, Node.SIGNAL)) 
    LockSupport.unpark(node.thread);
     return true; 
    }

当一个线程在调用了await方法以后,直到线程等待的某个条件为真的时候才会被唤醒。这种方式为线程提供了更加简单的等待/通知模式。Condition必须要配合锁一起使用,因为对共享状态变量的访问发生在多线程环境下。一个Condition的实例必须与一个Lock绑定,因此Condition一般都是作为Lock的内部实现。

条件队列转同步队列可以参考 CyclicBarrie里的debug流程

Lock接口的主要方法如下

void lock():
给对象加锁,如果锁未被其他线程使用,则当前线 程将获取该锁;如果锁正在被其他线程持有,则将禁用当前线程,直到当
前线程获取锁。

boolean tryLock():
试图给对象加锁,如果锁未被其他线程使 用,则将获取该锁并返回true,否则返回false。tryLock()和lock()的区
别在于tryLock()只是“试图”获取锁,如果没有可用锁,就会立即返回。

lock()
在锁不可用时会一直等待,直到获取到可用锁。

tryLock(long timeout TimeUnit unit):
创建定时锁,如果在给定的等待时间内有可用锁,则获取该锁。

void unlock():
释放当前线程所持有的锁。锁只能由持有者释放,如果当前线程并不持有该锁却执行该方法,则抛出异常。

Condition newCondition():
创建条件对象,获取等待通知组件。该组件和当前锁绑定,当前线程只有获取了锁才能调用该组件的await(),
在调用后当前线程将释放锁。

getHoldCount():
查询当前线程保持此锁的次数,也就是此线程执行lock方法的次数。

getQueueLength():
返回等待获取此锁的线程估计数,比如启动 5个线程,1 个线程获得锁,此时返回4。getWaitQueueLength(Condition condition):返回在Condition条件下等待该锁的线程数量。比如有 5 个线程用同一个condition对象,并
且这 5 个线程都执行了condition对象的await方法,那么执行此方法将返
回5。

hasWaiters(Condition condition):
查询是否有线程正在等待与 给定条件有关的锁,即对于指定的contidion对象,有多少线程执行了
condition.await方法。

hasQueuedThread(Thread thread):
查询给定的线程是否等待获取该锁。

hasQueuedThreads():
查询是否有线程等待该锁。

isFair():
查询该锁是否为公平锁。

isHeldByCurrentThread():
查询当前线程是否持有该锁,线程执行lock方法的前后状态分别是false和true。

isLock():
判断此锁是否被线程占用。

lockInterruptibly():
如果当前线程未被中断,则获取该锁。

synchronized和ReentrantLock的比较

synchronized和ReentrantLock的共同点如下。

  • 都用于控制多线程对共享对象的访问。
  • 都是可重入锁。
  • 都保证了可见性和互斥性。

synchronized和ReentrantLock的不同点如下。

  • ReentrantLock显式获取和释放锁;synchronized隐式获取和释放
    锁。为了避免程序出现异常而无法正常释放锁,在使用ReentrantLock时必
    须在finally控制块中进行解锁操作。
  • ReentrantLock可响应中断、可轮回,为处理锁提供了更多的灵活
    性。
  • ReentrantLock是API级别的,synchronized是JVM级别的。
  • ReentrantLock可以定义公平锁。
  • ReentrantLock通过Condition可以绑定多个条件。
  • 二者的底层实现不一样:synchronized是同步阻塞,采用的是悲观
    并发策略;Lock是同步非阻塞,采用的是乐观并发策略。
  • Lock 是 一 个 接 口 , 而 synchronized 是 Java 中 的 关 键 字 ,
    synchronized是由内置的语言实现的。
  • 我们通过Lock可以知道有没有成功获取锁,通过synchronized却无
    法做到。
  • Lock可以通过分别定义读写锁提高多个线程读操作的效率。

其他知识点

Java 多线程基础
深入理解aqs
ReentrantLock用法详解
深入理解信号量Semaphore
深入理解并发三大特性
并发编程之深入理解CAS
深入理解CountDownLatch
Java 线程池