Java内置锁:深度解析ReentrantReadWriteLock并发类

时间:2024-01-24 08:57:42

Java内置锁:深度解析ReentrantReadWriteLock并发类 - 程序员古德

ReentrantLock和ReentrantReadWriteLock是Java中用于线程同步的重要工具。ReentrantLock提供独占访问,适合需要保护共享资源不被并发修改的场景,同时支持可重入性,适用于递归操作。而ReentrantReadWriteLock则通过读写分离,允许多个线程同时读取资源,但仅允许一个线程写入,从而提高了并发性能。这种锁机制在处理大量读操作和较少写操作的场景中尤为有效。

定义

Java内置锁:深度解析ReentrantReadWriteLock并发类 - 程序员古德

假如,有一个图书馆,图书馆每天都有读者前来借阅书籍,同时也有图书管理员在不断的更新和整理书架上的书籍。

在这个场景中,读者们就好比是线程中的读操作,他们只希望能够安静地阅读书籍,而不需要对书籍进行任何修改。图书管理员则好比是线程中的写操作,他们需要对书架上的书籍进行增加、删除或整理。为了保证图书馆的正常运营,需要制定一些规则来确保读者和管理员之间不会发生冲突,这就是ReadWriteLock接口发挥作用的地方,如下规则:

  1. 当有读者在阅读书籍时(即读锁被占用时),其他读者也可以同时阅读,因为读操作之间是不会相互干扰的,这就像多个读者可以同时站在不同的书架前阅读书籍一样。
  2. 当图书管理员需要整理书架时(即写锁被请求时),必须确保没有读者正在阅读那个书架上的书籍,否则整理过程中可能会导致读者找不到他们正在阅读的书籍,因此,写锁是独占的,一旦被图书管理员占用,其他读者和管理员都必须等待。
  3. 同时,当图书管理员正在整理书架时,其他管理员也不能同时进行整理工作,以免发生混乱。

通过ReadWriteLock接口,可以灵活地控制读操作和写操作之间的并发访问,从而提高程序的性能和响应能力。在这个图书馆的例子中,确保了读者能够高效地阅读书籍,同时管理员也能够安全地进行书架的整理工作。

代码案例

Java内置锁:深度解析ReentrantReadWriteLock并发类 - 程序员古德

假设,有一个共享的数据结构,一个存储用户信息的Map,这个Map会被多个线程同时访问,有的线程需要读取用户信息(读操作),而有的线程需要更新用户信息(写操作),为了保证数据的一致性和并发性能,可以使用ReadWriteLock来管理对Map的访问,如下代码案例:

import java.util.HashMap;  
import java.util.Map;  
import java.util.concurrent.locks.ReadWriteLock;  
import java.util.concurrent.locks.ReentrantReadWriteLock;  
  
public class UserStorage {  
    // 存储用户信息的Map  
    private final Map<String, String> userInfoMap = new HashMap<>();  
    // 读写锁  
    private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();  
  
    // 获取用户信息(读操作)  
    public String getUserInfo(String userId) {  
        // 获取读锁  
        readWriteLock.readLock().lock();  
        try {  
            // 从Map中读取用户信息  
            return userInfoMap.get(userId);  
        } finally {  
            // 释放读锁  
            readWriteLock.readLock().unlock();  
        }  
    }  
  
    // 更新用户信息(写操作)  
    public void updateUserInfo(String userId, String userInfo) {  
        // 获取写锁  
        readWriteLock.writeLock().lock();  
        try {  
            // 更新Map中的用户信息  
            userInfoMap.put(userId, userInfo);  
        } finally {  
            // 释放写锁  
            readWriteLock.writeLock().unlock();  
        }  
    }  
}  
  
public class Client {  
    public static void main(String[] args) {  
        // 创建UserStorage实例  
        UserStorage userStorage = new UserStorage();  
  
        // 模拟多个线程同时访问UserStorage  
        Runnable readTask = () -> {  
            for (int i = 0; i < 5; i++) {  
                String userId = "user" + i;  
                String userInfo = userStorage.getUserInfo(userId);  
                System.out.println("Read Thread: " + Thread.currentThread().getName() + ", User Info: " + userInfo);  
                try {  
                    Thread.sleep(100);  
                } catch (InterruptedException e) {  
                    e.printStackTrace();  
                }  
            }  
        };  
  
        Runnable writeTask = () -> {  
            for (int i = 0; i < 5; i++) {  
                String userId = "user" + i;  
                String userInfo = "Info" + i;  
                userStorage.updateUserInfo(userId, userInfo);  
                System.out.println("Write Thread: " + Thread.currentThread().getName() + ", Updated User Info: " + userInfo);  
                try {  
                    Thread.sleep(200);  
                } catch (InterruptedException e) {  
                    e.printStackTrace();  
                }  
            }  
        };  
  
        // 启动读线程和写线程  
        Thread readThread1 = new Thread(readTask, "ReadThread-1");  
        Thread readThread2 = new Thread(readTask, "ReadThread-2");  
        Thread writeThread = new Thread(writeTask, "WriteThread");  
  
        readThread1.start();  
        readThread2.start();  
        writeThread.start();  
    }  
}

UserStorage类包含一个Map用于存储用户信息,以及一个ReadWriteLock用于控制对Map的并发访问,getUserInfo方法用于获取用户信息,它首先获取读锁,然后从Map中读取用户信息,最后释放读锁,updateUserInfo方法用于更新用户信息,它首先获取写锁,然后更新Map中的用户信息,最后释放写锁,Client类包含一个main方法,用于模拟多个线程同时访问UserStorage,这里创建了两个读线程和一个写线程,它们分别执行读任务和写任务。

运行结果如下,由于线程调度的不确定性,每次运行的结果可能会有所不同,但大致上,会看到读线程和写线程交替执行,读线程可以同时执行,而写线程在执行时会阻止其他线程获取写锁或读锁。

Write Thread: WriteThread, Updated User Info: Info0  
Read Thread: ReadThread-1, User Info: Info0  
Read Thread: ReadThread-2, User Info: Info0  
Write Thread: WriteThread, Updated User Info: Info1  
Read Thread: ReadThread-1, User Info: Info1  
Read Thread: ReadThread-2, User Info: Info1  
...

核心API

ReadWriteLock接口是java.util.concurrent.locks包中的一个重要接口,它定义了读取锁和写入锁的相关操作,ReadWriteLock允许对共享资源进行更高级的并发访问控制,通过分离读操作和写操作来提高并发性能,以下是ReadWriteLock接口中主要方法的作用:

  1. readLock(): 此方法返回一个用于读取操作的锁,多个读取锁可以同时被持有,而不会相互阻塞,但是在持有读取锁的情况下,任何尝试获取写入锁的线程都将被阻塞,直到所有读取锁被释放。
  2. writeLock(): 此方法返回一个用于写入操作的锁,写入锁是独占的,这意味着在给定时间内,只有一个线程能够持有写入锁,当一个线程持有写入锁时,其他任何尝试获取读取锁或写入锁的线程都将被阻塞。

核心总结

Java内置锁:深度解析ReentrantReadWriteLock并发类 - 程序员古德

ReentrantLock和ReentrantReadWriteLock的区别?

1、ReentrantLock

把ReentrantLock想象成一把普通的锁,当一个线程拥有了这把锁,其他线程就不能再获得它,直到拥有锁的线程释放它,这种锁非常适合那些需要独占访问资源的场景,例如,当正在修改一个共享数据结构时,不希望其他线程同时修改它,这时你就可以使用ReentrantLock。此外,ReentrantLock还有一个很酷的特性,那就是可重入性,这意味着同一个线程可以多次获得同一个锁而不会发生死锁,这对于一些需要递归操作的场景非常有用。

2、ReentrantReadWriteLock

与ReentrantLock不同,ReentrantReadWriteLock把锁分为读锁和写锁两种,这是一个非常实用的设计,因为在很多应用场景中,读操作远多于写操作,而且多个读操作之间通常不会相互干扰。使用ReentrantReadWriteLock,多个线程可以同时获得读锁来读取资源,但只有一个线程可以获得写锁来修改资源,并且当有一个线程拥有写锁时,其他线程不能获得读锁或写锁。这种读写分离的设计可以大大提高并发性能,因为读操作不再受到写操作的阻塞。

3、总结

ReentrantLock和ReentrantReadWriteLock都是强大的线程同步工具,如果只需要独占访问资源,那么ReentrantLock是一个不错的选择,但如果应用中有大量的读操作和较少的写操作,并且希望提高并发性能,那么ReentrantReadWriteLock将是更好的选择。

关注我,每天学习互联网编程技术 - 程序员古德