曾经我们介绍AQS时,基本都是排它锁(互斥锁),这些锁在同一时刻只允许一个线程进行访问,而读写锁在同一时刻允许多个线程访问。
当读操作远远高于写操作时,这时候使用读写锁让读-读可以并发,提高性能。写操作肯定要互斥,因为要防止数据的脏读。类似于数据库中的select ... from ... lock in share mode,
读写锁是基于共享锁实现的,因为多个读的线程可以同时的获取到锁,锁的Owner有多个。
类大致如下。
public class ReentrantReadWriteLock implements ReadWriteLock { private final ReentrantReadWriteLock.ReadLock readerLock; private final ReentrantReadWriteLock.WriteLock writerLock; final Sync sync; //写锁 public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; } //读锁 public ReentrantReadWriteLock.ReadLock readLock() { return readerLock; } }
首先我们来自己创建一个可以读写的类:
@Slf4j class DataContainer { private ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock(); //获取写锁对象 private ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock(); //获取读锁对象 private ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock(); private String s = "date"; //读取数据,只要不涉及到写数据只要使用读锁即可 public String getData() throws InterruptedException { String data = null; readLock.lock(); try { data = s; log.debug("读取数据..."); Thread.sleep(2000); } finally { readLock.unlock(); } return data; } public void setData(String data) throws InterruptedException { writeLock.lock(); try { s = data; log.debug("写入数据..."); Thread.sleep(200); } finally { writeLock.unlock(); } } }
读写用的不是同一个锁。
测试类:
public static void main(String[] args) throws InterruptedException { DataContainer dc = new DataContainer(); new Thread(() -> { log.debug("{}", dc.getData()); }, "t1").start(); new Thread(() -> { log.debug("{}", dc.getData()); }, "t2").start(); //10:35:01.435 [t2] 读取数据... //10:35:01.435 [t1] 读取数据... //10:35:03.445 [t2] date //10:35:03.445 [t1] date }
可以看到两次重读的读取操作都同时得到锁了。而写读或者是写写操作只能是互斥的得到锁。
特点:
我们先来做一个单线程下的缓存。入戏,具体的dao层就是普通的查找和修改,直接略过
@Slf4j //装饰者模式,给dao层加一个缓存功能 class StudentDaoCache extends StudentDao { private StudentDao dao = new StudentDao(); //缓存容器 private Map<QueryObject, Student> cache = new HashMap<>(); @Override public Student getStudent(String sno) throws SQLException { QueryObject queryObject = new QueryObject("getStudent", sno); //1.先从缓存中找 Student student = cache.get(queryObject); if (student != null) { log.debug("从缓存中拿到{}数据", sno); return student; } //2.如果缓存没有,再从数据库找 student = dao.getStudent(sno); //把查到的数据加入到缓存 cache.put(queryObject, student); return student; } @Override public boolean updateStudent(Student student) throws SQLException { //需要先清空缓存 cache.clear(); return dao.updateStudent(student); } //把查询的sql语句和参数整体作为key class QueryObject { private String String; private Object args; public QueryObject(java.lang.String string, Object args) { String = string; this.args = args; } //重写hashcode和equals @Override public boolean equals(Object o) {} @Override public int hashCode() {} } }
测试类:
public class AQSTest { public static void main(String[] args) throws InterruptedException, SQLException { StudentDao dao = new StudentDaoCache(); Student student = dao.getStudent("2019139001"); Student student1 = dao.getStudent("2019139004"); Student student2 = dao.getStudent("2019139001"); Student student3 = dao.getStudent("2019139001"); student.name = "张三"; dao.updateStudent(student); Student student4 = dao.getStudent("2019139001"); //12:40:04.661 [main] 查找数据库---找到结果Student{sno='2019139001', name='王五', age=20} //12:40:04.680 [main] 查找数据库---找到结果Student{sno='2019139004', name='欧阳冲', age=19} //12:40:04.681 [main] 从缓存中拿到2019139001数据 //12:40:04.681 [main] 从缓存中拿到2019139001数据 //12:40:04.967 [main] 更新2019139001的身份信息 //12:40:04.982 [main] 查找数据库---找到结果Student{sno='2019139001', name='张三', age=20} } }
可以看到缓存在单线程下是能够正确的工作,但如果有多个用户同时操作数据库,同时维护该缓存,那么会出现如下的问题:
我们需要加锁来解决问题:
@Slf4j class StudentDaoCache extends StudentDao { private ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock(); private ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock(); private ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock(); private StudentDao dao = new StudentDao(); //缓存容器 private Map<QueryObject, Student> cache = new HashMap<>(); @Override public Student getStudent(String sno) throws SQLException { QueryObject queryObject = new QueryObject("getStudent", sno); Student student = null; //加读锁 readLock.lock(); try { //1.先从缓存中找 student = cache.get(queryObject); if (student != null) { log.debug("从缓存中拿到{}数据", sno); return student; } } finally { readLock.unlock();//释放读锁,因为不支持锁的升级 } //加写锁,因为涉及到写操作了 writeLock.lock(); try { //双重检查,再检查一遍缓存是否已经有了,反之多个读线程 student = cache.get(queryObject); if (student != null) { log.debug("从缓存中拿到{}数据", sno); return student; } //再从数据库找 student = dao.getStudent(sno); //把查到的数据加入到缓存 cache.put(queryObject, student); return student; }finally { writeLock.unlock(); } } @Override public boolean updateStudent(Student student) throws SQLException { writeLock.lock(); try { //清空缓存和更新库这两个操作变成整体的了,原子的 cache.clear(); return dao.updateStudent(student); } finally { writeLock.unlock(); } } }
加了读写锁后那几个问题也就相应的解决了。以上的缓存写的比较low,不要太在意。
JDK8加入,是为了进一步优化读性能,它的特点是在使用读锁,写锁时必须配合【戳】使用。如:
加解读锁:
long stamp= stampedLock.readLock(); stampedLock.unlockRead(stamp);
加解写锁:
long stamp = stampedLock.writeLock(); stampedLock.unlock(stamp);
乐观读,StampedLock支持tryOptimisticRead()方法(乐观读),读取完毕后需要做一次戳校验如果校验通过,表示这期间确实没有写操作,数据可以安全使用,如果校验没通过,需要重新获取读锁,保证数据安全。
public long tryOptimisticRead() { long s; return (((s = state) & WBIT) == 0L) ? (s & SBITS) : 0L; } //验戳 public boolean validate(long stamp) { U.loadFence(); return (stamp & SBITS) == (state & SBITS); }
使用方式:
long stamp = stampedLock.tryOptimisticRead(); //验戳 if (stampedLock.validate(stamp)) { //锁升级 }
该锁不支持重入