目录
ReentrantReadWriteLock的特点
ReentrantLock本身是一个独占锁,与优化后的synchronized在并发性能上没有太多的优势,所以需要分场景优化。基于读写分离的思想(比如mysql的一主多从,读写分离)那么ReentrantReadWriteLock就诞生了,只有读与写同时发生时需要互斥,读与读之间是安全的。那么就是AQS的独占和共享模式。
读写锁适用读多些少的场景,特点有:
1、允许多个线程同时读取共享变量
2、只允许一个线程写共享变量
3、如果一个写线程在执行写操作时,禁止读线程读取共享变量
读写锁与互斥锁的主要区别:读写锁允许多个线程同时读共享变量,而互斥锁是不允许的,这就是性能较优的关键。
读写锁与COW(Copy On Write)模式也很用于混淆:相同点都是读多写少的模式否则对性能影响较大。 不同点 读写锁:写较多时,读会进行等待,相互斥,但是没有数据一致性问题。 COW:写较多时,不会影响读的性能,但是可能出现短时间一致性的问题,以及大量的内存占用,影响GC。
ReentrantReadWriteLock的结构
先看看顶层接口ReadWriteLock:
public interface ReadWriteLock {
Lock readLock();
Lock writeLock();
}
在看看实现类ReentrantReadWriteLock:
public class ReentrantReadWriteLock implements ReadWriteLock {
// 读写锁是内部类,继承自内部类Sync, AQS
private final ReentrantReadWriteLock.ReadLock readerLock;
private final ReentrantReadWriteLock.WriteLock writerLock;
public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }
public ReentrantReadWriteLock.ReadLock readLock() { return readerLock; }
// 读写锁类的父类Sync,AQS子类
final Sync sync;
abstract static class Sync extends AbstractQueuedSynchronizer { //省略内容 }
}
在具体看看读写锁的,lock、unlock方法(刚好分别是AQS的独占锁和共享锁):
public class ReentrantReadWriteLock implements ReadWriteLock {
// 写锁
public static class WriteLock implements Lock {
private final Sync sync;
public void lock() {
sync.acquire(1); // AQS独占锁的获取方法
}
public void unlock() {
sync.release(1); // AQS独占锁的释放方法
}
}
// 读锁
public static class ReadLock implements Lock {
private final Sync sync;
public void lock() {
sync.acquireShared(1); // AQS共享锁的获取方法
}
public void unlock() {
sync.releaseShared(1); // AQS共享锁的释放方法
}
}
}
ReentrantReadWriteLock项目上的使用
背景:项目上的使用,数据本身不多,并且修改也是基本不会发生(参考上面的读写锁特点,以及与COW的特点对比)。但是为了防止数据改变之后,还需要重启等,所以还是可以运行定时线程执行重新查询全量数据操作,所以使用了读写锁。当前因为数据半年不会变动,所以使用Spring Boot的CommandLineRunner回调加载实现(回调的执行时机可以参见:Spring Boot源码(二) - SpringApplication的run方法)。但是如果后续需要定时任务更新缓存,则可以参见使用定时任务线程执行,如下:
@Slf4j
@Repository
public class MdmAgencyCache implements InitializingBean {
public static final ScheduledExecutorService SINGLE_POOL = Executors.newSingleThreadScheduledExecutor();
@Override
public void afterPropertiesSet() throws Exception {
// 当前Bean的声明周期回调时初始化定时任务
SINGLE_POOL.scheduleAtFixedRate(() -> {
// 1、查询全量数据
// 2、在写锁中,写缓存
writeAll(allAgency);
}), 0, 100, TimeUnit.SECONDS);
}
}
项目上的使用方式如下,写锁与查询数据并写入缓存对应, 读锁与读取缓存数据对应【读与读可并行】。
@Slf4j
@Repository
public class MdmAgencyCache implements InitializingBean, CommandLineRunner {
/**
* 缓存 {@code CONF_DELY_SHIPPING_CONDITION}数据
*/
private static final Set<RemoteAgencyDTO> CACHE = new ConcurrentHashSet<>(50);
private final MdmFeignService mdmFeignService;
public MdmAgencyCache(MdmFeignService mdmFeignService) {
this.mdmFeignService = mdmFeignService;
}
@Override
public void run(String... args) {
final List<RemoteAgencyDTO> allAgency = mdmFeignService.getAllAgency();
log.info("mdmFeignService RemoteAgencyDTO list= {}", JSON.toJSONString(allAgency));
if (CollectionUtil.isEmpty(allAgency)) {
log.error("获取全量 *** 信息错误");
throw new RuntimeException("获取全量 *** 信息错误");
}
writeAll(allAgency);
}
/**
* 读写锁
*/
private static final ReadWriteLock READ_WRITE_LOCK = new ReentrantReadWriteLock();
private final Lock readLock = READ_WRITE_LOCK.readLock();
private final Lock writeLock = READ_WRITE_LOCK.writeLock();
@Override
public void afterPropertiesSet() throws Exception {
}
/**
* 写入操作
* @param value 单条数据
*/
public void writeAll(List<RemoteAgencyDTO> value) {
writeLock.lock();
try {
CACHE.addAll(value);
} finally {
writeLock.unlock();
}
}
/**
* 办事处编码获取办事处名称
*
* @param agencyCode ***编码
* @return ***名称
*/
public String getAgencyName(String agencyCode) {
readLock.lock();
try {
for (RemoteAgencyDTO dto : CACHE) {
if (dto.getCode().equals(agencyCode)) {
return dto.getName();
}
}
return null;
} catch (Exception e) {
return null;
} finally {
readLock.unlock();
}
}
}