生产案例
最近在做一个功能的时候,需要用到MCC码。然后看了一下前人写的代码,发现是用的读写锁ReadWriteLock去做的定时刷新。
读写锁原理
ReadWriteLock接口表示存在2把锁:一个读取锁,一个写入锁。
public interface ReadWriteLock {
/**
* Returns the lock used for reading.
*
* @return the lock used for reading
*/
Lock readLock();
/**
* Returns the lock used for writing.
*
* @return the lock used for writing
*/
Lock writeLock();
}
基于AQS实现的ReentrantReadWriteLock中,单个AQS子类将同时管理读取加锁与写入加锁。ReentrantReadWriteLock使用了一个16位的状态表示写入锁的计数,并且使用了另一个16位的状态来表示读取锁的计数。在读取锁上的操作使用共享的获取方法与释放方法,在写入锁的操作使用独占的方法与释放方法。
AQS内部维护了一个等待线程队列,其中记录了某个线程请求的是独占访问还是共享访问。在ReentrantReadWriteLock中,当锁可用时,如果位于队列头部的线程执行写入操作,那么线程会得到这个锁,如果位于队列头部的线程执行读取访问,那么队列中在第一个写入线程之前的所有线程都将获得这个锁。
类图
从上面的类图可以发现,ReentrantReadWriteLock是基于AQS去实现的。里面读取锁是通过ReadLock控制的,写入锁是WriteLock控制的。
其中的Sync不且复用了AQS的能力,并且可以自主选择是否为公平锁:NonfairSync,FairSync
使用方式
以刚才的MCC为例,要完成类似以上功能需求,黑名单控制:
- 根据用户手机号判断是否为黑名单,如果是则拒绝,反之则放过
- 每天晚上定时更新一次黑名单列表
假设上述需求用读写锁去做,要怎么实现呢?
我们实现一个基本的,可能代码结构不一定标准,只是为说明使用方式
基础使用
public class ReadWriteLockTest {
/**
* 读锁测试
*/
@Test
public void testReadLock() {
// 读写锁,控制并发
ReadWriteLock lock = new ReentrantReadWriteLock();
try {
// 获取读锁
lock.readLock().lock();
System.out.println("check result is " + isBlackNum("13699001634"));
} finally {
// 释放读锁
lock.readLock().unlock();
}
}
/**
* 写锁测试
*/
@Test
public void testWriteLock() {
// 读写锁,控制并发
ReadWriteLock lock = new ReentrantReadWriteLock();
try {
refreshBlackList();
lock.writeLock().lock();
} finally {
lock.writeLock().unlock();
}
}
/**
* 是否命中黑名单
* @param telephone 手机号
* @return
*/
public boolean isBlackNum(String telephone) {
System.out.println("telephone" + telephone + " check");
return Optional.ofNullable(getBlackList()).orElse(Lists.newArrayList()).contains(telephone);
}
/**
* 返回黑名单列表
* @return
*/
public List<String> getBlackList() {
return Stream.of("13699001634", "13699001633").collect(Collectors.toList());
}
/**
* 刷新黑名单
*/
public void refreshBlackList() {
System.out.println("refresh blackList");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
- 读取锁:lock.readLock(),先lock,后面释放unlock
- 写入锁:lock.writeLock(),也是先lock,后面释放
读取锁-共享
前面说了读取锁是共享的,也就是读取的时候多个线程可以同时读取信息,测试一下。
模拟3个线程同时请求,对判断方法的线程休眠3s。
public class ReadWriteLockTest {
/**
* 读取并发读取测试
*/
@Test
public void readLockShareTest() {
ReadWriteLock lock = new ReentrantReadWriteLock();
ExecutorService executor = Executors.newCachedThreadPool();
List<Callable<Boolean>> tasks = Lists.newArrayList();
for (int i = 0; i < 3; i++) {
StringBuilder telephoneSb = new StringBuilder("1369900163");
telephoneSb.append(i);
Callable<Boolean> task = () -> {
try {
lock.readLock().lock();
return isBlackNum(telephoneSb.toString());
} finally {
lock.readLock().unlock();
}
};
tasks.add(task);
}
long beginTime = System.currentTimeMillis();
List<Future<Boolean>> futureLit = Lists.newArrayList();
tasks.stream().forEach(task -> {
Future<Boolean> isBlackTelephone = executor.submit(task);
futureLit.add(isBlackTelephone);
});
futureLit.stream().forEach(booleanFuture -> {
try {
booleanFuture.get();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
});
long endTime = System.currentTimeMillis();
System.out.println("总共耗时:" + (endTime - beginTime));
}
/**
* 是否命中黑名单
* @param telephone 手机号
* @return
*/
public boolean isBlackNum(String telephone) {
long beginTime = System.currentTimeMillis();
System.out.println("收到获取黑名单列表请求:" + beginTime + ",telephone" + telephone + " check");
return Optional.ofNullable(getBlackList()).orElse(Lists.newArrayList()).contains(telephone);
}
/**
* 返回黑名单列表
* @return
*/
public List<String> getBlackList() {
try {
// 线程休眠3s,模拟远程获取数据
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return Stream.of("13699001634", "13699001633").collect(Collectors.toList());
}
}
打印结果:
收到获取黑名单列表请求:1586091816390,telephone13699001630 check
收到获取黑名单列表请求:1586091816390,telephone13699001631 check
收到获取黑名单列表请求:1586091816390,telephone13699001632 check
总共耗时:3023
从上面可以看出,3个线程同时请求黑名单方法,但是3个请求几乎同时到达,所以这个读取并没有阻塞,都获取了锁。如果是阻塞同步方法的话,正常应该是3s一次打印日志。
写入锁-独占
public class ReadWriteLockTest {
/**
* 读取并发写入测试
*/
@Test
public void readLockShareTest() {
ReadWriteLock lock = new ReentrantReadWriteLock();
ExecutorService executor = Executors.newCachedThreadPool();
List<Callable<Boolean>> tasks = Lists.newArrayList();
for (int i = 0; i < 3; i++) {
StringBuilder telephoneSb = new StringBuilder("1369900163");
telephoneSb.append(i);
Callable<Boolean> task = () -> {
try {
lock.writeLock().lock();
return isBlackNum(telephoneSb.toString());
} finally {
lock.writeLock().unlock();
}
};
tasks.add(task);
}
long beginTime = System.currentTimeMillis();
List<Future<Boolean>> futureLit = Lists.newArrayList();
tasks.stream().forEach(task -> {
Future<Boolean> isBlackTelephone = executor.submit(task);
futureLit.add(isBlackTelephone);
});
futureLit.stream().forEach(booleanFuture -> {
try {
booleanFuture.get();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
});
long endTime = System.currentTimeMillis();
System.out.println("总共耗时:" + (endTime - beginTime));
}
/**
* 是否命中黑名单
* @param telephone 手机号
* @return
*/
public boolean isBlackNum(String telephone) {
long beginTime = System.currentTimeMillis();
System.out.println("收到获取黑名单列表请求:" + beginTime + ",telephone" + telephone + " check");
return Optional.ofNullable(getBlackList()).orElse(Lists.newArrayList()).contains(telephone);
}
/**
* 返回黑名单列表
* @return
*/
public List<String> getBlackList() {
try {
// 线程休眠3s,模拟远程获取数据
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return Stream.of("13699001634", "13699001633").collect(Collectors.toList());
}
}
打印日志:收到获取黑名单列表请求:1586091930884,telephone13699001630 check
收到获取黑名单列表请求:1586091933894,telephone13699001631 check
收到获取黑名单列表请求:1586091936894,telephone13699001632 check
总共耗时:9019
发现总共耗时9s多,并且每个请求直接相隔3s,说明已经是串行执行的
源码剖析
应用场景
读写锁适用于
- 读多写少的场景
- 对实时性要求不是很高
比如黑白名单这种定时维护,能有效提高效率,并且防止并发
后续我们学习一下AQS的原理及源码分析,这样回头看一下这个理解会更加深刻