ReentrantLock和ReentrantReadWriteLock是Java中用于线程同步的重要工具。ReentrantLock提供独占访问,适合需要保护共享资源不被并发修改的场景,同时支持可重入性,适用于递归操作。而ReentrantReadWriteLock则通过读写分离,允许多个线程同时读取资源,但仅允许一个线程写入,从而提高了并发性能。这种锁机制在处理大量读操作和较少写操作的场景中尤为有效。
定义
假如,有一个图书馆,图书馆每天都有读者前来借阅书籍,同时也有图书管理员在不断的更新和整理书架上的书籍。
在这个场景中,读者们就好比是线程中的读操作,他们只希望能够安静地阅读书籍,而不需要对书籍进行任何修改。图书管理员则好比是线程中的写操作,他们需要对书架上的书籍进行增加、删除或整理。为了保证图书馆的正常运营,需要制定一些规则来确保读者和管理员之间不会发生冲突,这就是ReadWriteLock接口发挥作用的地方,如下规则:
- 当有读者在阅读书籍时(即读锁被占用时),其他读者也可以同时阅读,因为读操作之间是不会相互干扰的,这就像多个读者可以同时站在不同的书架前阅读书籍一样。
- 当图书管理员需要整理书架时(即写锁被请求时),必须确保没有读者正在阅读那个书架上的书籍,否则整理过程中可能会导致读者找不到他们正在阅读的书籍,因此,写锁是独占的,一旦被图书管理员占用,其他读者和管理员都必须等待。
- 同时,当图书管理员正在整理书架时,其他管理员也不能同时进行整理工作,以免发生混乱。
通过ReadWriteLock接口,可以灵活地控制读操作和写操作之间的并发访问,从而提高程序的性能和响应能力。在这个图书馆的例子中,确保了读者能够高效地阅读书籍,同时管理员也能够安全地进行书架的整理工作。
代码案例
假设,有一个共享的数据结构,一个存储用户信息的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接口中主要方法的作用:
- readLock(): 此方法返回一个用于读取操作的锁,多个读取锁可以同时被持有,而不会相互阻塞,但是在持有读取锁的情况下,任何尝试获取写入锁的线程都将被阻塞,直到所有读取锁被释放。
- writeLock(): 此方法返回一个用于写入操作的锁,写入锁是独占的,这意味着在给定时间内,只有一个线程能够持有写入锁,当一个线程持有写入锁时,其他任何尝试获取读取锁或写入锁的线程都将被阻塞。
核心总结
ReentrantLock和ReentrantReadWriteLock的区别?
1、ReentrantLock
把ReentrantLock想象成一把普通的锁,当一个线程拥有了这把锁,其他线程就不能再获得它,直到拥有锁的线程释放它,这种锁非常适合那些需要独占访问资源的场景,例如,当正在修改一个共享数据结构时,不希望其他线程同时修改它,这时你就可以使用ReentrantLock。此外,ReentrantLock还有一个很酷的特性,那就是可重入性,这意味着同一个线程可以多次获得同一个锁而不会发生死锁,这对于一些需要递归操作的场景非常有用。
2、ReentrantReadWriteLock
与ReentrantLock不同,ReentrantReadWriteLock把锁分为读锁和写锁两种,这是一个非常实用的设计,因为在很多应用场景中,读操作远多于写操作,而且多个读操作之间通常不会相互干扰。使用ReentrantReadWriteLock,多个线程可以同时获得读锁来读取资源,但只有一个线程可以获得写锁来修改资源,并且当有一个线程拥有写锁时,其他线程不能获得读锁或写锁。这种读写分离的设计可以大大提高并发性能,因为读操作不再受到写操作的阻塞。
3、总结
ReentrantLock和ReentrantReadWriteLock都是强大的线程同步工具,如果只需要独占访问资源,那么ReentrantLock是一个不错的选择,但如果应用中有大量的读操作和较少的写操作,并且希望提高并发性能,那么ReentrantReadWriteLock将是更好的选择。