读写锁ReadWriteLock

       隐式锁Synchronized、重入锁ReetrantLock都是互斥锁、独占锁,即同一个锁只能每时每刻至多由一个线程来获持有。互斥,是一种保守策略,虽然避免了“写/写”、“读/写”冲突,但也阻止了安全的“读/读”发生。然而,在不少情况下,数据结构上的操作都是“读操作”,如果此时能放宽加锁需求,允许多个读线程同时访问数据结构,那么将极大地提升程序的性能;而这种能支持共享的锁便被设计出来 - - 读写锁ReadWriteLock。

一、读写锁ReadWriteLock介绍

1、ReadWriteLock接口

   首先,得明确一点,ReadWriteLock接口并没有继承Lock接口,可参考上一篇文章的继承结构图,ReadWriteLock 仅仅定义了两个方法,即readLock、writeLock方法;

  • Lock readLock( ): 返回用于读取操作的锁:
  • Lock writeLock( ): 返回用于写入操作的锁。

2、 读-写锁的性能

  与互斥锁相比,读-写锁允许对共享数据进行更高级别的并发访问。虽然一次只有一个线程(writer 线程)可以修改共享数据,但在许多情况下,任何数量的线程可以同时读取共享数据(reader 线程),读-写锁利用了这一点。从理论上讲,与互斥锁相比,使用读-写锁所允许的并发性增强将带来更大的性能提高。但在实践中,只有在多处理器上频繁地访问读取数据结构,才能提高性能。 而在其他情况下,读-写锁的性能却比独占锁的性能要差一点,这是因为读-写锁的复杂性更高。所以要对程序进行分析,判断读-写锁是否能提高性能。

3、ReadWriteLock接口实现时的可选策略

       尽管读-写锁的基本操作是直截了当的,但实现仍然必须作出许多决策,这些决策可能会影响给定应用程序中读-写锁的效果。这些策略的例子包括:

  • 释放优先: 当一个写入操作释放写锁时,并且队列中同时存在读线程和写线程时,那么是读线程优先获得锁,还是写线程,或者说是最先发出请求的线程
  • 读线程插队: 如果当读线程持有着读锁时,有写线程在等待,那么新到达的读线程能否立即获得访问权,还是应该在写线程后面等待?如果允许读线程插队到线程前面,那么将提高并发性,但却可能造成写线程发生饥饿问题。
  • 重入性: 读锁、写锁是否允许重入。
  • 降级: 如果一个线程持有写锁,那么它能否在不释放锁的情况下降级成一个读锁?
  • 升级: 拥有读锁的线程能否优于其他正在等待的读线程和写线程而升级成为一个写锁?在大多数的读-写锁实现中并不支持升级,因为很容易造成死锁(如果两个读线程同时升级为写锁,那么二者都不会释放读取锁)

二、读写锁的实现类 - - ReentrantReadWriteLock

ReentrantReadWriteLock实现了ReadWriteLock接口。

1、ReentrantReadWriteLock的实现策略:

  • 可重入的加锁
  • 提供公平锁与非公平锁(默认)的选择。与ReentrantLock类似,都是在构造方法中传入参数来决定,关于公平锁与非公平锁的详细可参考我的上一篇博文;
  • 读线程不能插队: 尽管当读线程持有着读锁时,写线程等待获取锁,这时候新到达的其他读线程都必须等待它们前面的写线程使用完并释放了写锁,才能获得读锁。
  • 写线程可以降级为读线程(支持降级),但是读线程不能升级为写线程(不支持升级);

2、ReentrantReadWriteLock 的读锁与写锁

ReentrantReadWriteLock实现的是ReadWriteLock接口,并没有实现Lock接口,但其管理的读锁ReentrantReadWriteLock.ReadLock 、写锁ReentrantReadWriteLock.WriteLock
都是其内部类,并且是实现Lock接口。注意以下两点:

  • 读锁、写锁都支持定时获取锁、中断锁、非阻塞获取锁,与ReetrantLock相似;
  • Condition 支持 :只能用于写锁,读锁是不支持的(因为读锁是共享锁)。写入锁提供了一个 Condition 实现,对于写入锁来说,该实现的行为与 ReentrantLock.newCondition() 提供的 Condition 实现对 ReentrantLock 所做的行为相同。
    读取锁不支持 Condition,readLock().newCondition() 会抛出 UnsupportedOperationException;

3、ReentrantReadWriteLock 提供的监视系统状态的方法

boolean hasQueuedThread(Thread thread):
查询是否给定线程正在等待获取读取或写入锁。注意,因为随时可能发生取消操作,所以返回 true 并不保证此线程将获取锁。此方法主要用于监视系统状态。
boolean hasQueuedThreads( ):
查询是否所有的线程正在等待获取读取或写入锁。注意,因为随时可能发生取消操作,所以返回 true 并不保证任何其他线程将获取锁。此方法主要用于监视系统状态。
boolean hasWaiters(Condition condition)
查询是否有些线程正在等待与写入锁有关的给定条件。注意,因为随时可能发生取消操作,所以返回 true 并不保证任何其他线程将获取锁。此方法主要用于监视系统状态。
boolean isFair( )
如果此锁将公平性设置为 ture,则返回 true。
boolean isWriteLocked( )
查询是否某个线程保持了写入锁。
boolean isWriteLockedByCurrentThread( )
查询当前线程是否保持了写入锁。

int getWaitQueueLength(Condition condition)
返回正等待与写入锁相关的给定条件的线程估计数目。注意,因为随时可能发生超时和中断,所以只能将估计值作为实际等待线程数的上限。此方法设计用于监视系统状态,而不是同步控制。
int getWriteHoldCount( )
查询当前线程在此锁上保持的重入写入锁数量。
int getQueueLength( )
返回等待获取读取或写入锁的线程估计数目。
int getReadHoldCount( )
查询当前线程在此锁上保持的重入读取锁数量。
int getReadLockCount( )
查询为此锁保持的读取锁数量。
还有几个protected方法,不再详述。

读写锁

Java提供了一个基于AQS到读写锁实现ReentrantReadWriteLock,该读写锁到实现原理是:将同步变量state按照高16位和低16位进行拆分,高16位表示读锁,低16位表示写锁。

写锁的获取与释放

写锁是一个独占锁,所以我们看一下ReentrantReadWriteLocktryAcquire(arg)的实现:

protected final boolean tryAcquire(int acquires) {
            Thread current = Thread.currentThread();
            int c = getState();
            int w = exclusiveCount(c);
            if (c != 0) {
                if (w == 0 || current != getExclusiveOwnerThread())
                    return false;
                if (w + exclusiveCount(acquires) > MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
                // Reentrant acquire
                setState(c + acquires);
                return true;
            }
            if (writerShouldBlock() ||
                !compareAndSetState(c, c + acquires))
                return false;
            setExclusiveOwnerThread(current);
            return true;
        }

 上述代码的处理流程已经非常清晰:

  1.     获取同步状态,并从中分离出低16为的写锁状态
  2.     如果同步状态不为0,说明存在读锁或写锁
  3.     如果存在读锁(c !=0 && w == 0),则不能获取写锁(保证写对读的可见性)
  4.     如果当前线程不是上次获取写锁的线程,则不能获取写锁(写锁为独占锁)
  5.     如果以上判断均通过,则在低16为写锁同步状态上利用CAS进行修改(增加写锁同步状态,实现可重入)
  6.     将当前线程设置为写锁的获取线程

写锁的释放过程与独占锁基本相同:

protected final boolean tryRelease(int releases) {
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            int nextc = getState() - releases;
            boolean free = exclusiveCount(nextc) == 0;
            if (free)
                setExclusiveOwnerThread(null);
            setState(nextc);
            return free;
        }

 在释放的过程中,不断减少读锁同步状态,只为同步状态为0时,写锁完全释放。

读锁的获取与释放

读锁是一个共享锁,获取读锁的步骤如下:

  1.     获取当前同步状态
  2.     计算高16为读锁状态+1后的值
  3.     如果大于能够获取到的读锁的最大值,则抛出异常
  4.     如果存在写锁并且当前线程不是写锁的获取者,则获取读锁失败
  5.     如果上述判断都通过,则利用CAS重新设置读锁的同步状态

读锁的获取步骤与写锁类似,即不断的释放写锁状态,直到为0时,表示没有线程获取读锁。

在JDK1.6以后,读锁的实现比上述过程更加复杂,有兴趣的同学可以看一下最新的后去读锁的源码。

@ Example1: 读写锁的使用实例

  在使用某些种类的 Collection 时,可以使用 ReentrantReadWriteLock 来提高并发性。通常,在预期 collection 很大,读取者线程访问它的次数多于写入者线程,并且 entail 操作的开销高于同步开销时,这很值得一试。例如,以下是一个使用 TreeMap 的类,预期它很大,并且能被同时访问。

class RWDictionary {
    private final Map<String, Data> m = new TreeMap<String, Data>();
    private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
    private final Lock r = rwl.readLock();
    private final Lock w = rwl.writeLock();

    public Data get(String key) {
        r.lock();
        try { return m.get(key); }
        finally { r.unlock(); }
    }
    public String[] allKeys() {
        r.lock();
        try { return m.keySet().toArray(); }
        finally { r.unlock(); }
    }
    public Data put(String key, Data value) {
        w.lock();
        try { return m.put(key, value); }
        finally { w.unlock(); }
    }
    public void clear() {
        w.lock();
        try { m.clear(); }
        finally { w.unlock(); }
    }
 }

@ Example2: ReentrantReadWriteLock的写锁降级

   public static int count = 5;
   
    public static void main(String[] args) {
        //创建一个非公平的读写锁
        ReadWriteLock lock = new ReentrantReadWriteLock(false);
        
        Thread threadA = new Thread("threadA"){
            @Override
            public void run() {
                //获取读锁
                lock.readLock().lock();
                System.out.println("成功获取读锁,count的值是:"+count);
                if(count<10){
                    lock.readLock().unlock();
                    //在获取写锁前,必须先释放读锁
                    lock.writeLock().lock();
                    System.out.println("成功获取写锁");
                    count += count*3;
                    
                    //获取读锁,此时没有释放写锁,即为写锁降级为读锁
                    lock.readLock().lock();
                    //成功获取读锁,写锁降级成功,释放写锁
                    lock.writeLock().unlock();
                    System.out.println("写锁成功降级成读锁,count的值是:"+count);
                }
            }
        };
        threadA.start();
    }

     运行结果:
    成功获取读锁,count的值是:5
    成功获取写锁
    写锁成功降级成读锁,count的值是:20

       在线程持有读锁的情况下,该线程不能取得写锁(因为获取写锁的时候,如果发现当前的读锁被占用,就马上获取失败,不管读锁是不是被当前线程持有)。

       在线程持有写锁的情况下,该线程可以继续获取读锁(获取读锁时如果发现写锁被占用,只有写锁没有被当前线程占用的情况才会获取失败)。

       仔细想想,这个设计是合理的:因为当线程获取读锁的时候,可能有其他线程同时也在持有读锁,因此不能把获取读锁的线程“升级”为写锁;而对于获得写锁的线程,它一定独占了读写锁,因此可以继续让它获取读锁,当它同时获取了写锁和读锁后,还可以先释放写锁继续持有读锁,这样一个写锁就“降级”为了读锁。

    读写锁是在重入锁ReentrantLock基础上的一大改造,其通过在重入锁上维护一个读锁一个写锁实现的。对于ReentrantLock和ReentrantreadWriteLock的使用需要在开发者自己根据实际项目的情况而定。对于读写锁当读的操作远远大于写操作的时候会增加程序很高的并发量和吞吐量。虽说在高并发的情况下,读写锁的效率很高,但是同时又会存在一些问题,比如当读并发很高时读操作长时间占有锁,导致写锁长时间无法被获取而导致的线程饥饿问题,因此在JDK1.8中又在ReentrantReadWriteLock的基础上新增了一个读写并发锁StampLock。

综上:

        一个线程要想同时持有写锁和读锁,必须先获取写锁再获取读锁;写锁可以“降级”为读锁;读锁不能“升级”为写锁。

读写锁详情:https://www.cnblogs.com/xiaoxi/p/9140541.html

猜你喜欢

转载自blog.csdn.net/JinXYan/article/details/88864262