读写锁是一种将锁的访问权限分为读操作和写操作两类的并发控制工具,核心设计目标是让多个读操作可以并行执行,同时保证写操作的排他性,从而在读多写少的场景中大幅提升并发性能。共享锁也就是S锁,是读操作获取的锁,允许多个线程同时持有;排他锁也就是X锁,是写操作获取的锁,同一时间只能有一个线程持有,且会排斥其他所有锁的获取。

共享锁与排他锁的基础特性
共享锁(S锁)的特性
共享锁是专门为读操作设计的锁类型,当一个线程获取共享锁之后,其他线程仍然可以获取共享锁来执行读操作,但是无法获取排他锁执行写操作。这种设计的核心原因是读操作不会修改共享资源的内容,多个线程同时读不会产生数据不一致的问题,因此不需要互斥。
排他锁(X锁)的特性
排他锁是专门为写操作设计的锁类型,当一个线程获取排他锁之后,其他线程无论是想要获取共享锁执行读操作,还是想要获取排他锁执行写操作,都会被阻塞。这是因为写操作会修改共享资源的内容,如果允许其他线程同时读或者写,会产生脏读、数据覆盖等一致性问题,因此必须保证写操作的完全排他性。
共享锁与排他锁的兼容性矩阵
两种锁之间的兼容性可以通过矩阵清晰展示,矩阵的行列分别代表当前已经持有的锁类型,以及新请求的锁类型,结果表示新请求是否可以成功获取锁:
| 已持有锁类型 | 新请求共享锁(S) | 新请求排他锁(X) |
|---|---|---|
| 无锁 | 允许 | 允许 |
| 共享锁(S) | 允许 | 阻塞 |
| 排他锁(X) | 阻塞 | 阻塞 |
从矩阵可以看出,只有多个共享锁之间是相互兼容的,其他所有组合都存在互斥关系,这就是读写锁互斥关系的核心规则。
代码示例演示兼容性规则
下面以Java中的ReentrantReadWriteLock为例,演示共享锁和排他锁的兼容性逻辑:
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadWriteLockDemo {
private static final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
private static final ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
private static final ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
public static void main(String[] args) {
// 场景1:两个读线程同时获取共享锁,不会互斥
new Thread(() -> {
readLock.lock();
try {
System.out.println("线程1获取共享锁,执行读操作");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
readLock.unlock();
}
}).start();
new Thread(() -> {
readLock.lock();
try {
System.out.println("线程2获取共享锁,执行读操作");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
readLock.unlock();
}
}).start();
// 场景2:读线程持有共享锁时,写线程获取排他锁会被阻塞
new Thread(() -> {
readLock.lock();
try {
System.out.println("线程3获取共享锁,执行读操作,持有锁3秒");
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
readLock.unlock();
}
}).start();
new Thread(() -> {
try {
// 等待读线程先获取锁
Thread.sleep(500);
writeLock.lock();
try {
System.out.println("线程4获取排他锁,执行写操作");
} finally {
writeLock.unlock();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
// 场景3:写线程持有排他锁时,读线程获取共享锁会被阻塞
new Thread(() -> {
try {
Thread.sleep(3500);
writeLock.lock();
try {
System.out.println("线程5获取排他锁,执行写操作,持有锁3秒");
Thread.sleep(3000);
} finally {
writeLock.unlock();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
new Thread(() -> {
try {
// 等待写线程先获取锁
Thread.sleep(4000);
readLock.lock();
try {
System.out.println("线程6获取共享锁,执行读操作");
} finally {
readLock.unlock();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
运行上述代码可以观察到,前两个读线程的读操作会同时执行,证明共享锁之间兼容;读线程持有锁期间,写线程的获取操作会等到读线程释放锁之后才执行,证明共享锁和排他锁互斥;写线程持有锁期间,读线程的获取操作会等到写线程释放锁之后才执行,证明排他锁和共享锁互斥。
实际开发中的注意事项
在使用读写锁时,需要注意避免锁升级的问题,也就是一个线程已经持有共享锁的情况下,尝试获取排他锁,这种情况会导致线程自己阻塞,因为共享锁没有释放的情况下,排他锁无法获取。另外如果读操作和写操作都需要访问共享资源,要根据场景选择合适的锁类型,读多写少的场景使用读写锁可以大幅提升性能,而写多或者读写均衡的场景,普通互斥锁可能是更合适的选择。