ReentrantReadWriteLock
是读写锁,和 ReentrantLock
会有所不同,对于读多写少的场景使用 ReentrantReadWriteLock
性能会比 ReentrantLock
高出不少。ReentrantLock
即使是多线程读也需要每个线程获取锁。当读操作远远高于写操作时,这时候使用 读写锁 让 读-读 可以并发,提高性能,类似于数据库中的 select ...from ... lock in share mode
。ReentrantLock
类似,其它线程无论读还是写都必须获取锁。writeLock
与 readLock
(但必须先获取 writeLock
再获取 readLock
, 反过来进行获取会导致死锁)。public class ReadWriteLockSample {
/**
* 测试『读-读』并发。
*
* @throws InterruptedException 中断异常
*/
@Test
public void test01() throws InterruptedException {
DataContainer container = new DataContainer();
Thread t1 = new Thread(container::read, "t1");
Thread t2 = new Thread(container::read, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
// 10:05:07.574 [t1] get read lock
// 10:05:07.574 [t2] get read lock
// 10:05:08.590 [t2] read unlock
// 10:05:08.590 [t1] read unlock
// 从上面结果可以看到,t1锁定期间,t2读操作不受影响。
}
/**
* 测试『读-写』相互阻塞。
*
* @throws InterruptedException 中断异常
*/
@Test
public void test02() throws InterruptedException {
DataContainer container = new DataContainer();
Thread t1 = new Thread(container::read, "t1");
sleep(1);
Thread t2 = new Thread(container::write, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
// 10:06:10.423 [t2] get write lock
// 10:06:10.423 [t1] get read lock
// 10:06:11.433 [t2] write unlock
// 10:06:12.443 [t1] read unlock
}
/**
* 测试『写-写』相互阻塞。
*
* @throws InterruptedException 中断异常
*/
@Test
public void test03() throws InterruptedException {
DataContainer container = new DataContainer();
Thread t1 = new Thread(container::write, "t1");
sleep(1);
Thread t2 = new Thread(container::write, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
// 10:06:59.386 [t2] get write lock
// 10:06:59.386 [t1] get write lock
// 10:07:00.409 [t2] write unlock
// 10:07:01.419 [t1] write unlock
}
}
/**
* 提供一个 数据容器类 内部分别使用:
* 读锁保护数据的 `read()` 方法,写锁保护数据的 `write()` 方法。
*/
@Slf4j
class DataContainer {
private Object data;
private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
private ReentrantReadWriteLock.ReadLock r = rwl.readLock();
private ReentrantReadWriteLock.WriteLock w = rwl.writeLock();
public Object read() {
log.debug("get read lock");
r.lock();
try {
sleep(1);
return data;
} finally {
log.debug("read unlock");
r.unlock();
}
}
public void write() {
log.debug("get write lock");
w.lock();
try {
sleep(1);
} finally {
log.debug("write unlock");
w.unlock();
}
}
}
r.lock();
try {
// ...
w.lock();
try {
// ...
} finally{
w.unlock();
}
} finally{
r.unlock();
}
public class CachedData {
private Object data;
private volatile boolean cacheValid;
private static final ReentrantReadWriteLock RWL = new ReentrantReadWriteLock();
public void processCachedData() {
RWL.readLock().lock();
if (!cacheValid) {
// 加写锁前需要释放读锁。
RWL.readLock().unlock();
RWL.writeLock().lock();
try {
// 判断是否有其它线程已经获取了写锁、更新了缓存, 避免重复更新。
if (!cacheValid) {
data = "value...";
cacheValid = true;
}
// 降级为读锁, 释放写锁, 这样能够让其它线程读取缓存。
RWL.readLock().lock();
} finally {
RWL.writeLock().unlock();
}
}
try {
use(data);
} finally {
RWL.readLock().unlock();
}
}
public void use(Object data) {
// do something...
}
}
使用读写锁,实现一个简单的按需加载缓存。
public class GenericCachedDao<T> {
Map<SqlPair, T> cache = new HashMap<>();
ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
GenericDao dao = new GenericDao();
public boolean update(String sql, Object... params) {
SqlPair key = new SqlPair(sql, params);
// 加写锁, 防止其它线程对缓存读取和更改。
rwl.writeLock().lock();
try {
boolean isUpdated = dao.update(sql, params);
cache.clear();
return isUpdated;
} finally {
rwl.writeLock().unlock();
}
}
public T queryOne(Class<T> beanClass, String sql, Object... params) {
SqlPair key = new SqlPair(sql, params);
// 加读锁,防止其他线程对缓存更改。
rwl.readLock().lock();
try {
T value = cache.get(key);
if (null != value) {
return value;
}
} finally {
rwl.readLock().unlock();
}
// 加写锁,防止其它线程对缓存读取和更改。
rwl.writeLock().lock();
try {
// 为了防止重复查询数据库,需要进行二次检查。
T value = cache.get(key);
if (null == value) {
// 仍为空则查询数据库。
value = dao.queryOne(beanClass, sql, params);
// 将数据库查询的结果放入缓存。
cache.put(key, value);
}
return value;
} finally {
rwl.writeLock().unlock();
}
}
}
class GenericDao {
public <T> T queryOne(Class<T> beanClass, String sql, Object... params) {
// do something...
return null;
}
public boolean update(String sql, Object... params) {
if (!sql.isEmpty() && params.length > 0) {
// do something...
return true;
}
return false;
}
}
@AllArgsConstructor
@EqualsAndHashCode
class SqlPair {
private String sql;
private Object[] params;
}
CAS
去更新。读写锁用的是同一个
Sycn
同步器,因此等待队列、state
等也是同一个。
ReentrantLock
加锁相比没有特殊之处,不同是写锁状态占了 state
的低 16 位,而读锁使用的是 state
的高 16 位。r.lock
,这时进入读锁的 sync.acquireShared(1)
流程,首先会进入 tryAcquireShared()
流程。如果有写锁占据,那么 tryAcquireShared()
返回 -1 表示失败。(补充:0 表示成功,但后继节点不会继续唤醒;正数表示成功,而且数值是还有几个后继节点需要唤醒,读写锁返回 1。)sync.doAcquireShared(1)
流程,首先也是调用 addWaiter()
添加节点,不同之处在于节点被设置为 Node.SHARED
模式而非 Node.EXCLUSIVE
模式,注意此时 t2 仍处于活跃状态。tryAcquireShared(1)
来尝试获取锁。doAcquireShared()
内 for (;;)
循环一次,把前驱节点的 waitStatus
改为 -1,再 for (;;)
循环一次尝试 tryAcquireShared(1)
如果还不成功,那么在 parkAndCheckInterrupt()
处 park
(灰色表示)。sync.release(1)
流程,调用 sync.tryRelease(1)
成功;sync.unparkSuccessor
,即让老二恢复运行,这时 t2 在 doAcquireShared()
内parkAndCheckInterrupt()
处恢复运行,这回再来一次 for (;;)
执行 tryAcquireShared()
成功则让读锁计数加一;setHeadAndPropagate(node, 1)
,它原本所在节点被置为头节点。setHeadAndPropagate()
方法内还会检查下一个节点是否是 shared
,如果是则调用 ;doReleaseShared()
将 head
的状态从 -1 改为 0 并唤醒老二,这时 t3 在 doAcquireShared()
内parkAndCheckInterrupt()
处恢复运行;for (;;)
执行 tryAcquireShared()
成功则让读锁计数加一;setHeadAndPropagate(node, 1)
,它原本所在节点被置为头节点;shared
了,因此不会继续唤醒 t4 所在节点。sync.releaseShared(1)
中,调用 tryReleaseShared(1)
让计数减一,但由于计数还不为零;sync.releaseShared(1)
中,调用 tryReleaseShared(1)
让计数减一,这回计数为零了,进入doReleaseShared()
将头节点从 -1 改为 0 并唤醒老二,即之后 t4 在 acquireQueued()
中 parkAndCheckInterrupt()
处恢复运行,再次 for (;;)
这次自己是老二,并且没有其他竞争,tryAcquire(1)
成功,修改头结点,流程结束。该类自
JDK
8 加入,是为了进一步优化读性能,它的特点是在使用读锁、写锁时都必须配合『戳』使用。StampedLock
是比ReentrantReadWriteLock
更快的一种锁,支持乐观读、悲观读锁和写锁。和ReentrantReadWriteLock
不同的是,StampedLock
支持多个线程申请乐观读的同时,还允许一个线程申请写锁。
public class StampedLockSample {
private static final StampedLock LOCK = new StampedLock();
public static void main(String[] args) {
// 1.加解读锁。
long stamp = LOCK.readLock();
LOCK.unlockRead(stamp);
// 2.加解写锁。
long stamp1 = LOCK.writeLock();
LOCK.unlockWrite(stamp1);
/* *
* 3.乐观锁:StampedLock 支持 tryOptimisticRead() 方法(乐观读)。
* 读取完毕后需要做一次 戳校验 如果校验通过,表示这期间确实没有写操作,数据可以安全使用;
* 如果校验没通过,需要重新获取读锁,保证数据安全。
*/
long stamp2 = LOCK.tryOptimisticRead();
// 验戳。
if (!LOCK.validate(stamp)) {
// 锁升级。
}
}
}
需求:提供一个数据容器类,内部分别使用
StampedLock
实现读锁保护数据的read()
方法,写锁保护数据的write()
方法,并测试在多线程下的使用情况。
@Slf4j
public class DataContainerStamped {
private int data;
private static final StampedLock LOCK = new StampedLock();
public DataContainerStamped(int data) {
this.data = data;
}
public int read(int readTime) {
// 乐观读。
long stamp = LOCK.tryOptimisticRead();
log.debug("optimistic read locking...stamp:[{}]", stamp);
sleep(readTime, TimeUnit.SECONDS);
// 验戳。
if (LOCK.validate(stamp)) {
log.debug("read finish...stamp:[{}], data:[{}]", stamp, data);
return data;
}
// 读锁升级。
log.debug("updating to read lock... stamp:[{}]", stamp);
stamp = LOCK.readLock();
try {
log.debug("read lock stamp:[{}]", stamp);
sleep(readTime, TimeUnit.SECONDS);
log.debug("read finish...stamp:[{}], data:[{}]", stamp, data);
return data;
} finally {
log.debug("read unlock stamp:[{}]", stamp);
// 释放读锁。
LOCK.unlockRead(stamp);
}
}
public void write(int newData) {
long stamp = LOCK.writeLock();
log.debug("write lock stamp:[{}]", stamp);
try {
sleep(2, TimeUnit.SECONDS);
this.data = newData;
} finally {
log.debug("write unlock stamp:[{}]", stamp);
LOCK.unlockWrite(stamp);
}
}
}
public class DataContainerStampedTests {
/**
* 测试 『读-读』可以优化。
*/
@Test
public void testReadAndRead() throws InterruptedException {
DataContainerStamped dataContainer = new DataContainerStamped(1);
Thread t1 = new Thread(() -> dataContainer.read(1), "t1");
sleep(500, TimeUnit.MILLISECONDS);
Thread t2 = new Thread(() -> dataContainer.read(0), "t2");
t1.start();
t2.start();
t1.join();
t2.join();
// [t1] optimistic read locking...stamp:[256]
// [t2] optimistic read locking...stamp:[256]
// [t2] read finish...stamp:[256], data:[1]
// [t1] read finish...stamp:[256], data:[1]
// 输出结果,可以看到实际没有加读锁。
}
/**
* 测试 『读-写』 时优化读补加读锁。
*/
@Test
public void testReadAndWrite() throws InterruptedException {
DataContainerStamped dataContainer = new DataContainerStamped(1);
Thread t1 = new Thread(() -> dataContainer.read(1), "t1");
sleep(500, TimeUnit.MILLISECONDS);
Thread t2 = new Thread(() -> dataContainer.write(100), "t2");
t1.start();
t2.start();
t1.join();
t2.join();
// [t1] optimistic read locking...stamp:[256]
// [t2] write lock stamp:[384]
// [t1] updating to read lock... stamp:[256]
// [t2] write unlock stamp:[384]
// [t1] read lock stamp:[513]
// [t1] read finish...stamp:[513], data:[100]
// [t1] read unlock stamp:[513]
}
}
StampedLock
不支持条件变量。StampedLock
不支持可重入。[ˈsɛməˌfɔr]
Semaphore
(信号量)是用来控制同时访问特定资源的线程数量,它通过协调各个线程,以保证合理的使用公共资源。
public static void main(String[] args) {
// 许可设置为3。
Semaphore semaphore = new Semaphore(3);
// 假设 5 个线程同时运行。
for (int i = 0; i < 5; i++) {
new Thread(() -> {
try {
// 获取许可。
semaphore.acquire();
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
log.debug("running...");
sleep(2, TimeUnit.SECONDS);
log.debug("ending...");
} finally {
// 释放许可。
semaphore.release();
}
}).start();
// 16:26.370 [Thread-1] running...
// 16:26.370 [Thread-0] running...
// 16:26.370 [Thread-2] running...
// 16:28.379 [Thread-2] ending...
// 16:28.379 [Thread-1] ending...
// 16:28.379 [Thread-0] ending...
// 16:28.379 [Thread-4] running...
// 16:28.379 [Thread-3] running...
// 16:30.391 [Thread-3] ending...
// 16:30.391 [Thread-4] ending...
}
}
需求说明:
Semaphore
限流,在访问高峰期时,让请求线程阻塞,高峰期过去再释放许可,当然它只适合限制单机线程数量,并且仅是限制线程数,而不是限制资源数(例如连接数,请对比 Tomcat
LimitLatch
的实现)。Semaphore
实现简单连接池,对比『享元模式』下的实现(用 wait-notify
),性能和可读性显然更好,注意下面的实现中线程数和数据库连接数是相等的。代码实现:
@Slf4j
public class ConnectionPoolBySemaphoreSample {
/* *
* 连接池大小。
*/
private int poolSize;
/* *
* 连接对象数组。
*/
private Connection[] connections;
/* *
* 连接状态数组 0 表示空闲, 1 表示繁忙。
*/
private AtomicIntegerArray states;
/* *
* 信号量。
*/
private Semaphore semaphore;
/**
* 初始化构造器。
*
* @param poolSize 池大小
*/
public ConnectionPoolBySemaphoreSample(int poolSize) {
this.poolSize = poolSize;
// 让许可数与资源数一致
this.semaphore = new Semaphore(poolSize);
this.connections = new Connection[poolSize];
this.states = new AtomicIntegerArray(new int[poolSize]);
for (int i = 0; i < poolSize; i++) {
connections[i] = new MockConnection("connect-" + (i + 1));
}
}
/**
* 借连接。
*
* @return {@link Connection}
*/
public Connection borrow() { // t0, t1, t2 线程进入。
// 获取许可。
try {
// 没有许可的线程,在此等待。
semaphore.acquire();
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 0; i < poolSize; i++) {
// 获取空闲连接。
if (states.get(i) == 0) {
if (states.compareAndSet(i, 0, 1)) {
log.debug("borrow {}", connections[i]);
return connections[i];
}
}
}
// *该代码段不会被执行到。
return null;
}
/**
* 归还连接。
*
* @param conn 具体的连接
*/
public void free(Connection conn) {
for (int i = 0; i < poolSize; i++) {
if (connections[i] == conn) {
states.set(i, 0);
log.debug("free {}", conn);
semaphore.release();
break;
}
}
}
}
public class ConnectionPoolBySemaphoreSampleTests {
public static void main(String[] args) {
ConnectionPoolBySemaphoreSample pool = new ConnectionPoolBySemaphoreSample(2);
// 模拟三个线程去使用连接池中的两个连接。
for (int i = 0; i < 3; i++) {
new Thread(() -> {
Connection conn = pool.borrow();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
pool.free(conn);
}).start();
}
// [Thread-0] borrow connect-1
// [Thread-1] borrow connect-2
// [Thread-0] free connect-1
// [Thread-1] free connect-2
// [Thread-2] borrow connect-1
// [Thread-2] free connect-1
}
}
Semaphore
有点像一个停车场,permits
就好像停车位数量,当线程获得了permits
就像是获得了停车位,然后停车场显示空余车位减一。
permits
(state
)为 3,这时 5 个线程来获取资源;cas
竞争成功,而 t0 和 t3 竞争失败,进入 AQS
队列 park
阻塞;permits
,接下来 t0 竞争成功,permits
再次设置为 0,设置自己为 head
节点,断开原来的 head
节点,unpark
接下来的 t3 节点,但由于 permits
是 0,因此 t3 在尝试不成功后再次进入 park
状态。“-------怕什么真理无穷,进一寸有一寸的欢喜。”
微信公众号搜索:饺子泡牛奶。