欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

ReentrantReadWriteLock/ReentrantLock 重入锁

程序员文章站 2022-07-13 09:16:00
...

转自:http://blog.csdn.net/vking_wang/article/details/9952063 (【Java线程】锁机制:synchronized、Lock、Condition)

 

.ReentrantReadWriteLock(读写锁)的使用

Lock比传统线程模型中的synchronized方式更加面向对象,与生活中的锁类似,锁本身也应该是一个对象。两个线程执行的代码片段要实现同步互斥的效果,它们必须用同一个Lock对象。

读写锁:分为读锁和写锁,多个读锁不互斥(共享读锁),读锁与写锁互斥(互斥写锁),这是由jvm自己控制的,你只要上好相应的锁即可。如果你的代码只读数据,可以很多人同时读,但不能同时写,那就上读锁;如果你的代码修改数据,只能有一个人在写,且不能同时读取,那就上写锁。总之,读的时候上读锁,写的时候上写锁!

ReentrantReadWriteLock会使用两把锁来解决问题,一个读锁,一个写锁

线程进入读锁的前提条件:

    没有其他线程的写锁,

    没有写请求或者有写请求,但调用线程和持有锁的线程是同一个

 

线程进入写锁的前提条件:

    没有其他线程的读锁

    没有其他线程的写锁

ReentrantReadWriteLock,首先要做的是与ReentrantLock划清界限。它和后者都是单独的实现,彼此之间没有继承或实现的关系

然后就是总结这个锁机制的特性了: 

     (a).重入方面其内部的WriteLock可以获取ReadLock,但是反过来ReadLock想要获得WriteLock则永远都不要想。 

     (b).WriteLock可以降级为ReadLock,顺序是:先获得WriteLock再获得ReadLock,然后释放WriteLock,这时候线程将保持Readlock的持有。反过来ReadLock想要升级为WriteLock则不可能,为什么?参看(a)

     (c).ReadLock可以被多个线程持有并且在作用时排斥任何的WriteLock,而WriteLock则是完全的互斥。这一特性最为重要,因为对于高读取频率而相对较低写入的数据结构,使用此类锁同步机制则可以提高并发量。 

     (d).不管是ReadLock还是WriteLock都支持Interrupt,语义与ReentrantLock一致。 

     (e).WriteLock支持Condition并且与ReentrantLock语义一致,而ReadLock则不能使用Condition,否则抛出UnsupportedOperationException异常。  

 

---------- 以上摘抄自读写锁的使用http://www.cnblogs.com/liuling/p/2013-8-21-03.html

 

ReentrantReadWriteLock synchronized 的区别

 

1.拆分读写锁场景. 提高并发量:

ReadLock可以被多个线程持有并且在作用时排斥任何的WriteLock,而WriteLock则是完全的互斥。这一特性最为重要,因为对于高读取频率而相对较低写入的数据结构,使用此类锁同步机制则可以提高并发量。 

2.不管是ReadLock还是WriteLock都支持Interrupt,语义与ReentrantLock一致。 

3.写锁支持Condition重入.

 

使用ReentrantReadWriteLock 

 

1,放在成员变量的位置.

2,如果声明为static 则表示是类锁.对实例对象共享.

   如果没有声明为static 则表示是对象锁. 对单列对象共享.对多个new出来的对象不共享.

3,使用时声明为final. 表示不允许修改引用指向.

4,一把对象锁在多个同步代码中的使用如下.


ReentrantReadWriteLock/ReentrantLock 重入锁
            
    
    博客分类: java/开源框架 java锁重入锁 
 

 

 

. ReentrantLock(重入锁)的使用

synchronized原语和ReentrantLock在一般情况下没有什么区别,但是在非常复杂的同步应用中,请考虑使用ReentrantLock,

特别是遇到下面几种种需求的时候。 

1.某个线程在等待一个锁的控制权的这段时间需要中断 

2.需要分开处理一些wait-notify,ReentrantLock里面的Condition应用,能够控制notify哪个线程 

3.具有公平锁功能,每个到来的线程都将排队等候 

 

先说第一种情况

 

ReentrantLock的lock机制有2种,忽略中断锁和响应中断锁,这给我们带来了很大的灵活性。比如:如果A、B2个线程去竞争锁,A线程得到了锁,B线程等待,但是A线程这个时候实在有太多事情要处理,就是一直不返回,B线程可能就会等不及了,想中断自己,不再等待这个锁了,转而处理其他事情。

这个时候ReentrantLock就提供了2种机制,

第一,B线程中断自己(或者别的线程中断它),但是ReentrantLock不去响应,继续让B线程等待,你再怎么中断,我全当耳边风(synchronized原语就是如此);

第二,B线程中断自己(或者别的线程中断它),ReentrantLock处理了这个中断,并且不再等待这个锁的到来,完全放弃。

 

ReentrantLock是一个互斥的同步器,其实现了接口Lock,里面的功能函数主要有:

1. ‍lock() -- 阻塞模式获取资源

2. ‍lockInterruptibly() -- 可中断模式获取资源

3. ‍tryLock() -- 尝试获取资源

4. tryLock(time) -- 在一段时间内尝试获取资源

5. ‍unlock() -- 释放资源

 

ReentrantLock实现Lock有两种模式即公平模式和不公平模式

Concurrent包下的同步器都是基于AQS框架,在ReentrantLock里面会看到这样三个类

-----------------------------------------------------------------------

static abstract class Sync extends AbstractQueuedSynchronizer {

    abstract void lock();

    final boolean nonfairTryAcquire(int acquires) { ... }

    protected final boolean tryRelease(int releases) { ... }

}

 

-----------------------------------------------------------------------

final static class NonfairSync extends Sync {

    protected final boolean tryAcquire(int acquires) { ... }

    final void lock() { ... }

}

-----------------------------------------------------------------------

final static class FairSync extends Sync {

    final void lock() { ... }

    protected final boolean tryAcquire(int acquires) { ... }

}

-----------------------------------------------------------------------

再回归到ReentrantLock对Lock的实现上

0. ‍ReentrantLock实例化

   ReentrantLock有个属性sync,实际上对Lock接口的实现都是包装了一下这个sync的实现

   如果是公平模式则创建一个FairSync对象,否则创建一个NonfairSync对象,默认是不公平模式

1. lock() 调用sync.lock()

   公平模式下:直接走AQSacquire函数,此函数的逻辑走一次tryAcquire,如果成功

   线程拜托同步器的控制,否则加入NODE链表,进入acquireQueuedtryAcquire,休眠,被唤醒的轮回

   不公平模式下和公平模式下逻辑大体上是一样的,不同点有两个:

   a. 在执行tryAcquire之前的操作,不公平模式会直接compareAndSetState(0, 1)原子性的设置AQS的资源

   0表示目前没有线程占据资源,则直接抢占资源,不管AQSNODE链表的FIFO原则

   b. tryAcquire的原理不一样,不公平模式的tryAcquire只看compareAndSetState(0, 1)能否成功

   而公平模式还会加一个条件就是此线程对于的NODE是不是NODE链表的第一个

   c. 由于tryAcquire的实现不一样,而公平模式和不公平模式在lock期间走的逻辑是一样的(AQSacquireQueued的逻辑)

   d. 对于一个线程在获取到资源后再调用lock会导致AQS的资源做累加操作,同理线程要彻底的释放资源就必须同样

   次数的调用unlock来做对应的累减操作,因为对应ReentrantLock来说tryAcquire成功一个必须的条件就是compareAndSetState(0, 1)

   e. 由于acquireQueued过程中屏蔽了线程中断,只是在线程拜托同步器控制后,如果记录线程在此期间被中断过则标记线程的

   中断状态

2. ‍lockInterruptibly() 调用sync.acquireInterruptibly(1),上一篇文章讲过AQS的核心函数,这个过程和acquireQueued

   是一样的,只不过在阻塞期间如果被标记中断则线程在park期间被唤醒,然后直接退出那个轮回,抛出中断异常

   由于公平模式和不公平模式下对tryAcquire的实现不一样导致lockInterruptibly逻辑也是不一样

3. tryLock() 函数只是尝试性的去获取一下锁,跟tryAcquire一样,这两种模式下走的代码一样都是公平模式下的代码

4. tryLock(time) 调用sync.tryAcquireNanos(time),上一篇文章讲过AQS的核心函数,这个过程和acquireQueued一样,

   a. 在阻塞前会先计算阻塞的时间,进入休眠

   b. 如果被中断则会判断时间是否到了

      1. 如果没到则且被其他线程设置了中断标志,退出那个轮回,抛出中断异常,如果没有被设置中断标记则是前一个线程

      释放了资源再唤醒了它,其继续走那个轮回,轮回中,如果tryAcquire成功则摆脱了同步器的控制,否则回到a

      2. 如果时间到了则退出轮回,获取资源失败

5. ‍unlock() 调用sync.release(1),上一篇文章讲过AQS的核心函数,release函数会调用Sync实现的tryRelease函数来判断

   释放资源是否成功,即Sync.tryRelease函数,其逻辑过程是

   a. 首先判断目前占据资源的线程是不是调用者,如果不是会抛出异常IllegalMonitorStateException

   b. 如果是则进行AQS资源的减1逻辑,如果再减1AQS资源变成0则表示调用线程测得放弃了此锁,返回给release的值的TRUE

   release会唤醒下一个线程

-----------------------------------------------------------------------

整体来看ReentrantLock互斥锁的实现大致是

1. 自己实现AQS的tryAcquire和tryRelease逻辑,tryAcquire表示尝试去获取锁,tryRelease表示尝试去释放锁

2. ReentrantLock对lock(),trylock(),trylock(time),unlock()的实现都是使用AQS的框架,然后AQS的框架又返回调用

ReentrantLock实现的tryAcquire和tryRelease来对线程是否获取锁和释放锁成功做出依据判断

 

---------以上摘抄自重入锁的使用 http://blog.csdn.net/eclipser1987/article/details/7301828

 

 

第二种情况: 线程间通信Condition

 

Condition可以替代传统的线程间通信,await()替换wait(),用signal()替换notify(),用signalAll()替换notifyAll()

——为什么方法名不直接叫wait()/notify()/nofityAll()?因为Object的这几个方法是final的,不可重写!

 

传统线程的通信方式,Condition都可以实现。

注意,Condition是被绑定到Lock上的,要创建一个LockCondition必须用newCondition()方法。

 

Condition的强大之处在于它可以为多个线程间建立不同的Condition

JDK文档中的一个例子:

假定有一个绑定的缓冲区,它支持 put  take 方法。如果试图在空的缓冲区上执行 take 操作,则在某一个项变得可用之前,线程将一直阻塞;如果试图在满的缓冲区上执行 put 操作,则在有空间变得可用之前,线程将一直阻塞。

我们喜欢在单独的等待 set 中保存put 线程和take 线程,这样就可以在缓冲区中的项或空间变得可用时利用最佳规划,一次只通知一个线程。

可以使用两个Condition 实例来做到这一点。

 

——其实就是java.util.concurrent.ArrayBlockingQueue的功能

  1. class BoundedBuffer {  
  1.   final Lock lock = new ReentrantLock();          //锁对象  
  2.   final Condition notFull  = lock.newCondition(); //写线程锁  
  3.   final Condition notEmpty = lock.newCondition(); //读线程锁  
  4.   
  5.   final Object[] items = new Object[100];//缓存队列  
  6.   int putptr;  //写索引  
  7.   int takeptr; //读索引  
  8.   int count;   //队列中数据数目  
  9.   
  10.   //  
  11.   public void put(Object x) throws InterruptedException {  
  12.     lock.lock(); //锁定  
  13.     try {  
  14.       // 如果队列满,则阻塞<写线程>  
  15.       while (count == items.length) {  
  16.         notFull.await();   
  17.       }  
  18.       // 写入队列,并更新写索引  
  19.       items[putptr] = x;   
  20.       if (++putptr == items.length) putptr = 0;   
  21.       ++count;  
  22.   
  23.       // 唤醒<读线程>  
  24.       notEmpty.signal();   
  25.     } finally {   
  26.       lock.unlock();//解除锁定   
  27.     }   
  28.   }  
  29.   
  30.   //   
  31.   public Object take() throws InterruptedException {   
  32.     lock.lock(); //锁定   
  33.     try {  
  34.       // 如果队列空,则阻塞<读线程>  
  35.       while (count == 0) {  
  36.          notEmpty.await();  
  37.       }  
  38.   
  39.       //读取队列,并更新读索引  
  40.       Object x = items[takeptr];   
  41.       if (++takeptr == items.length) takeptr = 0;  
  42.       --count;  
  43.   
  44.       // 唤醒<写线程>  
  45.       notFull.signal();   
  46.       return x;   
  47.     } finally {   
  48.       lock.unlock();//解除锁定   
  49.     }   
  50.   }   

 

优点:

假设缓存队列中已经存满,那么阻塞的肯定是写线程,唤醒的肯定是读线程,相反,阻塞的肯定是读线程,唤醒的肯定是写线程。

 

那么假设只有一个Condition会有什么效果呢?缓存队列中已经存满,这个Lock不知道唤醒的是读线程还是写线程了,如果唤醒的是读线程,皆大欢喜,如果唤醒的是写线程,那么线程刚被唤醒,又被阻塞了,这时又去唤醒,这样就浪费了很多时间。

 

 

----- 以上摘抄自线程间通信 http://blog.csdn.net/vking_wang/article/details/9952063

  • ReentrantReadWriteLock/ReentrantLock 重入锁
            
    
    博客分类: java/开源框架 java锁重入锁 
  • 大小: 104.4 KB