理解Java并发编程:ReentrantLock和ReentrantReadWriteLock

2022-07-14 11:05:29

Lock锁

Lock是自JDK1.5起在Java并发包(JUC)中提供的锁。与synchronized相比,Lock提供了更加灵活的、可扩展的锁相关操作。支持多个与锁相关的条件。

锁本质上是用于控制多个线程对共享资源访问的工具。 通常,锁提供对共享资源的独占访问,一次只有一个线程可以获取锁,所有对共享资源的访问都需要先获取锁。 但是,某场景下可能允许并发访问共享资源,例如对某个共享资源的读操作,这就要求锁具备这个功能。Lock接口的一个具体实现:ReadWriteLock就可以实现多个线程同时读。这种特性是synchronized不具备的。

synchronized方法或代码块隐式提供了对每个对象关联的监视器锁(monitor)的访问,所有锁的获取和释放必须在一个代码块结构中。即当获取多个锁时,它们必须以相反的顺序释放,并且所有锁必须在它们被获取的同一个代码范围内释放。

虽然synchronized方法和代码块的作用域机制使得我们实现并发编程更加容易,避免了许多涉及锁的常见编程错误。但在某些情况下,我们需要以更灵活的方式使用锁。 比如我们需要先获取A锁,然后获取B锁,接着释放A锁获取C锁,然后释放B锁获取D锁。像这种复杂的场景,synchronized无能为力。而Lock接口的实现允许在不同范围内获取和释放锁,并允许以任何顺序获取和释放多个锁。

这种灵活性也要求我们必须控制好锁的获取和释放。不像synchronized那样,锁的获取和释放是由底层自动实现的,编程中只要使用关键字就行了。

因此在通常情况下,我们应该是遵循下列使用Lock的习惯:

Lock lock=...;
 lock.lock();try{// access the resource protected by this lock}finally{
   lock.unlock();}

当锁定和解锁发生在不同的作用域时,必须确保持有锁时执行的所有代码都受到 try-finally 或 try-catch 的保护,以保证在必要时释放锁。

Lock的实现提供了多种形式的锁。

1)非阻塞方式获取锁:tryLock();

2)可中断的方式获取锁:lockInterruptibly();

3)获取锁时还可以加入超时时间:tryLock(long, TimeUnit);

Lock实例只是普通对象,它们本身也可以用作synchronized语句中的目标对象。 获取Lock实例的监视器锁(monitor)与调用该实例的任何lock方法没有特定的关系。 建议不要在synchronized方法或者代码块中使用Lock实例作为锁定的对象,以避免混淆。

Lock锁的方法

publicinterfaceLock{voidlock();voidlockInterruptibly()throwsInterruptedException;booleantryLock();booleantryLock(long time,TimeUnit unit)throwsInterruptedException;voidunlock();ConditionnewCondition();}

lock

以阻塞的方式获取锁,如果获取锁失败,则该线程进入休眠状态,线程调度被暂停,直到获得锁。

lockInterruptibly

与lock方法相比,该方法可以响应中断。

如果获取锁成功则立即返回。否则线程进入休眠状态,暂停被线程调度,直到下列情况发生:

1)获取到锁

2)其它线程中断了该线程,并且支持锁获取中断

如果当前线程在获取锁时被中断(并且支持中断获取锁)则该方法会抛出一个InterruptedException。

tryLock

以非阻塞方式获取锁,如果锁可用则返回true;如果不可用则立即返回false,而不是阻塞线程。

一贯用法如下:

Lock lock=...;if(lock.tryLock()){try{// 业务逻辑A}finally{
     lock.unlock();}}else{// 业务逻辑B}

tryLock(long time, TimeUnit unit)

和tryLock方法相比,多了支持设置等待时间和中断响应。

如果锁可用,则此方法立即返回true。 如果锁不可用,则当前线程将出于线程调度目的而被禁用并处于休眠状态,直到以下三种情况之一发生:

1)锁被当前线程获取到

2)其他一些线程中断当前线程,并且支持中断获取锁

3)经过了指定的等待时间

如果当前线程在获取锁时被中断(支持中断获取锁的前提下),则该方法会抛出一个InterruptedException。

unlock

有加锁,必然有释放锁。unlock方法就是用于释放锁。

newCondition

Lock锁提供了一个与之相关的条件对象(Condition)。该方法就是返回绑定到此Lock实例的新Condition实例。

在等待Condition条件之前,当前线程必须先持有与Condition相关联的锁。 调用Condition.await()方法将在等待之前自动释放锁,并在等待返回之前重新获取锁。

后面会介绍具体的案例。

Lock锁的优势

当一个线程调用了synchronized方法之后,如果没有成功获取到锁,就立即陷入了等待,没有第二种选择。而Lock锁可以选择不同的方式,可以立即陷入等待,也可以立即返回,也可以等待一段时间。具体看你调用哪个API方法。

Lock锁的使用案例

Lock是一个接口,其有多种实现类,常用的有ReentrantLock、ReentrantReadWriteLock等,我们一一举例。

ReentrantLock

ReentrantLock是Lock接口最常用的实现类,它一个可重入的锁,可以认为和synchronized具有相同的语义,但更具扩展性。

ReentrantLock锁会被最近一次成功获取到锁的但还未释放锁的线程拥有。ReentrantLock的lock方法会在成功获取到锁时返回,如果检测到当前线程已经拥有了该锁,则该方法立即返回。

ReentrantLock的构造函数接受一个可选的公平参数。当设置为为true,即公平锁时,锁倾向于授予等待时间最长的线程。反之,不能保证任何特定的锁的获取顺序,即非公平。

注意,在高并发场景下,多个线程访问的公平锁的程序可能会显示出较低的总体吞吐量(即更慢,通常慢得多)。并且在公平性方面(保证不出现饿死的情况)和非公平锁的差异也很小。

另外,锁的公平性并不能保证线程调度的公平性。因此,使用公平锁的众多线程之一可能会连续多次获得它,而其他活动线程一直没能够持有该锁。特别要强调的是,使用没有设置等待时间的tryLock()方法是不遵守公平性设置的。

ReentrantLock典型的使用模式如下:

classX{privatefinalReentrantLock lock=newReentrantLock();// ...publicvoidmethod(){
     lock.lock();// block until condition holdstry{// ... method body}finally{
       lock.unlock()}}}

举例1:快速入门

下面的例子中,方法method1中会对成员变量count自增,而自增操作不是一个原子操作,有读数据、加法操作、写数据多个步骤。因此需要加锁保证多线程环境下的并发安全。

加锁时,能保证输出的结果是正确的累计值;而不加锁时,输出的最终结果可能小于预期。可以对比下加锁和不加锁的运行结果。

publicclassTestLock{privateReentrantLock lock=newReentrantLock();privateint count=0;publicvoidmethod1(){//lock.lock();//注释这行代码取消加锁try{Thread.sleep(1000);System.out.println(++count);}catch(InterruptedException e){
            e.printStackTrace();}finally{//lock.unlock();}}publicstaticvoidmain(String[] args){TestLock testLock=newTestLock();//创建多个线程对共享变量同时进行操作for(int i=0; i<20000; i++){newThread(newRunnable(){@Overridepublicvoidrun(){
                    testLock.method1();}}).start();}}}

举例2:基本使用

publicclassTest1{privateLock lock=newReentrantLock();publicvoidmethod1(){try{
            lock.lock();System.out.println("method1 business");}finally{
            lock.unlock();}}publicvoidmethod2(){try{
            lock.lock();System.out.println("method2 business");}finally{
            lock.unlock();}}publicstaticvoidmain(String[] args){Test1 test=newTest1();Thread thread1=newThread(()->{for(int i=0; i<10; i++){
                test.method1();try{Thread.sleep(200);}catch(InterruptedException e){
                    e.printStackTrace();}}});Thread thread2=newThread(()->{for(int i=0; i<10; i++){
                test.method2();try{Thread.sleep(500);}catch(InterruptedException e){
                    e.printStackTrace();}}});

        thread1.start();
        thread2.start();}}

上述案例展示了多个方法共享一把锁的场景,运行程序,我们可以看到程序正常输出20次,两个线程交替执行完成。

如果我们忘记了释放锁,此时会发生什么情况呢?比如注释掉第一个方法的释放锁操作。

publicvoidmethod1(){try{
            lock.lock();System.out.println("method1 business");}finally{//lock.unlock();}}

此时只输出了10次method1 business,并且程序没有终止。因为method1 先被调用,并且一直持有锁没有释放,导致method2获取锁失败,一直等待 。然而method1 可以不断重新获取锁(可重入的),因此第一个线程循环调用method1方法后输出。

上述使用Lock的方式和synchronized类似,都是一直等待,直到获得锁。

在实际开发中要遵循try{...}finally{...}这种使用Lock锁的范式,防止锁得不到释放。

上述代码method2方法改进为使用tryLock,则可以避免程序阻塞,虽然获取锁失败,但是程序最终能执行完成,示例如下。

publicvoidmethod2(){//        try {//            lock.lock();//            System.out.println("method2 business");//        } finally {//            lock.unlock();//        }boolean success=false;try{
            success= lock.tryLock(1000,TimeUnit.MILLISECONDS);}catch(InterruptedException e){
            e.printStackTrace();}if(success){System.out.println("lock success");}else{System.out.println("lock fail");}}

举例3:可重入性

下列例子中,虽然get方法中也要获取锁,但是并不会导致死锁,因为ReentrantLock 支持重入。

publicclassTest02extendsThread{privateReentrantLock lock=newReentrantLock();publicvoidget(){
        lock.lock();try{System.out.println("get方法"+Thread.currentThread().getName());}finally{
            lock.unlock();}}publicvoidset(){
        lock.lock();try{System.out.println("set方法"+Thread.currentThread().getName());get();//虽然get方法中也要获取锁,但是不会死锁,因为ReentrantReadWriteLock 支持重入}catch(Exception e){
            e.printStackTrace();}finally{
            lock.unlock();}}@Overridepublicvoidrun(){set();}publicstaticvoidmain(String[] args){for(int i=0; i<4; i++){Test02 ss=newTest02();
            ss.start();}}}//输出
set方法Thread-0
get方法Thread-0
set方法Thread-1
set方法Thread-2
get方法Thread-1
get方法Thread-2
set方法Thread-3
get方法Thread-3

公平锁、非公平锁

如果在绝对时间上,先对锁进行获取的请求一定会先被满足,那么这个锁是公平的,反之,是不公平的。公平的获取锁,也就是等待时间最长的线程最优先获取锁,也可以说锁获取是顺序的。

ReentrantLock类提供了一个构造函数,通过传入fair参数控制锁是否是公平的。

1)公平锁

公平和非公平锁的队列都基于锁内部维护的一个双向链表,表结点Node的值就是每一个请求当前锁的线程。公平锁每次都是依次从队首取值。

2)非公平锁

在等待锁的过程中, 如果有任意新的线程妄图获取锁,都是有很大的几率直接争抢到锁的。

公平锁是先到先得,按序进行。非公平锁就是不排队直接拿,失败再说。公平锁保证了锁的获取按照FIFO原则,而代价是进行大量的线程切换。非公平性锁虽然可能造成线程“饥饿”,但由于先尝试争抢,失败后才阻塞,因此最大限度减少线程切换,具有更大的吞吐量。

ReentrantReadWriteLock

前面提到锁(如ReentrantLock、synchronized)都是排他锁,这些锁在同一时刻只允许一个线程进行访问,而读写锁在同一时刻可以允许多个读线程访问。在写线程访问时,所有的读线程和其他写线程均被阻塞。读写锁维护了一对锁,一个读锁和一个写锁,通过分离读锁和写锁,使得并发性相比一般的排他锁有了很大提升。
假设你的程序中涉及到对一些共享资源的读和写操作,且写操作没有读操作那么频繁。在没有写操作的时候,两个线程同时读一个资源没有任何问题,那么应该允许多个线程能同时读取共享资源。但是如果有一个线程去写这些共享资源,就不应该再有其它线程对该资源进行读或写(也就是说:读-读能共存,读-写不能共存,写-写不能共存)。这就需要一个读/写锁来解决这个问题。

JDK5引入的ReentrantReadWriteLock 就可以来实现读写锁。

案例1:快速入门

分别使用读锁和写锁,从运行效果可以发现读锁是可以共享的,而写锁是独占的;

因此读锁可以近乎同时完成操作,而写锁是排他的,需要等待前一个线程操作完成并释放掉锁,下一个线程才能进行操作。

publicclassReadWriteLockTest{privateReadWriteLock readWriteLock=newReentrantReadWriteLock();publicvoidmethod1(){//readWriteLock.readLock().lock();
        readWriteLock.writeLock().lock();try{Thread.sleep(1000);System.out.println("method1 business");}catch(Exception e){
            e.printStackTrace();}finally{//readWriteLock.readLock().unlock();
            readWriteLock.writeLock().unlock();}}publicstaticvoidmain(String[] args){ReadWriteLockTest readWriteLockTest=newReadWriteLockTest();IntStream.range(0,10).forEach(i->newThread(readWriteLockTest::method1).start());}}

案例2:自定义缓存

我们以一个自定义缓存的例子来演示读写锁的使用。

publicclassCache{staticMap<String,Object> map=newHashMap<String,Object>();staticReentrantReadWriteLock rwl=newReentrantReadWriteLock();staticLock r= rwl.readLock();staticLock w= rwl.writeLock();// 获取一个key对应的valuepublicstaticfinalObjectget(String key){
        r.lock();try{//为了演示出效果,get操作再细分如下,开始—>结束视为原子步骤System.out.println("正在做读的操作,key:"+ key+" 开始");Thread.sleep(100);Object object= map.get(key);System.out.println("正在做读的操作,key:"+ key+",value:"+ object+" 结束");System.out.println();return object;}catch(InterruptedException e){
            e.printStackTrace();}finally{
            r.unlock();}return key;}// 设置key对应的value,并返回旧有的valuepublicstaticfinalObjectput(String key,Object value){
        w.lock();try{//为了演示效果,set操作再细分如下,开始—>结束整个过程视为原子步骤System.out.println("正在做写的操作,key:"+ key+",value:"+ value+"开始.");Thread.sleep(100);Object object= map.put(key, value);System.out.println("正在做写的操作,key:"+ key+",value:"+ value+"结束.");System.out.println();return object;}catch(InterruptedException e){
            e.printStackTrace();}finally{
            w.unlock();}return value;}// 清空所有的内容publicstaticfinalvoidclear(){
        w.lock();try{
            map.clear();}finally{
            w.unlock();}}publicstaticvoidmain(String[] args){newThread(newRunnable(){@Overridepublicvoidrun(){for(int i=0; i<10; i++){Cache.put(i+"", i+"");}}}).start();newThread(newRunnable(){@Overridepublicvoid
  • 作者:程猿薇茑
  • 原文链接:https://bigbird.blog.csdn.net/article/details/121130637
    更新时间:2022-07-14 11:05:29