七:深入理解 ReentrantReadWriteLock —— 读写锁

1、ReentrantReadWriteLock 入门

1.1、概念

ReentrantReadWriteLock:读写锁。它表示两个锁,一个是读操作相关的锁,称为共享锁;一个是写相关的锁,称为排他锁。适用于读多写少的场景(如果用独占锁,效率及其低下),提高并发性能,但也需要更多的内存和处理器时间来维护状态信息

在没有写操作的情况下,多个线程同时读一个资源没有任何问题,所以应该允许多个线程同时读取共享资源;但是,如果一个线程想去写这些共享资源,就不应该允许其他线程对该资源进行读和写的操作了

  • 并发:读读
  • 互斥:读写、写读、写写

ReentrantLockReentrantReadWriteLock 区别

  • ReentrantLock:互斥锁,它允许同一线程对共享资源进行重入,即该线程在获得锁后可以再次获得该锁而不被阻塞
  • ReentrantReadWriteLock:由一个读锁和一个写锁组成,它允许多个线程同时读取共享资源,但只允许一个线程写入共享资源

当前线程获取读锁的条件:

  • 没有其它线程的写锁
  • 没有写请求或者有写请求,但当前线程和持有锁的线程是同一个线程

当前线程获取写锁的条件:

  • 没有其它线程的读锁
  • 没有其它线程的写锁

三个重要的特性:

  1. 公平选择性:支持非公平(默认)和公平的锁获取方式,吞吐量还是非公平优于公平
  2. 可重入:读锁和写锁都支持线程重入。读线程获取读锁后,能够再次获取读锁,但是不能获取写锁;写线程在获取写锁之后能够再次获取写锁,同时也可以获取读锁
  3. 锁降级:线程获取写锁之后获取读锁,再释放写锁,这样实现了写锁变为读锁,也叫锁降级

1.2、案例

1.2.1、写写互斥

public class Test {

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

    public static void put(String key, Object value) throws InterruptedException {
        System.out.println("开始----" +Thread.currentThread().getName());
        lock.writeLock().lock();
        try {
            System.out.println("执行------" +Thread.currentThread().getName());
            map.put(key, value);
            Thread.sleep(1000);
        } finally {
            lock.writeLock().unlock();
            System.out.println("结束------" +Thread.currentThread().getName());
        }
    }

    public static Object get(String key) {
        System.out.println("开始----" +Thread.currentThread().getName());
        lock.readLock().lock();
        try {
            System.out.println("执行------" +Thread.currentThread().getName());
            return map.get(key);
        } finally {
            lock.readLock().unlock();
            System.out.println("执行------" +Thread.currentThread().getName());
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Runnable writeTask = () -> {
            try {
                put("1", 1);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        };
        new Thread(writeTask).start();
        new Thread(writeTask).start();
    }
}

执行结果:

开始----Thread-1
开始----Thread-0
执行------Thread-1
结束------Thread-1
执行------Thread-0
结束------Thread-0

根据结果可以:线程 1、0 同时执行,但线程 1 获取写锁后,线程 0 阻塞,直至线程 1 执行完释放写锁之后,线程 0 获取写锁执行,最后释放写锁

当然,读者也可以去测试 读写、读读 等。

1.2.2 锁降级

锁降级先获取写锁,然后再获取读锁,最后释放写锁的过程。在这个过程中,线程可以先访问共享资源,然后放弃写权限,转而访问读资源【可以避免写操作期间读操作的阻塞,提高并发性能;由写锁降为读锁,释放写锁后,仍然持有读锁】。

还是上述那个例子:

public static void put(String key, Object value) {
    lock.writeLock().lock();
    try {
        map.put(key, value);
        
        // 获取读锁
        lock.readLock().lock();
    } finally {
        lock.writeLock().unlock();
        // 此处如果不释放读锁,其他线程获取写锁时将被阻塞
        //lock.readLock().unlock();
    }
}

2、ReentrantReadWriteLock 源码解析

2.1、属性

public class ReentrantReadWriteLock implements ReadWriteLock {
	// 读锁
	private final ReentrantReadWriteLock.ReadLock readerLock;
	// 写锁
	private final ReentrantReadWriteLock.WriteLock writerLock;
	// 同步机制
	final Sync sync;
}

2.2、构造方法

public ReentrantReadWriteLock() {
    this(false);
}

public ReentrantReadWriteLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
    readerLock = new ReadLock(this);
    writerLock = new WriteLock(this);
}

默认非公平机制

2.3、内部类

// AQS:分公平、非公平
abstract static class Sync extends AbstractQueuedSynchronizer {
    // 尝试获取锁
	protected final boolean tryAcquire(int acquires) {}
	// 尝试释放锁
	protected final boolean tryRelease(int releases) {}
	protected final int tryAcquireShared(int unused) {}
	
	// 写锁是否应该阻塞
	abstract boolean writerShouldBlock();
	abstract boolean readerShouldBlock();
	
	// 每个读线程持有的计数:Sync 构造函数中初始化;读线程计数为 0 时删除
	private transient ThreadLocalHoldCounter readHolds;
	// 缓存最后一个读锁的计数
	private transient HoldCounter cachedHoldCounter;
	// 记录第一个读锁
	private transient Thread firstReader = null;
	// 第一个读锁的计数器
	private transient int firstReaderHoldCount;
	
	// 内部类,继承了 ThreadLocal,和当前线程绑定
	static final class ThreadLocalHoldCounter extends ThreadLocal<HoldCounter> {
        public HoldCounter initialValue() {
            return new HoldCounter();
        }
    }
    
    // 内部类:读线程的计数器
    static final class HoldCounter {
        int count = 0;
        final long tid = getThreadId(Thread.currentThread());
    }
}

// 非公平
static final class NonfairSync extends Sync {
	
	final boolean writerShouldBlock() {
        return false;
    }
}

// 公平
static final class FairSync extends Sync {
	
	final boolean writerShouldBlock() {
        return hasQueuedPredecessors();
    }
}

// 读锁
public static class ReadLock implements Lock {
}

// 写锁
public static class WriteLock implements Lock {
	private final Sync sync;
	// 加锁:通过 Sync#acquire() 方法加锁
	public void lock() {}
}

2.4、读写状态的设计 —— 按位切割使用

在分析 ReentrantLock 的时候,Sync 内部类类是继承于 AQS,以 int state 为线程加锁状态:state == 0 表示未加锁;state > 0 表示已加锁

同样,ReentrantReadWriteLock 也是继承于 AQS 来实现同步,那 int state 是如何同时来区分读锁和写锁的呢?

如果要用一个变量维护多种状态,需要采用 “按位切割使用” 的方式来维护这个变量,将其切分为两部分:高 16 为表示读低 16 为表示写

查看 Sync 类中的静态变量:

abstract static class Sync extends AbstractQueuedSynchronizer {
	
	// 高16位为读锁,低16位为写锁
	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;
	
	// 获取读锁的数量【高 16 位:移位】
	static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }
	// 获取写锁的数量【& 与运算】
	static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
	
}

移位操作符:在二进制下进行移位【十进制转化为二进制】

  • <<左移。如果左移过程中超过了 32 位,高位就会舍弃,低位补零【最终结果:十进制数 * (2 ^ n)】。如:3 << 2 = 3 * (2 ^ 2) = 12
  • >>右移。低位会舍弃,高位补零【最终结果:十进制数 / (2 ^ n)】
  • >>>右移零填充运算符【用来执行无符号位移的位运算符】:将一个数的二进制表示向右移动,并用 0 填充左侧的空位

所以,通过移位操作符:

  • state【高 16:读;低 16:写】:00000000 00000000 00000000 00000000
  • SHARED_UNIT:                         00000000 00000001 00000000 00000000
  • MAX_COUNT:                            00000000 00000000 11111111  11111111
  • EXCLUSIVE_MASK:                  00000000 00000000 11111111 11111111

2.5、【写锁】加锁方法 lock() —— ReentrantReadWriteLock.WriteLock

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

调用 AQS#acquire() 方法【Sync 没有重写此方法,父类有】

2.4.1、acquire() 方法 —— AQS

public final void acquire(int arg) {
    if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) {
    	selfInterrupt();
   	}
}

acquire() 方法在之前的文章 六:ReentrantLock —— 可重入锁 中已经分析过:只有 tryAcquire() 方法供子类实现。这里实现的子类是 ReentrantReadWriteLock.Sync

2.4.1.1、tryAcquire() 方法 —— AQS,由 ReentrantReadWriteLock.Sync 实现
// 尝试获取写锁
protected final boolean tryAcquire(int acquires) {     
    Thread current = Thread.currentThread();
    // 获取 state 的值
    int c = getState();
    // 获取写锁的数量
    int w = exclusiveCount(c);
    if (c != 0) {
    	// 要么就是读锁,要么就是写锁
    	// 1.如果没有写锁【有读锁】,不管是否当前线程是持有锁线程,直接返回【持有读锁线程无法再持有写锁:读写互斥】
    	// 2.如果是写锁,但是不是当前持有锁线程,直接返回【写写互斥】
        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;
    }
    // 没有读锁、写锁,竞争写锁
    if (writerShouldBlock() || !compareAndSetState(c, c + acquires)) {
    	return false;
   	}
    setExclusiveOwnerThread(current);
    return true;
}
2.4.1.1.1 writerShouldBlock() 方法 —— ReentrantReadWriteLock.Sync,由子类实现

分公平锁、非公平锁

  • 公平锁:由 AQS#hasQueuedPredecessors() 方法决定【同步等待队列中有线程等待(返回 true),则不竞争锁,直接返回 false;否则,竞争写锁,进行 CAS 操作,返回 true】
  • 非公平锁:直接抢占,进行 CAS 操作,返回 true

2.6、【写锁】解锁方法 unlock() —— ReentrantReadWriteLock.WriteLock

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

调用 AQS#release() 方法【Sync 没有重写此方法,父类有】

2.6.1、release() 方法 —— AQS

public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

tryRelease() 方法被 Sync 类实现

2.6.1.1、tryRelease() 方法 —— AQS,由 ReentrantReadWriteLock.Sync 实现
protected final boolean tryRelease(int releases) {
    if (!isHeldExclusively()) {
    	// 如果当前线程不是持有锁线程,则抛异常
    	throw new IllegalMonitorStateException();
    }  
    int nextc = getState() - releases;
    // boolean free = (exclusiveCount(nextc) == 0);
    // 判断写锁数量是否为 0【重入】
    boolean free = exclusiveCount(nextc) == 0;
    if (free) {
    	setExclusiveOwnerThread(null);
    }
    setState(nextc);
    return free;
}

2.7、【读锁】加锁方法 lock() —— ReentrantReadWriteLock.ReadLock

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

调用 AQS#acquireShared() 方法

2.7.1、acquireShared() 方法 —— AQS

public final void acquireShared(int arg) {
    if (tryAcquireShared(arg) < 0) {
    	doAcquireShared(arg);
   	}
}
2.7.1.1、tryAcquireShared() 方法 —— AQS,由 ReentrantReadWriteLock.Sync 实现
protected final int tryAcquireShared(int unused) {
    Thread current = Thread.currentThread();
    int c = getState();
    if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current) {
    	// 如果有写锁,且当前线程不是持有写锁线程,则返回 -1【写读互斥】
    	return -1;
   	}
    int r = sharedCount(c);
    // 如果获取到读锁,则返回 1
    // 1.readerShouldBlock() 方法
    // 公平锁:有线程排队,返回 true,直接跳过;否则,返回 false
    // 非公平锁:为防止写线程饥饿问题,所以判断 head 节点的后驱节点是否为写锁,如果是,返回 true,直接跳过;否则,返回 false
    // 2.compareAndSetState(c, c + SHARED_UNIT):CAS 操作 state 的高 16 位(读锁)
    if (!readerShouldBlock() && r < MAX_COUNT && compareAndSetState(c, c + SHARED_UNIT)) {
        if (r == 0) {
        	// 如果之前没有读锁,则这是第一个线程获取读锁,计数器记为 1,且用 firstReader 执向
            firstReader = current;
            firstReaderHoldCount = 1;
        } else if (firstReader == current) {
        	// 如果之前有读锁,且当前线程等于 firstReader(第一个获取读锁的线程),则自加
            firstReaderHoldCount++;
        } else {
        	// 缓存最后一个读线程的计数
            HoldCounter rh = cachedHoldCounter;
            // 我是第 2 个拿到读锁的(rh == null)| 我是第 N(N > 2)个拿到读锁的,且当前线程不是最后一个线程,那么将 cachedHoldCounter 设置为最后一个线程
            if (rh == null || rh.tid != getThreadId(current)) {
            	cachedHoldCounter = rh = readHolds.get();
            } else if (rh.count == 0) {
            	// 我是第 N(N > 2)个拿到读锁的,当前线程是最后一个线程,且计数器为 0 【读锁释放】
				readHolds.set(rh);
			}
			// 计数器自增
            rh.count++;
        }
        return 1;
    }
    // 用于 CAS 操作失败【N (N > 2)个线程同时执行 CAS 操作】
    return fullTryAcquireShared(current);
}
  1. 如果写锁被其它线程持有,则获取读锁失败,返回 -1
  2. 判断是否因同步队列策略而阻塞【公平锁:有线程排队,跳过逻辑;否则,执行逻辑;非公平锁:为防止写线程饥饿问题,所以判断 head 节点的后驱节点是否为写锁,如果是,跳过逻辑;否则,执行逻辑】。如果不阻塞,就执行 CAS 操作更新 state
  3. 如果步骤 2 失败,因为线程显然不符合条件【CAS 失败或计数饱和】,则使用完整的重试循环链接到版本
2.7.1.1.1、readerShouldBlock() 方法 —— ReentrantReadWriteLock.Sync,由子类实现

公平锁:由 hasQueuedPredecessors() 方法决定【同步等待队列中有线程等待(返回 true),则不竞争锁,直接返回 false;否则,竞争写锁,进行 CAS 操作,返回 true】

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

非公平锁:由 apparentlyFirstQueuedIsExclusive() 方法决定【如果同步等待队列中不为空,且 head 节点的后继节点为【独占式】节点,则返回 true;否则,返回 false;如果有读锁,写锁是无法获取的,防止写线程饥饿,一直阻塞(读锁是共享的,可以一直获取)】

final boolean readerShouldBlock() {
    return apparentlyFirstQueuedIsExclusive();
}
2.7.1.1.1.1、apparentlyFirstQueuedIsExclusive() 方法 —— AQS
final boolean apparentlyFirstQueuedIsExclusive() {
    Node h, s;
    return (h = head) != null && (s = h.next)  != null && !s.isShared() && s.thread != null;
}

如果同步等待队列中不为空,且 head 节点的后继节点为【独占式】节点,则返回 true;否则,返回 false

2.7.1.1.2、fullTryAcquireShared() 方法 —— ReentrantReadWriteLock.Sync
final int fullTryAcquireShared(Thread current) {
    HoldCounter rh = null;
    // 自旋
    for (;;) {
        int c = getState();
        // 是否存在写锁
        if (exclusiveCount(c) != 0) {
            if (getExclusiveOwnerThread() != current) {
            	// 如果存在写锁,且当前线程不是持有写锁的线程,返回 -1;如果是,就会去持有读锁
            	return -1;
           	}
        // 公平:有排队的,进入逻辑;没排队的,过!
        // 非公平:head 的 next 是写不,是,进入逻辑;如果不是,过!
        } else if (readerShouldBlock()) {
            if (firstReader == current) {
            	// assert firstReaderHoldCount > 0;
            } else {
            	// 需要阻塞:去掉最后一个计数为 0 的
                if (rh == null) {
                    rh = cachedHoldCounter;
                    if (rh == null || rh.tid != getThreadId(current)) {
                        rh = readHolds.get();
                        if (rh.count == 0) {
                        	readHolds.remove();
                       	}
                    }
                }
                if (rh.count == 0) {
                	return -1;
               	}
            }
        }
        if (sharedCount(c) == MAX_COUNT) {
        	throw new Error("Maximum lock count exceeded");
       	}
       	// 尝试获取读锁
        if (compareAndSetState(c, c + SHARED_UNIT)) {
            if (sharedCount(c) == 0) {
            	// 第一次获取读锁
                firstReader = current;
                firstReaderHoldCount = 1;
            } else if (firstReader == current) {
                firstReaderHoldCount++;
            } else {
                if (rh == null) {
                	rh = cachedHoldCounter;
                }
                if (rh == null || rh.tid != getThreadId(current)) {
                	rh = readHolds.get();	
                } else if (rh.count == 0)
                    readHolds.set(rh);
                rh.count++;
                cachedHoldCounter = rh;
            }
            return 1;
        }
    }
}

2.8、【读锁】解锁方法 unlock() —— ReentrantReadWriteLock.ReadLock

protected final boolean tryReleaseShared(int unused) {
    Thread current = Thread.currentThread();
    if (firstReader == current) {
        if (firstReaderHoldCount == 1)
            firstReader = null;
        else
            firstReaderHoldCount--;
    } else {
        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;
    }
    for (;;) {
        int c = getState();
        int nextc = c - SHARED_UNIT;
        if (compareAndSetState(c, nextc))
            return nextc == 0;
    }
}
  1. 判断当前线程是否是第一个获取读锁的线程。如果是,它将检查读锁的持有次数,如果持有次数为1,则将第一个读者置为null,否则将持有次数减1
  2. 如果当前线程不是第一个获取读锁的线程,则会尝试从缓存中获取HoldCounter对象,该对象记录了线程持有的读锁次数。如果缓存中的HoldCounter对象与当前线程不匹配,则从readHolds中获取与当前线程对应的HoldCounter对象。接着,它将该对象的持有次数减1,并在线程持有次数不大于1时,从readHolds中移除该对象。如果持有次数小于等于0,会抛出一个异常
  3. 最后,该函数进入一个循环,不断尝试将锁的状态减去SHARED_UNIT(表示读锁的单位),并使用compareAndSetState方法原子地更新锁的状态。如果更新成功,它会检查更新后的状态是否为0,如果是,则返回true,表示成功释放了读锁
  • 14
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值