并发库——ReadWriteLock

原则

  1. 允许多个线程同时读共享变量
  2. 只允许一个线程写共享变量
  3. 如果一个线程正在写共享变量,此时禁止线程读共享变量。

通过以上原则,我们不难看出:读写锁与互斥锁最大区别是:读写锁允许多个线程同时读共享变量,而互斥锁不允许。这也是读写锁在读多写少环境下性能优于互斥锁的关键。

概念

  • 可重入:当线程拿到锁的时候,在临界区再次尝试获取同一把锁,此时会锁的状态*volatile int state++*会加一。这种特性叫做可重入。
  • 锁升级:在拿到读锁且没有释放读锁的时候,尝试去获取写锁,这种情况被称为锁升级。读写锁不支持锁升级
  • 锁降级:在拿到写锁且没有释放写锁的时候,尝试去获取读锁,这种情况被称为锁降级。读写锁支持锁降级。
  • CAS指令 : compareAndSetState:只有当前 count 的值与期望值 expect 相等时,才会将 count 更新为newValue. 在解决并发时,CAS和自旋更搭哦~~

CAS 的伪代码:

class SimulatedCAS {
    valotile int count;
    
    addOne() {
        int newValue = 0;
        do {
            newValue = count + 1; // 1
        } while (count != compareAndSetState(count, newValue) // 2
        )
    }
    
    synchronized compareAndSetState(int expect, int newValue) {
        // 读取当前值
        int curValue = count;
        // 比较期望值与当前值
        if(expect == curValue) {
            // 更新count值
            count = newValue;
        }
        // 返回写入前的值
        return curValue;
    }
}

从它的实现不难看出,它重视值的变化,而忽略属性的存在,因此会存在 ABA的问题。

什么是ABA问题那? 假设 count 值原本是 A,线程T1 执行完 1 处代码后,执行 2 处代码前, count 可能会被线程 T2 更新为 B,之后又被线程 T3 更新为了 A,这样线程 T1 看到的 count 是 A,但其实中间该值已经被其他线程修改过,这就是 ABA 问题。如果我们只关心值的修改,而忽略中间状态的改变,就不存在 ABA 问题,例如: 原子类(AtomicLong)的值递增。但是当关心中间状态时,就会出现 ABA 问题。此时,我们可以添加一个版本号来解决ABA。可以参考 AtomicMarkableReference的实现机制

原理

读写锁将同步状态描述为一个 int 型变量 state, 将该变量按位切割后,高 16 位用于表示读锁的状态,低 16 位用于表示写锁的状态。

  • 写锁:一种支持可重入的互斥锁,即:同一时刻该锁只允许一个线程持有。
  • 读锁:一种支持可重入的共享锁,即:同一时刻该锁允许多个线程持有。

写锁的获取与释放

写锁是一种支持可重入的独占锁,如果当前线程持有了写锁,再次尝试获取时,会增加写状态;如果当前线程获取写锁时,读锁已经被获取或者该线程不是拿到写锁的线程,则该线程进入等待状态。

public class ReentrantReadWriteLock implements ReadWriteLock {
    abstract static class Sync extends AbstractQueuedSynchronizer {
        // 获取写锁
        protected final boolean tryAcquire(int acquires) {
            Thread current = Thread.currentThread();
            int c = getState(); // 获取同步状态
            int w = exclusiveCount(c); // 获取低16位的写状态
            if(c != 0) {
                // 1.如果写锁被某一线程持有,且持有写锁的线程与当前线程不一致; 2. 如果读锁存在,且持有写锁的线程与当前线程不一致
                if(w == 0 || current != getExclusiveOwnerThread())
                    return false;
                // 成功获取锁,更新同步状态
                setState(c + acquires);
                return true;
            }
            // 通过CAS更新状态
            if (writerShouldBlock() ||
                !compareAndSetState(c, c + acquires))
                return false;
            setExclusiveOwnerThread(current);
            return true;
        }
        // 释放写锁
        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;
        }
    }
}

读锁的获取

读锁是一把支持可重入的共享锁,即同一时刻该锁能被多个线程持有。

protected final int tryAcquireShared(int unused) { 
    
    Thread current = Thread.currentThread();
    int c = getState();
    // 如果有其他线程获取了写锁,则获取失败
    if(exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current) {
        return -1;
    }
    int r = shareCount(c); // 获取读锁的持有数量
    // 通过CAS修改读锁的同步状态并记录数量
    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);
    
}

实战

使用缓存首先要解决的就是数据的初始化问题。缓存数据的初始化,可以采用一次性加载的方式,也可以采用按需加载的方式。如果数据量不大,可以采用在程序启动时一次性加载的方式。如果数据量非常大,则使用按需加载的方式,即:当应用查询缓存且数据不再缓存的时候,才触发加载源头相关数据进行缓存的操作。

使用读写锁实现一个通用的缓存工具。

public final class Cache<K,V> {
    // HashMap 本身是非线程安全的
    private final Map<K,V> cache = new HashMap<K,V>();
    
    private final ReadWriteLock rwl = new ReadWriteLock();
    
    private final Lock rlock = rwl.readLock();
    private final Lock wLock = rwl.writeLock();
    
    public V get(K key) {
        V value = null;
        rlock.lock();
        try{
            value = cache.get(key);
            // 注意: 不能在拿到读锁的时候,去获取写锁,即不支持锁升级。原因是为了保证数据的可见性。
        } finally {
            rlock.unlock();
        }
        // 如果命中缓存,则直接返回
        if(value != null) {
            return value;
        }
        // 如果没有缓存,则查找源头(可能是网络,也可能是数据库)然后写入缓存
        wLock.lock();
        try {
            // 再获取一次缓存,因为此时有可能被其他线程将数据写入缓存
            value = cache.get(key);
            if(value == null) {
                // 查找数据源头,得到 value
                cache.put(key,value);
            }
        } finally {
            wLock.unlock();
        }
        return value;
    }
    
    public void put(K key, V value) {
        wLock.lock();
        try {
            cache.put(key, value);
        } finally {
            wLock.unlock();
        }
    }
}

总结

ReadWriteLock 是一个接口,它的实现类是 ReentrantReadWriteLock。ReentrantReadWriteLock 提供了两把锁 ReadLockWriteLock。 同ReentrantLock一样,可以指定锁的公平/非公平特性,且都是可重入的。另外,同步的核心也是使用的AQS(AbstractQueuedStnchronized)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值