Java读写锁

摘要:
读/写锁可以允许多个读线程同时访问,但是当写线程访问时,所有读线程和其他写线程都被阻塞。读写锁维护一对锁,一个读写锁和一个写锁。通过分离读写锁,与一般的互斥锁相比,并发性大大提高。通常,读/写锁的性能比排他锁的性能好,因为大多数场景的读操作比写操作多。当读多于写时,读写锁可以提供比独占锁更好的并发性和吞吐量。Java还提供了读写锁。实现是ReentrantReadWriteLo

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

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

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

ReentrantReadWriteLock是对接口ReadWriteLock的实现,ReadWriteLock中仅定义了获取读锁和获取写锁的两个方法,即:

image-20210609133554724

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

image-20210609134108644

读写锁的实现分析

1.读写状态的设计

读写锁依赖于自定义同步器来实现同步功能,其读写状态就是同步器的同步状态。读写锁的自定义同步器需要在同步状态(一个整型变量)上维护多个读线程和一个写线程的状态,所以就需要“按位切割使用”这个整型变量。此处,读写锁将这个整型变量切分为了两个部分,高16位表示读,低16位表示写。划分方式如下图所示:

image-20210609094750711

上图表示的同步状态显示有两个线程已经获取了读锁。读写锁是通过位运算迅速确定读和写各自的状态的,假设当前的同步状态为state,那么读状态和写状态的计算方式如下:

写状态: state & 0x0000ffff     写状态加1:state+1
读状态: state >>> 16			读状态加1:state+(1<<16)

2.写锁的获取与释放

首先看一下写锁的加锁源码:

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时表示写锁已经被释放,从而等待读写线程能够继续访问读写锁,同时前一次写线程的修改对后续读写线程可见。

3.读锁的获取与释放

下面是读锁的源码:

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”。所以读写锁才能实现读读的过程共享,而读写、写读、写写的过程互斥。

4.锁降级

锁降级指的是写锁降级成为读锁。如果当前线程拥有写锁,然后将其释放,最后再获取读锁,这种分段完成的过程不能称之为锁降级。锁降级是指把持住(当前拥有的)写锁,再获取到读锁,随后释放先前拥有的写锁的过程。

ReentrantReadWriteLock不支持锁升级,目的是为了保证数据可见性,如果读锁已被多个线程获取,其中任意线程成功获取了写锁并更新了数据,则其更新对其他获取到读锁的线程是不可见的。

读写锁的使用示例

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方法中,需要先获取写锁,当获取了写锁之后,其他线程对于数据的读和写操作均会被阻塞,只有在写锁释放以后,其他的读写操作才能继续。

Cache类通过使用读写锁提升了读操作的并发性,也保证了每次写操作对所有的读写操作的可见性,同时还简化了编程方式。

参考:《Java并发编程的艺术》
https://tech.meituan.com/2018/11/15/java-lock.html

免责声明:文章转载自《Java读写锁》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇最大概率法分词及性能測试sed 替换文件内容下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

数据库连接池配置错误导致OOM

一、背景介绍: 运行在k8s集群中负责支付业务一个服务,运营一段时间就会被k8s kill,然后重启, 通过查看k8s 的event发现系统达到了memory到达了上限被集群kill调。 服务配置:jdk:1.8、堆内存:-Xmx800m-Xms800m 设置为800M, k8s的memory.limit设置为1G。 二、排查问题: 1.初步分析: 由于系...

C#多线程中等待线程池中的所有线程执行完毕后再执行下一个线程

网上找的,做个笔记记录一下。 有这么一个需求,就是巡检多台服务器是否都在线,点击巡检按钮后,按行读取DataGridView中的数据,并启行线程执行,这时会存在多个线程同时运行,但是什么时候给出用户提醒,说都巡检完成了呢,需要用到一个线程状态的检测。 最后的效果是这样子的,多个线程对表格按行进行服务器的巡检,只有等所有的巡检线都结束后,等待线程才会弹出一个...

performSelector

perfromSelector 底层源码地址:https://opensource.apple.com/tarballs/objc4/ 非延迟方法 - (id)performSelector:(SEL)sel { if (!sel) [self doesNotRecognizeSelector:sel]; return ((id(*)(id...

Servlet 异步处理

web容器会为每个请求分配一个线程,Servlet3.0新增了异步处理,解决多个线程不释放占据内存的问题。可以先释放容器分配给请求的线程与相关资源,减轻系统负担,原先释放了容器所分配线程的请求,其响应将被延后,可以在处理完成后再对客户端进行响应。 一、AsyncContex简介     为了支持异步处理,在ServletRequest上提供了startAs...

信号同步

转自:http://www.cnblogs.com/luminji/archive/2011/05/03/2034890.html 所谓线程同步,就是多个线程之间在某个对象上执行等待(也可理解为锁定该对象),直到该对象被解除锁定。C#中对象的类型分为引用类型和值类型。CLR在这两种类型上的等待是不一样的。我们可以简单的理解为在CLR中,值类型是不能被锁定的...

ios开发网络学习六:设置队列请求与RunLoop

#import "ViewController.h" @interface ViewController ()<NSURLConnectionDataDelegate> @end @implementationViewController -(void)touchesBegan:(NSSet<UITouch *> *)tou...