并发编程-09之ReentrantReadWriteLock

在这里插入图片描述

一 认识ReentrantReadWriteLock
1.1 读写锁介绍
现实中有这样一种场景:对共享资源有读和写的操作,且写操作没有读操作那么频繁(读多写少)。在没有写操作的时候,多个线程同时读一个资源没有任何问题,所以应该允许多个线程同时读取共享资源(读读可以并发);但是如果一个线程想去写这些共享资源,就不应该允许其他线程对该资源进行读和写操作了(读写,写读,写写互斥)。在读多于写的情况下,读写锁能够提供比排它锁更好的并发性和吞吐量。
针对这种场景,JAVA的并发包提供了读写锁ReentrantReadWriteLock,它内部,维护了一对相关的锁,一个用于只读操作,称为读锁;一个用于写入操作,称为写锁,描述如下:
线程获取读锁的前提条件:
● 没有其他线程的写锁
● 没有写请求或者有写请求,但调用线程和持有锁的线程是同一个。
线程进入写锁的前提条件:
● 没有其他线程的读锁
● 没有其他线程的写锁
而读写锁有以下三个重要的特性:
● 公平选择性:支持非公平(默认)和公平的锁获取方式,吞吐量非公平优于公平。
● 可重入:读锁和写锁都支持线程重入。以读写线程为例:读线程获取读锁后,能够再次获取读锁。写线程在获取写锁之后能够再次获取写锁,同时也可以获取读锁。
● 锁降级:遵循获取写锁、再获取读锁最后释放写锁的次序,写锁能够降级成为读锁。
1.2 ReentrantReadWriteLock的使用
读写锁接口
public interface ReadWriteLock {
/**
* Returns the lock used for reading.
*
* @return the lock used for reading
*/
Lock readLock();

/**
 * Returns the lock used for writing.
 *
 * @return the lock used for writing
 */
Lock writeLock();

}
ReentrantReadWriteLock类结构
ReentrantReadWriteLock是可重入的读写锁实现类。在它内部,维护了一对相关的锁,一个用于只读操作,另一个用于写入操作。只要没有 Writer 线程,读锁可以由多个 Reader 线程同时持有。也就是说,写锁是独占的,读锁是共享的。
/** Inner class providing readlock /
private final ReentrantReadWriteLock.ReadLock readerLock;
/
* Inner class providing writelock /
private final ReentrantReadWriteLock.WriteLock writerLock;
/
* Performs all synchronization mechanics */
final Sync sync;

/**
 * Creates a new {@code ReentrantReadWriteLock} with
 * default (nonfair) ordering properties.
 */
public ReentrantReadWriteLock() {
    this(false);
}

/**
 * Creates a new {@code ReentrantReadWriteLock} with
 * the given fairness policy.
 *
 * @param fair {@code true} if this lock should use a fair ordering policy
 */
public ReentrantReadWriteLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
    readerLock = new ReadLock(this);
    writerLock = new WriteLock(this);
}

public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }
public ReentrantReadWriteLock.ReadLock  readLock()  { return readerLock; }

简单使用
private static ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private static Lock readLock = readWriteLock.readLock();
private static Lock writeLock = readWriteLock.writeLock();

// 读操作上读锁
public static Object get(String key) {
    readLock.lock();
    try {
        log.info("读取数据");
        return new Object();
    }finally {
        readLock.unlock();
    }
}
//写操作上写锁
public static void put(String key,String Object) {
    writeLock.lock();
    try {
        log.info("写入数据");
    }finally {
        writeLock.unlock();
    }
}

注意:
● 读锁不支持条件变量
● 重入时升级不支持:持有读锁的情况下去获取写锁,会导致永久等待
● 重入时支持降级: 持有写锁的情况下可以去获取读锁
1.3 ReentrantReadWriteLock的应用场景
ReentrantReadWriteLock适合读多写少的场景
代码演示:
@Slf4j
public class ReentrantReadWriteLockDemo01 {

private static ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private static Lock readLock = readWriteLock.readLock();
private static Lock writeLock = readWriteLock.writeLock();

static Map<String, Object> map = new HashMap<String, Object>();

// 读操作上读锁
public static Object get(String key) {
    readLock.lock();
    try {
        log.info("加读锁成功");
        log.info("读入数据");
        return map.get(key);
    }finally {
        log.info("释放读锁");
        readLock.unlock();

    }
}
//写操作上写锁
public static void put(String key,String object) {
    writeLock.lock();
    try {
        log.info("加写锁成功");
        log.info("写入数据");
        map.put(key,object);
    }finally {
        log.info("释放写锁");
        writeLock.unlock();
    }
}

public static void main(String[] args) {


    new Thread(()->{
        Object o = get("123");
    },"线程1").start();

    new Thread(()->{
        Object o = get("123");
    },"线程2").start();


    new Thread(()->{
        put("123","1");
    },"线程3").start();


}

}
运行结果:

上述示例中,把一个非线程安全的HashMap作为缓存的实现,同时使用读写锁的读锁和写锁来保证Cache是线程安全的。在读操作get(String key)方法中,需要获取读锁,这使得并发访问该方法时不会被阻塞。写操作put(String key,Object value)方法和clear()方法,在更新 HashMap时必须提前获取写锁,当获取写锁后,其他线程对于读锁和写锁的获取均被阻塞,而只有写锁被释放之后,其他读写操作才能继续。Cache使用读写锁提升读操作的并发性,也保证每次写操作对所有的读写操作的可见性,同时简化了编程方式
1.4 ReentrantReadWriteLock的锁降级
锁降级指的是写锁降级成为读锁。如果当前线程拥有写锁,然后将其释放,最后再获取读锁,这种分段完成的过程不能称之为锁降级。锁降级是指把持住(当前拥有的)写锁,再获取到读锁,随后释放(先前拥有的)写锁的过程。锁降级可以帮助我们拿到当前线程修改后的结果而不被其他线程所破坏,防止更新丢失。
代码演示:
因为数据不常变化,所以多个线程可以并发地进行数据处理,当数据变更后,如果当前线程感知到数据变化,则进行数据的准备工作,同时其他处理线程被阻塞,直到当前线程完成数据的准备工作。
public class ReentrantReadWriteLockDemo02 {

private static final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
private static final Lock readLock = rwl.readLock();
private static final Lock writeLock = rwl.writeLock();
private static volatile boolean update = false;

public void processData() {
    readLock.lock();
    if (!update) {
        // 必须先释放读锁
        readLock.unlock();
        // 锁降级从写锁获取到开始
        writeLock.lock();
        try {
            if (!update) {
                // TODO 准备数据的流程(略)
                update = true;
            }
            readLock.lock();
        } finally {
            writeLock.unlock();
        }
        // 锁降级完成,写锁降级为读锁
    }
    try {
        //TODO  使用数据的流程(略)
    } finally {
        readLock.unlock();
    }
}

}
锁降级中读锁的获取是否必要呢?答案是必要的。主要是为了保证数据的可见性,如果当前线程不获取读锁而是直接释放写锁,假设此刻另一个线程(记作线程T)获取了写锁并修改了数据,那么当前线程无法感知线程T的数据更新。如果当前线程获取读锁,即遵循锁降级的步骤,则线程T将会被阻塞,直到当前线程使用数据并释放读锁之后,线程T才能获取写锁进行数据更新。
RentrantReadWriteLock不支持锁升级(把持读锁、获取写锁,最后释放读锁的过程)。目的也是保证数据可见性,如果读锁已被多个线程获取,其中任意线程成功获取了写锁并更新了数据,则其更新对其他获取到读锁的线程是不可见的。
二 RentrantReadWriteLock原理
思考:

  1. 读写锁肯定也是基于AQS实现的,state变量是如何记录读写状态的呢?
  2. 写锁是怎样获取和释放的?
  3. 读锁是怎样获取和释放的?
  4. 读锁和写锁的重入次数是如何记录的?

2.1 读写状态的设计
设计的精髓:用一个变量如何维护多种状态
在 ReentrantLock 中,使用 Sync ( 实际是 AQS )的 int 类型的 state 来表示同步状态,表示锁被一个线程重复获取的次数。但是,读写锁 ReentrantReadWriteLock 内部维护着一对读写锁,如果要用一个变量维护多种状态,需要采用“按位切割使用”的方式来维护这个变量,将其切分为两部分:高16为表示读,低16为表示写。
分割之后,读写锁是如何迅速确定读锁和写锁的状态呢?通过位运算。假如当前同步状态为S,那么:
● 写状态,等于 S & 0x0000FFFF(将高 16 位全部抹去)。 当写状态加1,等于S+1.
● 读状态,等于 S >>> 16 (无符号补 0 右移 16 位)。当读状态加1,等于S+(1<<16),也就是S+0x00010000
根据状态的划分能得出一个推论:S不等于0时,当写状态(S&0x0000FFFF)等于0时,则读状态(S>>>16)大于0,即读锁已被获取。

代码:
static final int SHARED_SHIFT = 16;
static final int SHARED_UNIT = (1 << SHARED_SHIFT);
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;

    /** Returns the number of shared holds represented in count  */
    static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }
    /** Returns the number of exclusive holds represented in count  */
    static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }

● exclusiveCount(int c) 静态方法,获得持有写状态的锁的次数。
● sharedCount(int c) 静态方法,获得持有读状态的锁的线程数量。不同于写锁,读锁可以同时被多个线程持有。而每个线程持有的读锁支持重入的特性,所以需要对每个线程持有的读锁的数量单独计数,这就需要用到 HoldCounter 计数器
2.2 HoldCounter计数
读锁的内在机制其实就是一个共享锁。一次共享锁的操作就相当于对HoldCounter计数器的操作。获取共享锁,则该计数器 + 1,释放共享锁,该计数器 - 1。只有当线程获取共享锁后才能对共享锁进行释放、重入操作。
static final class HoldCounter {
int count = 0;
// Use id, not reference, to avoid garbage retention
final long tid = getThreadId(Thread.currentThread());
}

    /**
     * ThreadLocal subclass. Easiest to explicitly define for sake
     * of deserialization mechanics.
     */
    static final class ThreadLocalHoldCounter
        extends ThreadLocal<HoldCounter> {
        public HoldCounter initialValue() {
            return new HoldCounter();
        }
    }

通过 ThreadLocalHoldCounter 类,HoldCounter 与线程进行绑定。HoldCounter 是绑定线程的一个计数器,而 ThreadLocalHoldCounter 则是线程绑定的 ThreadLocal。
● HoldCounter是用来记录读锁重入数的对象
● ThreadLocalHoldCounter是ThreadLocal变量,用来存放不是第一个获取读锁的线程的其他线程的读锁重入数对象
2.3 写锁加锁逻辑
public void lock() {
sync.acquire(1);
}
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
这里竞争写锁失败执行入队方法 acquireQueued(addWaiter(Node.EXCLUSIVE), arg))参照之前的文章,这里主要讲解RentrantReadWriteLock加写锁的逻辑。
protected final boolean tryAcquire(int acquires) {
/*
* Walkthrough:
* 1. If read count nonzero or write count nonzero
* and owner is a different thread, fail.
* 2. If count would saturate, fail. (This can only
* happen if count is already nonzero.)
* 3. Otherwise, this thread is eligible for lock if
* it is either a reentrant acquire or
* queue policy allows it. If so, update state
* and set owner.
*/
Thread current = Thread.currentThread();
//获取资源
int c = getState();
//获取独占状态
int w = exclusiveCount©;
//如果已经上了写锁或者读锁
if (c != 0) {
// (Note: if c != 0 and w == 0 then shared count != 0)
//w=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.已经有线程获取读锁,且获取读锁的线程不是当前线程,上写锁失败,返回false
2.有线程上了写锁且获取写锁的是当前线程,发生写锁重入,重入数加1
3.没有线程获取到锁,进入writerShouldBlock()方法
这里有公平和非公平的实现
公平实现:
final boolean writerShouldBlock() {
return hasQueuedPredecessors();
}

   public final boolean hasQueuedPredecessors() {
    // The correctness of this depends on head being initialized
    // before tail and on head.next being accurate if the current
    // thread is first in queue.
    Node t = tail; // Read fields in reverse initialization order
    Node h = head;
    Node s;
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}

这里是判断是否已经有同步队列,如果有同步队列则返回false竞争锁失败
非公平实现:
final boolean writerShouldBlock() {
return false; // writers can always barge
}
直接返回false,尝试去竞争锁,成功则把独占线程设置为当前线程
2.4 写锁释放逻辑
public void unlock() {
sync.release(1);
}
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
这里释放写锁成功执行唤醒同步队列线程的方法unparkSuccessor(h)参照之前的文章,这里主要讲解RentrantReadWriteLock释放写锁的逻辑。
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;
}
这块逻辑就是:
1.判断是否是当前线程获取锁,如果不是则抛出异常
2.写锁数量-1
3.如果写锁数量为0,解锁成功
4.如果写锁数量不为0,重入数就减1
2.5 读锁加锁逻辑
public void lock() {
sync.acquireShared(1);
}
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
这里竞争读锁失败执行入队方法 doAcquireShared(arg)m参照之前的文章,这里主要讲解RentrantReadWriteLock加读锁的逻辑。
protected final int tryAcquireShared(int unused) {
/*
* Walkthrough:
* 1. If write lock held by another thread, fail.
* 2. Otherwise, this thread is eligible for
* lock wrt state, so ask if it should block
* because of queue policy. If not, try
* to grant by CASing state and updating count.
* Note that step does not check for reentrant
* acquires, which is postponed to full version
* to avoid having to check hold count in
* the more typical non-reentrant case.
* 3. If step 2 fails either because thread
* apparently not eligible or CAS fails or count
* saturated, chain to version with full retry loop.
*/
Thread current = Thread.currentThread();
//获取状态
int c = getState();
//如果已经上了写锁且获取写锁的线程不是当前线程则获取读锁失败
if (exclusiveCount© != 0 &&
getExclusiveOwnerThread() != current)
return -1;
//获取状态量的高16位
int r = sharedCount©;
//判断是否应该阻塞
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
if (r == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
}
return 1;
}
return fullTryAcquireShared(current);
}
readerShouldBlock(),判断读锁应不应该阻塞,这里有公平和非公平实现,返回false,则不用阻塞,进行cas操作对状态量的高16位操作,这里又是三个判断,
1.如果是第一次上读锁,设置重入数为1
2.如果不是第一次上读锁,判断当前线程是否第一次上读锁的线程,是的话就加1
3.如果不是第一次上读锁的线程,设置第一次以后上读锁线程的重入数。
公平锁:
有同步队列的话,先去排队。
final boolean readerShouldBlock() {
return hasQueuedPredecessors();
}

    public final boolean hasQueuedPredecessors() {
    // The correctness of this depends on head being initialized
    // before tail and on head.next being accurate if the current
    // thread is first in queue.
    Node t = tail; // Read fields in reverse initialization order
    Node h = head;
    Node s;    
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
    }

非公平锁:
这里的判断就是同步队列中第一个线程等待上写锁!
final boolean readerShouldBlock() {
/* As a heuristic to avoid indefinite writer starvation,
* block if the thread that momentarily appears to be head
* of queue, if one exists, is a waiting writer. This is
* only a probabilistic effect since a new reader will not
* block if there is a waiting writer behind other enabled
* readers that have not yet drained from the queue.
*/
return apparentlyFirstQueuedIsExclusive();
}

   final boolean apparentlyFirstQueuedIsExclusive() {
    Node h, s;
    return (h = head) != null &&
        (s = h.next)  != null &&
        !s.isShared()         &&
        s.thread != null;
}

2.6 读锁释放逻辑
public void unlock() {
sync.releaseShared(1);
}
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
这里释放读锁成功执行唤醒同步队列线程的方法doReleaseShared()参照之前的文章,这里主要讲解RentrantReadWriteLock释放读锁的逻辑。
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
//如果是第一次上读锁的线程
if (firstReader == current) {
// assert firstReaderHoldCount > 0;
//未发生可重入,把firstReader置为空
if (firstReaderHoldCount == 1)
firstReader = null;
else
//重入数减1
firstReaderHoldCount–;
} else {
//第二次以后加读锁的线程重入数减1
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
int count = rh.count;
if (count <= 1) {
readHolds.remove();
if (count <= 0)
throw unmatchedUnlockException();
}
–rh.count;
}
//对高16根据重入次数-1.
for (;😉 {
int c = getState();
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc))
// Releasing the read lock has no effect on readers,
// but it may allow waiting writers to proceed if
// both read and write locks are now free.
return nextc == 0;
}
}
1.如果是第一个持有读锁的线程解锁,发生了可重入则可重入次数减1,如果没有发生可重入,则把第一个持有读锁的变量置为空
2.如果非第一次持有读锁的线程解锁,发生可重入,则把重入次数减1,如果没有发生可重入或者可重入次数达到1,则把当前线程重入计数器置空
3.通过cas把state变量的高16位减1

  • 21
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值