Java并发系列之三 ReadWriteLock源码解析

本文涉及的产品
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: Java并发系列之三 ReadWriteLock源码解析

1. ReadWriteLock接口



ReadWriteLock是一个java接口,它并没有继承Lock接口。提供了readLock()和writeLock(),分别返回一个读锁和写锁。

public interface ReadWriteLock {
    Lock readLock();
    Lock writeLock();
}


2. ReentrantReadWriteLock使用



ReentrantReadWriteLock类内部定义了静态内部类ReadLock和WriteLock。同时持有 readLock和writerLock对象。


public class ReadWriterLockUsage {
    private ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
    private Map<String, String> map = new HashMap<>();
    public String get(String key) {
        try {
            reentrantReadWriteLock.readLock().lock();
            System.out.println(Thread.currentThread().getName() + " readLock");
            try {
                TimeUnit.MILLISECONDS.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return map.get(key);
        } finally {
            reentrantReadWriteLock.readLock().unlock();
            System.out.println(Thread.currentThread().getName() + " readUnlock");
        }
    }
    public void put(String key, String value) {
        try {
            reentrantReadWriteLock.writeLock().lock();
            System.out.println(Thread.currentThread().getName() + " writeLock");
            try {
                TimeUnit.MILLISECONDS.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            map.put(key, value);
        } finally {
            reentrantReadWriteLock.writeLock().unlock();
            System.out.println(Thread.currentThread().getName() + " writeUnLock");
        }
    }
    public static void main(String[] args) {
        ReadWriterLockUsage usage = new ReadWriterLockUsage();
        for (int i = 0; i < 2; i++)
            new Thread() {
                @Override
                public void run() {
                    super.run();
                    while (true)
                        usage.put("", "");
                }
            }.start();
        for (int i = 0; i < 2; i++)
            new Thread() {
                @Override
                public void run() {
                    super.run();
                    while (true)
                        usage.get("");
                }
            }.start();
    }
}

运行结果可能如下


Thread-0 writeLock
Thread-0 writeUnLock
Thread-0 writeLock
Thread-0 writeUnLock
Thread-1 writeLock
Thread-1 writeUnLock
Thread-2 readLock
Thread-3 readLock
Thread-2 readUnlock
Thread-3 readUnlock
Thread-0 writeLock
Thread-0 writeUnLock
Thread-0 writeLock
Thread-0 writeUnLock
Thread-1 writeLock
Thread-1 writeUnLock
Thread-1 writeLock
Thread-1 writeUnLock

通过观察结果我们可以发现writeLock同一时刻只能被一个线程获得。而writeLock同一时刻可以被多个线程同时获得。


3. 源码解析



1. 锁状态


通过ReentrantLock的源码解析我们了解到,锁的状态由 private volatile int state来控制。每当线程获取到了锁 state会加1,每当线程释放了锁state会减1。当state=0表示锁当前处于非上锁状态。state定义在AbstractQueuedSynchronizer中。ReentrantLock持有一个AbstractQueuedSynchronizer对象。


那ReentrantReadWriteLock内部的锁是怎么实现的呢?我们知道ReentrantReadWriteLock内部有一个ReadLock和WriteLock。那么他们之间是完全独立的锁吗?如果不是独立的锁,那么state怎么来标识读写锁加锁的状态和次数呢?


事实上ReadLock和WriteLock是共用同一个AQS对象。AQS的state的值可以标识锁的状态。在读写锁的AQS实现中,int类型的state,我们知道int类型在计算机中有32位。它在读写所中,高16位标识读锁状态。低16位表示写锁状态。

    static final int SHARED_SHIFT   = 16;
    static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;//值为FFFF
    //当前读锁(共享锁)被获取的数量 c >>> SHARED_SHIFT表示向右移动16个单位,结果是int的高16位
    static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }
    //写锁(独占锁)被获取的数量,低16位
    static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }

2.WriteLock源码分析


首先我们来分析写锁的源码,因为写锁相对读锁更简单一些,写锁和ReentrantLock都是独占锁,所以我们先来分析它。还是从lock()开始分析


public static class  WriteLock implements Lock, java.io.Serializable {
    private final Sync sync;
    protected WriteLock(ReentrantReadWriteLock lock) {
        sync = lock.sync;
    }
    public void lock() {
        sync.acquire(1);
    }
}

我们知道sync的acquire(1)是个模板方法,先调用tryAcquire(1)方法如果获取成功,直接返回,如果获取失败,把线程封装成节点,加入队列,并且通过自旋来获取锁。在ReentrantLock源码分析中有详细讲解,读者如果不熟悉可以,先看ReentrantLock源码分析文章。我们重点来看下tryAcquire(1)在ReentrantReadWriteLock中的实现

 protected final boolean tryAcquire(int acquires) {
            /*
             * Walkthrough:
             * 1. If read count nonzero or write count nonzero
             *    and owner is a different thread, fail.
             *(如果读锁被获取 或者 写锁被获取而且获取到的线程不是当前线程,获取写锁失败)
             * 2. If count would saturate, fail. (This can only
             *    happen if count is already nonzero.)
             *(如果锁的数量超过限制,获取失败)
             * 3. Otherwise, this thread is eligible for lock if
             *    it is either a reentrant acquire or
             *    queue policy allows it. If so, update state
             *    and set owner.
             *(否则,如果该线程是可重入获取或队列策略允许,则该线程有资格进行锁定。 如果是这样,更新状态并设置所有者。)
             */
            Thread current = Thread.currentThread();
            int c = getState();
            int w = exclusiveCount(c);
            if (c != 0) {
                // (Note: if c != 0 and w == 0 then shared count != 0)
                //如果 c != 0 and w == 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;
        }

如果当前读锁被获取了,再去获取写锁会失败。简单吧,如果失败了,走的还是ReentrantLock那一套


3. ReadLock源码解析


我们知道读锁是共享锁,同一时刻可以被多个线程同时获取。接下来我们来分析下。还是从lock()方法开始

 public static class ReadLock implements Lock, java.io.Serializable {
        private final Sync sync;
        protected ReadLock(ReentrantReadWriteLock lock) {
            sync = lock.sync;
        }
        /**
         * Acquires the read lock.
         *
         * <p>Acquires the read lock if the write lock is not held by
         * another thread and returns immediately.
         *
         * <p>If the write lock is held by another thread then
         * the current thread becomes disabled for thread scheduling
         * purposes and lies dormant until the read lock has been acquired.
         */
        public void lock() {
            //如果写锁没有被其他线程占有立马获取到读锁
            //如果写锁被其他线程占有,那么当前线程会阻塞直到读锁被获取到
            sync.acquireShared(1);
        }
}

我们来看下sync.acquireShared(1)方法

 public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0)//如果尝试获取共享锁失败
            doAcquireShared(arg);

我们先来看下doAcquireShared()吧,如果是独占锁尝试获取失败,之后的流程我们是比较清楚的,我们可以对比观察他们之间的区别

 private void doAcquireShared(int arg) {
        //加入到AQS队列中 和独占锁是一样的
        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) {//如果获取成功
                    //获取成功共享锁和独占锁是有区别的,独占锁获取成功直接return。
                    //共享锁如果获取成功它会告诉下一个等待获取共享锁线程去获取共享锁
                        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);
        }
    }

我们再来看下tryAcquireShared()的实现吧

protected final int tryAcquireShared(int unused) {
            /*
             * Walkthrough:
             * 1. If write lock held by another thread, fail.
             * 2. Otherwise, this thread is eligible for
             *    lock wrt state, so ask if it should block
             *    because of queue policy. If not, try
             *    to grant by CASing state and updating count.
             *    Note that step does not check for reentrant
             *    acquires, which is postponed to full version
             *    to avoid having to check hold count in
             *    the more typical non-reentrant case.
             * 3. If step 2 fails either because thread
             *    apparently not eligible or CAS fails or count
             *    saturated, chain to version with full retry loop.
             */
            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);
        }

关于锁降级



锁降级是指在获取写锁的同时,读锁也能获取到,当然这种情况只限于同一个线程。降级是对写锁而言,对于读锁个人觉得应该是锁升级。附上java源码的例子

class CachedData {
   Object data;
   volatile boolean cacheValid;
   final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
   void processCachedData() {
     rwl.readLock().lock();
     if (!cacheValid) {
        // Must release read lock before acquiring write lock
        rwl.readLock().unlock();
        rwl.writeLock().lock();
        try {
          // Recheck state because another thread might have
          // acquired write lock and changed state before we did.
          if (!cacheValid) {
            data = ...
            cacheValid = true;
          }
          // Downgrade by acquiring read lock before releasing write lock
          rwl.readLock().lock();
        } finally {
          rwl.writeLock().unlock(); // Unlock write, still hold read
        }
     }
     try {
       use(data);
     } finally {
       rwl.readLock().unlock();
     }
   }
 }

下面那个读锁其实被升级为独占锁了,同一时刻只能被一个线程获取

相关文章
|
9天前
|
数据采集 人工智能 Java
Java产科专科电子病历系统源码
产科专科电子病历系统,全结构化设计,实现产科专科电子病历与院内HIS、LIS、PACS信息系统、区域妇幼信息平台的三级互联互通,系统由门诊系统、住院系统、数据统计模块三部分组成,它管理了孕妇从怀孕开始到生产结束42天一系列医院保健服务信息。
25 4
|
15天前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
45 2
|
6天前
|
数据采集 存储 Web App开发
Java爬虫:深入解析商品详情的利器
在数字化时代,信息处理能力成为企业竞争的关键。本文探讨如何利用Java编写高效、准确的商品详情爬虫,涵盖爬虫技术概述、Java爬虫优势、开发步骤、法律法规遵守及数据处理分析等内容,助力电商领域市场趋势把握与决策支持。
|
11天前
|
存储 安全 Java
Java多线程编程中的并发容器:深入解析与实战应用####
在本文中,我们将探讨Java多线程编程中的一个核心话题——并发容器。不同于传统单一线程环境下的数据结构,并发容器专为多线程场景设计,确保数据访问的线程安全性和高效性。我们将从基础概念出发,逐步深入到`java.util.concurrent`包下的核心并发容器实现,如`ConcurrentHashMap`、`CopyOnWriteArrayList`以及`BlockingQueue`等,通过实例代码演示其使用方法,并分析它们背后的设计原理与适用场景。无论你是Java并发编程的初学者还是希望深化理解的开发者,本文都将为你提供有价值的见解与实践指导。 --- ####
|
16天前
|
存储 安全 Linux
Golang的GMP调度模型与源码解析
【11月更文挑战第11天】GMP 调度模型是 Go 语言运行时系统的核心部分,用于高效管理和调度大量协程(goroutine)。它通过少量的操作系统线程(M)和逻辑处理器(P)来调度大量的轻量级协程(G),从而实现高性能的并发处理。GMP 模型通过本地队列和全局队列来减少锁竞争,提高调度效率。在 Go 源码中,`runtime.h` 文件定义了关键数据结构,`schedule()` 和 `findrunnable()` 函数实现了核心调度逻辑。通过深入研究 GMP 模型,可以更好地理解 Go 语言的并发机制。
|
2月前
|
缓存 Java 程序员
Map - LinkedHashSet&Map源码解析
Map - LinkedHashSet&Map源码解析
70 0
|
2月前
|
算法 Java 容器
Map - HashSet & HashMap 源码解析
Map - HashSet & HashMap 源码解析
57 0
|
2月前
|
存储 Java C++
Collection-PriorityQueue源码解析
Collection-PriorityQueue源码解析
62 0
|
2月前
|
安全 Java 程序员
Collection-Stack&Queue源码解析
Collection-Stack&Queue源码解析
85 0
|
28天前
|
消息中间件 缓存 安全
Future与FutureTask源码解析,接口阻塞问题及解决方案
【11月更文挑战第5天】在Java开发中,多线程编程是提高系统并发性能和资源利用率的重要手段。然而,多线程编程也带来了诸如线程安全、死锁、接口阻塞等一系列复杂问题。本文将深度剖析多线程优化技巧、Future与FutureTask的源码、接口阻塞问题及解决方案,并通过具体业务场景和Java代码示例进行实战演示。
43 3

推荐镜像

更多