ReentrantReadWriteLock/ReentrantLock 重入锁

转自: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,一把对象锁在多个同步代码中的使用如下.



 

 

 

. 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

猜你喜欢

转载自yjph83.iteye.com/blog/2370685