Java读写锁相关小记

	读写锁在同一时刻可以允许多个读线程访问,但是在写线程访问时,所有的读线程和其他写线程均被阻塞。读写锁维护了一对锁,一个读锁和一个写锁,通过分离读锁和写锁,使得并发性相比一般的排它锁有了很大的提升。

	一般情况下,读写锁的性能都会比排它锁好,因为大多数场景读是多于写的。在读多于写的情况下,读写锁能够提供比排它锁更好的并发性和吞吐量。Java并发包提供读写锁的实现是ReentrantReadWriteLock。它支持的特性有:

支持非公平和公平的锁获取方式,默认是非公平
支持锁的重进入
支持锁降级

ReentrantReadWriteLock是对接口ReadWriteLock的实现,ReadWriteLock中仅定义了获取读锁和获取写锁的两个方法—自行参考源码
lock readLock

这两个方法皆由ReentrantReadWriteLock类具体实现。通过观察ReentrantReadWriteLock的源码发现,其内部含有ReadLock和WriteLock这两个类,代表ReentrantReadWriteLock拥有的一对读锁和写锁,而这两个类又都是靠一个静态内部类Sync实现的。Sync是继承了AbstractQueuedSynchronizer,用于管理读写锁的同步状态

写锁的获取与释放

protected final boolean tryAcquire(int acquires) {
    Thread current = Thread.currentThread();
    int c = getState(); // 获取同步状态
    int w = exclusiveCount(c); // 根据同步状态获取写锁状态
    // 已经有线程获取到了锁
    if (c != 0) {
        // 如果写线程数(w)为0(换言之存在读锁) 或者写锁不为0,同时持有锁的线程不是当前线程就返回失败
        if (w == 0 || current != getExclusiveOwnerThread())
            return false;
        
        // 如果写入锁的数量大于最大数(65535,2的16次方-1)就抛出一个Error。
        if (w + exclusiveCount(acquires) > MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
        
        // 写锁重入
        setState(c + acquires);
        return true;
    }
    
    // 如果当且写线程数为0,并且当前线程需要阻塞那么就返回失败;或者如果通过CAS增加写线程数失败也返回失败。
    if (writerShouldBlock() ||
        !compareAndSetState(c, c + acquires))
        return false;
    
    // 如果c=0,w=0(没有写锁也没有读锁)或者c>0,w>0(重入),则设置当前线程为锁的拥有者
    setExclusiveOwnerThread(current);
    return true;
}

从上面的源码可以看出,写锁是一个支持重进入的排它锁。如果当前线程已经获取到了写锁,那么再次获取时,直接增加写状态。如果当前线程在获取写锁时,读锁已经被获取(读状态不为0)或者该线程不是已经获取写锁的线程,则当前线程进入等待状态。

之所以要判断读锁是否存在,是因为读写锁要确保写锁的操作对读锁可见,如果允许读锁在已被获取的情况下还能允许写锁的获取,那么正在运行的其他线程可能就无所感知当前写线程的操作。

写锁释放时,每次释放均减少写状态,当写状态为0时表示写锁已经被释放,从而等待读写线程能够继续访问读写锁,同时前一次写线程的修改对后续读写线程可见。

读锁的获取与释放

protected final int tryAcquireShared(int unused) {
    Thread current = Thread.currentThread();
    int c = getState();
    
     // 如果其他线程已经获取了写锁,则当前线程获取读锁失败,进入等待状态
    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);
}

读锁是一个支持重进入的共享锁,他能够被多个线程同时获取,在没有其他写线程访问(写状态为0)时,读锁总是会被成功地获取,而所做的也只是(线程安全地)增加读状态。

可以看到在tryAcquireShared(int unused)方法中,如果其他线程已经获取了写锁,则当前线程获取读锁失败,进入等待状态。

如果当前线程获取了写锁或者写锁未被获取,则当前线程(线程安全,依靠CAS保证)增加读状态(增加的值是1<<16),成功获取读锁。

需要注意的是,读状态是所有线程获取读锁次数的总和,而每个线程各自获取读锁的次数只能选择保存在ThreadLocal中,由线程自身维护。

读锁的每次释放(线程安全的,可能有多个读线程同时释放读锁)均减少读状态,减少的值是“1<<16”。所以读写锁才能实现读读的过程共享,而读写、写读、写写的过程互斥。

读写锁的示例

package concurrent.lock;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class Cache {

    static Map<String,Object> map = new HashMap<String, Object>();
    static ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
    static Lock r = rwl.readLock();
    static Lock w = rwl.writeLock();
    public static final Object get(String key){

        r.lock();

        try {
            return map.get(key);
        }finally {
            r.unlock();
        }
    }

    public static final Object put(String key,Object value){

        w.lock();
        try {
            return map.put(key,value);
        }finally {
            w.unlock();
        }
    }

    public static final void clear(){

        w.lock();
        try {
            map.clear();
        }finally {
            w.unlock();
        }
    }
}

上述Cache类使用了一个非线程安全的HashMap作为缓存的实现,同时使用了读写锁和读锁和写锁来保证Cache是线程安全的。在数据的读方法get(String key)中,需要先获取读锁,然后读取数据,这样使得并发读数据时不会被阻塞。而对数据进行修改相关的put和clear方法中,需要先获取写锁,当获取了写锁之后,其他线程对于数据的读和写操作均会被阻塞,只有在写锁释放以后,其他的读写操作才能继续。

原文出处https://www.cnblogs.com/yxym2016/p/14866563.html
参考链接https://tech.meituan.com/2018/11/15/java-lock.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值