ReentrantReadWriteLock-可重入读写锁

255 阅读7分钟

首先我们要知道怎么用这个锁,带着问题来进行源码分析

在接下来的代码中,我们创建了读写锁这个对象,并且从中获取了读锁和写锁,读写锁是互斥的,但是条件不一样,接下来我们就分析一下互斥条件;

1.同线程下,如果先获取读锁在获取写锁,下面这行代码中执行会成功打印读写锁2这几个字
2.同线程下,如果我们先获取读锁,在获取写锁,且读锁不进行解锁,那么线程将会阻塞
3.线程A,线程B A获取锁,且不解锁,B获取锁将被阻塞
4.线程A,线程B A获取锁,且不解锁,B获取锁将被阻塞
5.线程A,线程B A获取锁,且不解锁,B获取锁将可以成功被获取

结论:写锁在不同线程之间互斥,读锁在不同线程之间和写锁互斥,同线程之间 如果先来读锁后写锁,将会互斥,如果先写锁后读锁,将可以成功获取,并且不管读锁,还是写锁都是可重入锁

存在的疑问一: 读写锁互斥是如何实现的
存在的疑问二: 读写锁的内部实现

使用代码:接下来会通过这个使用代码来了解一下内部是如何实现的

//该行代码只是给出一个基本使用,可以自己拿java跑一下进行验证我上面的结论
//这一步选择是否是公平锁-
ReentrantReadWriteLock rlk = new ReentrantReadWriteLock(true);
//创建读锁
ReentrantReadWriteLock.ReadLock readLock = rlk.readLock();
//创建写锁
ReentrantReadWriteLock.WriteLock writeLock = rlk.writeLock();
new Thread(new Runnable() {
    @Override
    public void run() {
        writeLock.lock();
        readLock.lock();
        System.out.println("读锁写锁2");
        System.out.println(Thread.currentThread());
        readLock.unlock();
    }
}).start();
new Thread(new Runnable() {
    @Override
    public void run() {
        writeLock.lock();
        readLock.lock();
        System.out.println("读锁写锁1");
        System.out.println(Thread.currentThread() /*线程名字,优先级,组名字*/);
    }
}).start();

分析 ReentrantReadWriteLock 的架构

组织架构图:

classDiagram
AbstractQueuedSynchronizer<|-- Sync
Sync <|-- NofairSync
Sync <|-- fairSync


Lock <|--ReadLock
Lock <|--WriteLock
class ReentrantReadWriteLock{
-class Sync
-class FairSync
-class NonfairSync
-class ReadLock
-class WriteLock
}

分析类一:Sync

类的继承关系图

首先我们来一些前置知识,是来分析下面这段代码的,将过程写出来可能会更加直观的感受出来
-65535的Integer 类型的二进制数据是 11111111111111110000000000000001
65535的Integer 类型的二进制数据是 111111111111111100000000000000000
sharedCount = c >>> 16 右移16位放弃低符号位 并且符号不变
exclusiveCount = c&65535 与符号 那意味着这个只有正数 ,且小于2^8次幂

MAX_COUNT 最大数量 如果等于65536 那么共享的右移16位 如果一个那么还是0 如果是65536 那么最大就为1
EXCLUSIVE_MASK 这里是将符号作为与运算 也就是与上65535 低位与若1为1 不动高位 --

最终得出高16位标识读,16位表示写\color{#FF0000}{最终得出高16位标识读,低16位表示写}
image.png

Sync 源码部分解析


static final int SHARED_SHIFT   = 16;
static final int SHARED_UNIT    = (1 << SHARED_SHIFT); //65536
static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1; //65535 
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;//65535

// 高16共享锁
static int sharedCount(int c)    { return c >>> SHARED_SHIFT; } //无符号右移到低位 -65536 
//低16位独享锁  
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; } //与  高位


/**
*一个用于每个线程读取的计数器。作为ThreadLocal维护;缓存在cachedHoldCounter
**/
static final class HoldCounter {
    int count = 0;
    // Use id, not reference, to avoid garbage retention
    final long tid = getThreadId(Thread.currentThread());
}


//ThreadLocal子类。为了反序列化机制,最容易显式定义
static final class ThreadLocalHoldCounter
    extends ThreadLocal<HoldCounter> {
    public HoldCounter initialValue() {
        return new HoldCounter();
    }
}
//当前线程持有的可重入读锁的数量。仅在构造函数和readObject中初始化。当线程的读保持计数下降到0时移除
 private transient ThreadLocalHoldCounter readHolds;

/**
成功获取readLock的最后一个线程的持有计数。在通常情况下,
下一个要释放的线程是最后一个要获取的线程,这样可以节省ThreadLocal查找。这是非易失性的,因为它只是作为一种启发
式使用,对于线程缓存来说非常好。 可以比正在缓存读保持计数
的线程存活更长时间,但通过不保留对线程的引用来避免垃圾保留。 通过良性数据竞争访问;依赖于内存模型的最终字段和out- thin-air保证
**/
private transient HoldCounter cachedHoldCounter;


// 第一个/或者说最后一个 将共享计数保持为1的计数 
private transient Thread firstReader = null;
private transient int firstReaderHoldCount;

// 默认构造器,在初始化的时候,初始化,本地线程拥有技术,和status状态- 
Sync() {
    readHolds = new ThreadLocalHoldCounter();
    setState(getState()); // ensures visibility of readHolds
}

//读阻塞  写阻塞 子类实现 
abstract boolean readerShouldBlock();
abstract boolean writerShouldBlock();

读锁-写锁-流程: ->

一: 初始化可重入读写类


//在读写锁里面 
公平锁,直接乖乖排队,不管是写锁或者读锁
非公平锁, 写直接反感会false(不阻塞),读的话 
判断是否有排它模式中等待的线程,如果没有的话直接返回false 

public ReentrantReadWriteLock(boolean fair) {
    //初始化锁是否为公平锁
    sync = fair ? new FairSync() : new NonfairSync();
    //初始化读锁-- 是否为公平锁的Sync类传入 
    readerLock = new ReadLock(this);
    //初始化写锁 
    writerLock = new WriteLock(this);
}

//类似于这样
protected ReadLock(ReentrantReadWriteLock lock) {
    sync = lock.sync;
}
protected WriteLock(ReentrantReadWriteLock lock) {
    sync = lock.sync;
}

二: 获取读锁和写锁

//创建读锁
ReentrantReadWriteLock.ReadLock readLock = rlk.readLock();
//创建写锁
ReentrantReadWriteLock.WriteLock writeLock = rlk.writeLock();

获取读锁和写锁, 这个属于字段 在第一步的时候进行初始化了
public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }
public ReentrantReadWriteLock.ReadLock  readLock()  { return readerLock; }

三: 读锁上锁

获取读锁。 如果写锁没有被其他线程持有,则获取读锁并立即返回。 如果写锁由另一个线程持有,那么当前线程将出于线程调度的目的被禁用,并处于休眠状态,直到获得读锁

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



//这个调用的是AQS的方法,然后又因为SYNC继承了该方法, 所以tryAcquireShared 会调用到共享锁里面,判断是否获取成功  
public final void acquireShared(int arg) {
    if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);
}


protected final int tryAcquireShared(int unused) {
    /*
    介绍:
    1。如果写锁被另一个线程持有,则失败。
    2. 否则,这个线程有资格获得锁wrt状态,所以询问它是否应该
    因为队列策略而阻塞。如果没有,尝试通过套管状态和更新计数授
    予。注意,step没有检查可重入获取,这被推迟到完整版本,以
    避免在更典型的不可重入情况下必须检查hold count。
    3.如果步骤2失败,因为线程显然不合格或CAS失败或计数饱和,则使用
    完整的重试循环链接到版本。
     */
    Thread current = Thread.currentThread();
    int c = getState();
    //如果其他线程拥有写锁,那么直接返回-1 阻塞 
    if (exclusiveCount(c) != 0 &&
        getExclusiveOwnerThread() != current)
        return -1;
    int r = sharedCount(c);
    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);
}



//如果获取锁成功了 -- 那么就更新节点 进入常规排队 
private void doAcquireShared(int arg) {
    //在队列的尾巴添加一个节点
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        //不中断状态
        boolean interrupted = false;
        for (;;) {
            //如果上一个为节点是头节点
            final Node p = node.predecessor();
            if (p == head) {
                //尝试获取共享锁--上一步 
                int r = tryAcquireShared(arg);
                if (r >= 0) {
                    //如果获取成功,那么将设置头和传播
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    if (interrupted)
                        selfInterrupt();
                    failed = false;
                    return;
                }
            }
            //如果失败,或者其他人中断线程-那么将线程挂起,不然就一直循环- 
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

四:写锁上锁

//这是写锁 
public void lock() {
    sync.acquire(1);
}

/**
独占模式下获取,忽略中断。通过至少调用一次tryAcquire来实现,
成功时返回。否则,线程将排队,可能重复阻塞和解除阻塞,调用
tryAcquire直到成功。这个方法可以用来实现Lock.lock方法。 参
数: 获取参数。这个值会传递给tryAcquire,但不会被解释,可以表
示你喜欢的任何东西。
**/
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}


/**
介绍:
1.如果读计数非零或写计数非零且所有者是不同的线程,则失败。
2. 如果计数饱和,则失败。(这只会发生在count已经是非零的情况下。)
3.否则,如果该线程是可重入获取或队列策略允许,则该线程有资格获得锁。
如果是,更新state并设置owner。
**/
//相对读锁来说 写锁的尝试获取就吧暴力一点,我们可以认为只要满足判断条件 就可以获取写锁 
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;
}


/**
以独占不中断模式获取已在队列中的线程。由条件等待方法和获取方法使用。
**/
final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
        //如果获取成功,就是判断当前节点和上一个节点为头节点,
        //设置当前节点为头节点,走路线 
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

图解分析

可重入读写锁.jpg