ReentrantReadWriteLock说明

ReentrantReadWriteLock

线程安全的问题使用ReentrantLock就可以了,但是ReentrantLock是独占锁,只能有一个线程获取到锁,实际环境中可能会有读多写少的情况,此时ReentrantLock就满足不了了,而ReentrantReadWriteLock采用的读写分离的策略,允许多个线程获取读锁。

在这里插入图片描述

读写锁内部维护了一个ReadLock和WriteLock,他们依赖于Sync实现具体功能。而Sync实现于AQS,也都实现了公平和非公平两种策略。

AQS内部只维护了一个state状态,而读写锁需要读状态和写状态,一个state怎么表示两种状态呢?ReentrantReadWriteLock巧妙的使用了高低位,高16位表示读状态,也就是获取到读锁的次数;低16位表示获取到写锁的线程的可冲入次数。

static final int SHARED_SHIFT   = 16;
//共享锁(读锁)状态值65536
static final int SHARED_UNIT    = (1 << SHARED_SHIFT);
//共享锁线程最大个数65535
static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;
//排它锁(写锁)掩码。15个1
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;

//返回读锁线程数
static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }
//返回写锁线程数
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }

firstReader用来记录第一个获取到读锁的线程,firstReaderHoldCount则记录第一个获取到读锁的线程获取读锁的可重入次数。cachedHoldCounter用来记录最后一个获取读锁的线程获取读锁的可重入次数。

readHolds是ThreadLocal变量,用来存放除去第一个获取读锁线程外的其他线程获取读锁的可重入次数。ThreadLocalHoldCounter,initialValue方法返回一个HoldCounter对象

static final class ThreadLocalHoldCounter
    extends ThreadLocal<HoldCounter> {
    public HoldCounter initialValue() {
        return new HoldCounter();
    }
}
static final class HoldCounter {
    int count;          // initially 0
    // Use id, not reference, to avoid garbage retention
    final long tid = LockSupport.getThreadId(Thread.currentThread());
}

写锁的释放和获取

在ReentrantReadWriteLock中写锁使用WriteLock来实现

1、void lock()

读锁是个独占锁,只有一个线程可以获取到该锁。如果当前没有线程获取到读锁或写锁,则当前线程可以获取到写锁然后返回。如果当前已经有线程获取到读锁或写锁,则当前请求写锁的线程会被挂起。另外,写锁是可重入锁,如果当前线程已经获取到锁了,再次获取只是简单的把可重入次数加1后直接返回。

public void lock() {
    sync.acquire(1);
}

这里调用的是AQS的acquire方法,同样需要查看子类的tryAcquire方法实现

protected final boolean tryAcquire(int acquires) {
    //三种情况:
    //1、如果读锁或写锁已经被获取,且不是当前线程获取,返回失败,false
    //2、如果可重入计数超过最大值,返回失败,false
    //3、正常获得锁,返回成功,true
    Thread current = Thread.currentThread();
    //获取状态值
    int c = getState();
    //获取读锁状态
    int w = exclusiveCount(c);
    //c!=0说明读锁或写锁被某个线程获取了
    if (c != 0) {
        // (Note: if c != 0 and w == 0 then shared count != 0)
        //c!=0,W==0说明读锁被获取直接返回false
        //w!=0说明写锁已被获取,如果获取读锁的不是当前线程,则获取失败,返回false
        if (w == 0 || current != getExclusiveOwnerThread())
            return false;
        //到这里,说明当前线程已经获取了写锁,判断可重入次数是否大于最大值
        if (w + exclusiveCount(acquires) > MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
        // 设置状态值
        setState(c + acquires);
        return true;
    }
    //c==0说明是第一个来获取写锁的线程
    if (writerShouldBlock() ||
        !compareAndSetState(c, c + acquires))
        return false;
    setExclusiveOwnerThread(current);
    return true;
}

writerShouldBlock方法在公平和非公平策略中表现不同,非公平策略如下

final boolean writerShouldBlock() {
    return false; // writers can always barge
}

非公平锁每次都返回false,抢占式执行CAS操作,执行成功后设置当前线程为锁持有者并返回true。

公平策略代码如下:

final boolean writerShouldBlock() {
    return hasQueuedPredecessors();
}

会判断是否有符合条件的前驱节点,如果有就不去执行CAS操作且返回false,hasQueuedPredecessors方法详情见ReentrantLock说明。

其他获取锁的方式:lockInterruptibly()响应中断的请求方式,分析见ReentrantLock中的lockInterruptibly类似

​ tryLock()不会被阻塞的获取锁的方式

2、void unlock()

public void unlock() {
    sync.release(1);
}

调用的AQS的方法,需要子类实现

protected final boolean tryRelease(int releases) {
    //如果当前线程没有持有锁,报异常
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    //获取最新状态值
    int nextc = getState() - releases;
    //计算可重入值是否等于0
    boolean free = exclusiveCount(nextc) == 0;
    //等于0则释放锁,当前锁持有者置为null
    if (free)
        setExclusiveOwnerThread(null);
    setState(nextc);
    return free;
}

读锁的获取与释放

1、void lock()

public void lock() {
    sync.acquireShared(1);
}
public final void acquireShared(int arg) {
    if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);
}

直接看需要子类实现的tryAcquireShared方法

protected final int tryAcquireShared(int unused) {
    //如果有其他线程持有写锁,失败,进入阻塞状态;如果没有其他线程获取写锁,则获取读锁成功,AQS状态值高16位加1
    Thread current = Thread.currentThread();
    int c = getState();
    //写锁被获取,且不是当前线程获取,返回失败
    //从这里可以看出如果获取写锁的是当前线程其实是可以获取读锁的
    if (exclusiveCount(c) != 0 &&
        getExclusiveOwnerThread() != current)
        return -1;
    //获取读锁计数
    int r = sharedCount(c);
    //readerShouldBlock公平非公平策略的体现
    //公平锁判断是否有比当前线程更早的符合条件的线程,如果有此线程就阻塞
    //非公平锁判断队列第一个元素是否在获取写锁,如果是阻塞
    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 != LockSupport.getThreadId(current))
                cachedHoldCounter = rh = readHolds.get();
            else if (rh.count == 0)
                readHolds.set(rh);
            rh.count++;
        }
        return 1;
    }
    //执行到此代码说明在公平策略下有前驱节点未获取到读锁,非公平策略下说明有线程正在获取写锁
    //此方法与tryAcquireShared类似,只不过此方法是通过自旋来获取锁的
    return fullTryAcquireShared(current);
}

ReentrantReadWriteLock支持锁降级,就是在获取读锁之后再获取写锁

class CachedData {
   Object data;
   volatile boolean cacheValid;
   final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();

   void processCachedData() {
     rwl.readLock().lock();
     if (!cacheValid) {
        // Must release read lock before acquiring write lock
        rwl.readLock().unlock();
        rwl.writeLock().lock();
        try {
          // Recheck state because another thread might have
          // acquired write lock and changed state before we did.
          if (!cacheValid) {
            data = ...
            cacheValid = true;
          }
          // Downgrade by acquiring read lock before releasing write lock
          rwl.readLock().lock();
        } finally {
          rwl.writeLock().unlock(); // Unlock write, still hold read
        }
     }

     try {
       use(data);
     } finally {
       rwl.readLock().unlock();
     }
   }
 }

代码中声明了一个volatile类型的cacheValid变量,保证其可见性。首先获取读锁,如果cache不可用,则释放读锁,获取写锁,在更改数据之前,再检查一次cacheValid的值,然后修改数据,将cacheValid置为true,然后在释放写锁前获取读锁;此时,cache中数据可用,处理cache中数据,最后释放读锁。这个过程就是一个完整的锁降级的过程,目的是保证数据可见性,如果当前的线程C在修改完cache中的数据后,没有获取读锁而是直接释放了写锁,那么假设此时另一个线程T获取了写锁并修改了数据,那么C线程无法感知到数据已被修改,则数据出现错误。如果遵循锁降级的步骤,线程C在释放写锁之前获取读锁,那么线程T在获取写锁时将被阻塞,直到线程C完成数据处理过程,释放读锁。

其他获取锁的方式:lockInterruptibly()响应中断的请求方式,分析见ReentrantLock中的lockInterruptibly类似

​ tryLock()不会被阻塞的获取锁的方式

2、void unlock()

public void unlock() {
    sync.releaseShared(1);
}
public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}
protected final boolean tryReleaseShared(int unused) {
    Thread current = Thread.currentThread();
    //如果释放锁的线程是第一个获取的,则清除相关记录
    if (firstReader == current) {
        // assert firstReaderHoldCount > 0;
        if (firstReaderHoldCount == 1)
            firstReader = null;
        else
            firstReaderHoldCount--;
    } else {
        //清除记录当前锁的数据
        HoldCounter rh = cachedHoldCounter;
        if (rh == null ||
            rh.tid != LockSupport.getThreadId(current))
            rh = readHolds.get();
        int count = rh.count;
        if (count <= 1) {
            readHolds.remove();
            if (count <= 0)
                throw unmatchedUnlockException();
        }
        --rh.count;
    }
    //自旋释放锁
    for (;;) {
        int c = getState();
        int nextc = c - SHARED_UNIT;
        if (compareAndSetState(c, nextc))
            //释放锁后判断是否为0,如果是0则说明当前线程已经全部释放读锁,后面会执行doReleaseShared方法唤醒一个由于获取写锁而阻塞的现场
            return nextc == 0;
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

xhjwyy

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值