JUC读写锁

一、读写锁概述

在这里插入图片描述

Java 中读写锁(ReadWriteLock)的核心概念。读写锁是 Java 并发包(java.util.concurrent.locks)中解决特定并发场景下性能问题的重要工具。

1.1 核心目标

  • 提高【读多写少】场景下的并发性能

想象一个共享资源(比如一个配置字典、一个缓存或一个大型数据集):

  • 读操作read):通常不会修改数据,只是获取数据。多个线程同时读取同一个数据通常是安全的
  • 写操作write):会修改数据。为了保证数据一致性,写操作通常需要独占访问资源。即写操作进行时,不允许其他任何线程(读或写)访问资源。

传统的互斥锁(如 synchronizedReentrantLock)在访问共享资源时,无论读写,都只允许一个线程访问。这在“读多写少”的场景下会造成巨大的性能瓶颈大量只读线程被强制串行执行,即使它们之间本可以安全地并发读取

1.2 核心思想

读写锁的核心思想:分离读锁和写锁

读写锁巧妙地解决了这个问题,它维护了一对锁

  • 读锁(共享锁,read lock

    • 共享性:多个线程可以同时持有读锁。
    • 目的:允许多个线程并发地读取共享资源,极大地提升读取的吞吐量。
    • 约束:当一个(或多个)线程持有读锁时,任何线程都无法获取写锁。这是为了保证读取数据的一致性——防止在读取过程中数据被修改。
  • 写锁(独占锁,write lock

    • 排他性:同一时刻只能有一个线程持有写锁。

    • 目的:保证写操作的原子性和数据一致性。写操作需要独占访问资源。

    • 约束

      • 当一个线程持有写锁时,其他任何线程(无论是想读还是想写)都无法获取读锁或写锁
      • 在获取写锁之前,必须等待所有已持有的读锁释放。同样,在获取读锁之前,必须等待已持有的写锁释放

1.3 关键规则与保证

读写锁的行为严格遵循以下规则,这些规则是理解其并发语义的基础:

  1. 读读共享(Read-Read Sharing):多个线程可以同时获取并持有读锁,进行并发读取操作。这是性能提升的关键。

  2. 读写互斥(Read-Write Mutual Exclusion)

  • 如果一个线程持有读锁,另一个线程尝试获取写锁会被阻塞,直到所有读锁释放。
  • 如果一个线程持有写锁,另一个线程尝试获取读锁会被阻塞,直到写锁释放。
  1. 写写互斥(Write-Write Mutual Exclusion):同一时刻只能有一个线程持有写锁。尝试获取写锁的线程会被阻塞,直到当前写锁释放。

  2. 可重入性(Reentrancy)

    • Java 的标准实现 ReentrantReadWriteLock 支持锁的可重入
    • 一个线程可以重复获取它已经持有的读锁或写锁(需要相应次数的释放)。
    • 持有写锁的线程可以再次获取读锁(锁降级的关键)。
    • 持有读锁的线程不能直接获取写锁(尝试获取写锁会阻塞,可能导致死锁)。如果需要升级,必须先释放所有读锁,再尝试获取写锁,但这个操作不是原子的,中间状态可能被其他写线程抢占。因此,锁升级通常不被推荐,且标准实现不支持。
  3. 公平性(Fairness)

  • 类似于 ReentrantLockReentrantReadWriteLock 可以构造为公平锁非公平锁(默认)。
  • 公平锁:线程按照请求锁的顺序(近似 FIFO)获取锁。这有助于避免线程饥饿(如写线程一直被读线程抢占),但可能降低整体吞吐量。
  • 非公平锁:允许“插队”。当锁可用时,一个等待线程(无论等待了多久)可能比更早等待的线程优先获得锁。这能提高吞吐量,但可能导致某些线程(尤其是写线程)长时间饥饿。
  • 对读写锁的影响:在非公平模式下,一个释放写锁的线程,如果此时有等待的读线程和写线程,读线程通常能更快地集体获得读锁(因为允许多个读),导致等待的写线程可能延迟。公平模式则严格按照队列顺序,写线程有机会更快获得锁,但整体并发读性能可能稍低。

1.4 核心组件

Java 标准库通过 java.util.concurrent.locks.ReentrantReadWriteLock 类提供了读写锁的实现。它是 ReadWriteLock 接口的具体实现。

核心组件:

  • ReentrantReadWriteLock.ReadLock: 实现读锁的嵌套类。
  • ReentrantReadWriteLock.WriteLock: 实现写锁的嵌套类。
  • Sync (内部抽象类): 继承自 AbstractQueuedSynchronizer (AQS),是实现锁同步机制的核心。ReentrantReadWriteLock 内部维护一个 Sync 实例。

    • 状态表示 (state): Sync 使用一个 int 类型的 state 变量(32位)来同时表示读锁和写锁的状态。

      • 低 16 位 (0x0000FFFF): 表示写锁的重入次数
      • 高 16 位 (0xFFFF0000): 表示持有读锁的线程数(更精确地说,是每个获取读锁的线程持有的读锁计数之和,因为读锁可重入)。
    • 写锁获取: 检查 state 是否为 0(无锁)或低16位不为0且当前线程是写锁持有者(重入)。否则加入等待队列。

    • 读锁获取: 检查是否有写锁持有(state低16位不为0)且持有者不是当前线程(防止读锁升级死锁)。还要检查读锁数量是否溢出。在非公平模式下,如果队列头是写线程等待,新来的读线程可能被阻塞(避免写线程饥饿);公平模式则严格排队。

    • 锁释放: 相应减少 state 的高位(读)或低位(写)计数

二、使用示例

2.1 采用独占锁的姿势读、写数据

package cn.tcmeta.rwlock;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;

public class Counter {
    private int value; // 修改的值

    /**
     * 读取操作
     * @param lock
     */
    public void read(Lock lock){
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + " --> \t" +  " 正在读取数据....");
            try {
                TimeUnit.MILLISECONDS.sleep(100);
                System.out.println(Thread.currentThread().getName() + " --> \t" +  " 数据读取完成了... 读取到的值是: " + value);
            }catch (Exception e){
                e.printStackTrace();
            }
        }catch(Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }

    /**
     * 写操作
     * @param lock
     */
    public void write(Lock lock, int newValue){
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + " --> \t" +  " 正在修改值 ... ");
            try {
                TimeUnit.MILLISECONDS.sleep(200);
                value = newValue;
                System.out.println(Thread.currentThread().getName() + " --> \t" +  " 修改完成了, 最新值是: " + value);
            }catch (Exception e){
                e.printStackTrace();
            }

        }catch(Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
}

测试用例:

package cn.tcmeta.rwlock;

import java.util.concurrent.locks.ReentrantLock;

/**
 * @author: laoren
 * @description: 独占锁测试
 * @version: 1.0.0
 */
public class T1 {
    public static void main(String[] args) {
        ReentrantLock reentrantLock = new ReentrantLock();
        Counter counter = new Counter();
        // 2个线程,执行写入操作.
        for (int i = 0; i < 2; i++) {
            int tmp = i;
            new Thread(() -> {
                counter.write(reentrantLock, tmp);
            }, "write-thread: ").start();
        }

        // 18个线程,执行读取操作
        for (int i = 0; i < 18; i++) {
            int temp = i;
            new Thread(() -> {
                counter.read(reentrantLock);
            }, "read-thread: ").start();
        }
    }
}

在这里插入图片描述

2.2 使用读写锁读、写数据

public class T2 {
    public static void main(String[] args) {
        // 创建资源类对象
        Counter counter = new Counter();
        // 使用独占锁的姿势读、写操作
        ReentrantLock reentrantLock = new ReentrantLock();
        ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
        // 获取读锁
        ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
        // 获取写锁
        ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();

        // 2个线程,执行写入操作.
        for (int i = 0; i < 2; i++) {
            int tmp = i;
            new Thread(() -> {
                counter.write(writeLock, tmp);
            }, "write-thread: ").start();
        }

        // 18个线程,执行读取操作
        for (int i = 0; i < 18; i++) {
            int temp = i;
            new Thread(() -> {
                counter.read(readLock);
            }, "read-thread: ").start();
        }
    }
}

在这里插入图片描述
经过测试可以发现, 使用读写锁的时候, 因为读是共享的,所以效率较快.写是独享的

2.3 锁降级 (Lock Downgrading)

  • 这是读写锁提供的一个安全且有用的特性
  • 指一个线程先获取写锁 -> 再获取读锁 -> 然后释放写锁的过程。
  • 目的:在持有写锁修改数据后,不立即释放所有锁,而是先获取读锁(因为持有写锁时获取读锁总是成功的),再释放写锁。这样该线程在后续读取操作时仍然持有读锁(保证了读取自己修改后数据的可见性),同时允许其他读线程并发访问(因为写锁已释放)。
  • 关键点:降级过程(获取读锁 -> 释放写锁)在持有写锁的线程内部完成,是原子性的。其他线程无法在降级过程中插入获取写锁,保证了数据在降级后对其他读线程可见时的一致性。

示例流程【伪代码】:

writeLock.lock(); // 1. 获取写锁 (独占)
try {
    // ... 修改共享数据 ...
    readLock.lock(); // 2. 在释放写锁前先获取读锁 (锁降级开始,此时写锁未释放,读锁一定成功)
} finally {
    writeLock.unlock(); // 3. 释放写锁 (锁降级完成,现在只持有读锁)
}
try {
    // ... 读取刚刚修改的数据 (其他读线程此时也可以并发读取了) ...
} finally {
    readLock.unlock(); // 4. 释放读锁
}

示例代码:

package cn.tcmeta.rwlock;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * 锁降级
 */
public class LockDowngradingDemo {
    private static final ReentrantReadWriteLock LOCK = new ReentrantReadWriteLock();
    private static int shareCount; // 共享变量

    public static void main(String[] args) {
        Thread writeThread = new Thread(() -> {
            LOCK.writeLock().lock();
            try {
                System.out.println(Thread.currentThread().getName() + " --> \t" + " 开始更新共享变量");
                shareCount++;
                try {
                    TimeUnit.MILLISECONDS.sleep(100);
                } catch (Exception e) {
                    e.printStackTrace();
                }

                System.out.println(Thread.currentThread().getName() + " --> \t" + " 更新完成的共享变量的值是: " + shareCount);

                // 在持有写锁的情况下, 获取读锁
                LOCK.readLock().lock();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                // 释放写锁
                LOCK.writeLock().unlock();
            }


            // 进行读操作
            try {
                System.out.println(Thread.currentThread().getName() + " --> \t" + " 值是: " + shareCount);
            } finally {
                LOCK.readLock().unlock();
            }
        }, "write-thread:");

        Thread readThread = new Thread(() -> {
            LOCK.readLock().lock();
            try {
                System.out.println(Thread.currentThread().getName() + " --> \t" + " 当前的值是: " + shareCount);
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                LOCK.readLock().unlock();
            }
        }, "read-thread:");

        writeThread.start();

        try {
            TimeUnit.MILLISECONDS.sleep(3000);
        } catch (Exception e) {
            e.printStackTrace();
        }

        readThread.start();
    }
}

在这里插入图片描述

写线程无获取写锁,然后更新共享资源的值,并通过获取读锁实现降级.读线程直接获取读锁进行读操作.

三、应用场景

读写锁在Java并发编程中扮演着重要角色,特别适用于读多写少的场景。

3.1 缓存系统【高频读、低频更新】

package cn.tcmeta.rwlock;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.*;

public class Cache<K, V> {
    private final Map<K, V> cacheMap = new HashMap<>();
    private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
    private final Lock readLock = rwLock.readLock();
    private final Lock writeLock = rwLock.writeLock();

    // 获取缓存数据(多个线程可同时读取)
    public V get(K key) {
        readLock.lock();
        try {
            return cacheMap.get(key);
        } finally {
            readLock.unlock();
        }
    }

    // 更新缓存数据(独占写入)
    public void put(K key, V value) {
        writeLock.lock();
        try {
            cacheMap.put(key, value);
        } finally {
            writeLock.unlock();
        }
    }

    // 按需加载(经典读写锁应用)
    public V getOrLoad(K key, DataLoader<K, V> loader) {
        V value;
        // 先尝试读取
        readLock.lock();
        try {
            value = cacheMap.get(key);
            if (value != null) {
                return value;
            }
        } finally {
            readLock.unlock();
        }
        
        // 未找到数据,获取写锁加载
        writeLock.lock();
        try {
            // 双重检查(避免重复加载)
            value = cacheMap.get(key);
            if (value == null) {
                value = loader.load(key);
                cacheMap.put(key, value);
            }
            return value;
        } finally {
            writeLock.unlock();
        }
    }
    
    public interface DataLoader<K, V> {
        V load(K key);
    }
}

3.2 配置中心【配置读取多、更新少】

package cn.tcmeta.rwlock;

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

public class ConfigCenter {
    private volatile Map<String, String> config = new HashMap<>();
    private final ReadWriteLock rwLock = new ReentrantReadWriteLock(true); // 公平锁
    
    // 获取配置值
    public String getConfig(String key) {
        rwLock.readLock().lock();
        try {
            return config.get(key);
        } finally {
            rwLock.readLock().unlock();
        }
    }
    
    // 热更新配置
    public void updateConfig(Map<String, String> newConfig) {
        rwLock.writeLock().lock();
        try {
            Map<String, String> updated = new HashMap<>(config);
            updated.putAll(newConfig);
            config = updated; // volatile保证可见性
        } finally {
            rwLock.writeLock().unlock();
        }
    }
}

3.3 金融交易系统【账户查询多、转账少】

public class AccountService {
    private final Map<Long, Account> accounts = new ConcurrentHashMap<>();
    private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
    
    // 查询余额(高并发)
    public BigDecimal getBalance(long accountId) {
        rwLock.readLock().lock();
        try {
            Account acc = accounts.get(accountId);
            return acc != null ? acc.getBalance() : BigDecimal.ZERO;
        } finally {
            rwLock.readLock().unlock();
        }
    }
    
    // 转账操作(需要独占)
    public void transfer(long from, long to, BigDecimal amount) {
        rwLock.writeLock().lock();
        try {
            Account source = accounts.get(from);
            Account target = accounts.get(to);
            
            if (source.getBalance().compareTo(amount) < 0) {
                throw new InsufficientFundsException();
            }
            
            source.withdraw(amount);
            target.deposit(amount);
        } finally {
            rwLock.writeLock().unlock();
        }
    }
}

3.4 实时数据看板【数据读取多、更新少】

public class DashboardData {
    private volatile DataSnapshot currentSnapshot;
    private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
    
    // 获取当前数据快照
    public DataSnapshot getCurrentData() {
        rwLock.readLock().lock();
        try {
            return currentSnapshot;
        } finally {
            rwLock.readLock().unlock();
        }
    }
    
    // 更新数据(使用锁降级保证数据一致性)
    public void updateData(DataProcessor processor) {
        rwLock.writeLock().lock();
        try {
            // 处理数据(独占写权限)
            DataSnapshot newSnapshot = processor.process(currentSnapshot);
            
            // 锁降级开始
            rwLock.readLock().lock();
            try {
                currentSnapshot = newSnapshot;
            } finally {
                rwLock.writeLock().unlock(); // 释放写锁,保留读锁
            }
            
            // 此时其他读线程可以访问新数据
            logChanges(newSnapshot);
        } finally {
            rwLock.readLock().unlock();
        }
    }
}

四、场景分析图表

4.1 读写锁在缓存系统的工作流程

4.2 锁降级过程图解

在这里插入图片描述

4.3 不同场景下锁的选择

在这里插入图片描述

五、最佳实践与注意事项

5.1 锁选择原则

  • 读写比例 > 10:1:优先考虑读写锁
  • 读写比例 < 3:1:使用互斥锁更高效
  • 超高频读取:考虑StampedLock的乐观读

5.2 避免常见陷阱

// 错误示例:在读锁保护下修改数据
public void unsafeIncrement() {
    readLock.lock();
    try {
        counter++; // 危险操作!
    } finally {
        readLock.unlock();
    }
}

// 正确做法:写锁保护写操作
public void safeIncrement() {
    writeLock.lock();
    try {
        counter++;
    } finally {
        writeLock.unlock();
    }
}

5.3 性能调优技巧

// 1. 使用tryLock避免长时间阻塞
if (writeLock.tryLock(100, TimeUnit.MILLISECONDS)) {
    try {
        // 关键操作
    } finally {
        writeLock.unlock();
    }
}

// 2. 公平锁防止写线程饥饿
new ReentrantReadWriteLock(true); // 创建公平锁

// 3. 监控锁竞争情况
ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
System.out.println("读队列长度: " + lock.getReadLockCount());
System.out.println("写队列长度: " + lock.getQueueLength());

5.4 替代解决方案

场景推荐方案优点缺点
读多写少ReentrantReadWriteLock成熟稳定,支持锁降级写线程可能饥饿
超高并发读StampedLock乐观读性能极高API复杂,不可重入
读写均衡ReentrantLock简单高效读操作无法并发
数据快照CopyOnWriteArrayList读完全无锁写开销大,内存占用高

六、典型应用场景总结

  1. 数据检索系统
    • 场景:商品目录、用户资料查询
    • 特点:95%读操作,5%数据更新
    • 实现:读写锁保护核心数据集合
  2. 实时监控系统
    • 场景:服务器监控、交易大盘
    • 特点:高频读取,周期性数据刷新
    • 实现:锁降级保证数据更新一致性
  3. 多级缓存同步
    • 场景:本地缓存与中央缓存同步
    • 特点:本地读为主,偶尔批量更新
    • 实现:写锁保护缓存更新过程
  4. 配置管理系统
    • 场景:微服务配置中心
    • 特点:服务启动时密集读取,偶尔热更新
    • 实现:读写分离保证高可用性

读写锁在Java并发工具箱中是一个强大的工具,合理使用可以显著提升系统吞吐量。但必须注意其适用边界——在真正的"读多写少"场景中才能发挥最大价值。对于临界区短小的操作,或写操作频繁的场景,传统的互斥锁可能是更简单高效的选择。

七、没有了

学习愉快!


资料内容:
链接: https://pan.baidu.com/s/1_igGW3DT7pGsNJPMoOpV6g 提取码: r35a
如资料失效,请评论区留言或者留邮箱. 🎁

在这里插入图片描述

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值