诞生背景
前面提到的synchronized加锁基本都是排他锁,也就是说只要是多线程竞争情况下,就只能有一个线程获得锁,其余线程阻塞。虽然已经对synchronized进行性能优化了,但是这是从技术层面的优化,它的本质还是排他锁。
我们也可以从业务的角度出发,比如存在这种情况:多线程访问同步代码块时,大部分情况下都是读操作,少部分才是写操作。这样的话,如果还是将没获得锁的线程都阻塞起来,太影响性能了。因为读操作即使存在多个线程访问共享变量,但是不涉及修改也就不存在线程安全性问题了。
所以需要有个更加灵活的锁来处理这种读多写少的业务场景,于是乎,有了ReentrantReadWriteLock读写锁。
特点
读读(不互斥)
基于以上的业务场景,如果是多个线程同时读共享数据的话,应该是不需要阻塞的。下面来通过案例证明。
public class ReadWriteLockTest {
public static void main(String[] args) {
ReadWriteLockDemo demo = new ReadWriteLockDemo();
demo.cacheMap.put("data", 1);
// 线程1
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
demo.read();
}
}
}).start();
// 线程2
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
demo.read();
}
}
}).start();
}
}
class ReadWriteLockDemo {
ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
Lock read = readWriteLock.readLock();
// 共享数据
Map cacheMap = new HashMap<>();
public void read() {
read.lock(); // 上读锁并且不释放
System.out.println("线程:" + Thread.currentThread().getName() + "开始读数据...");
System.out.println(Thread.currentThread().getName() + "读数据为:" + cacheMap.get("data"));
System.out.println("线程:" + Thread.currentThread().getName() + "结束读数据!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
执行结果:在读锁没释放的情况下,线程1和线程2仍然可以同时读取数据,说明读读不存在互斥。
线程:Thread-1开始读数据...
Thread-1读数据为:1
线程:Thread-1结束读数据!
线程:Thread-0开始读数据...
Thread-0读数据为:1
线程:Thread-0结束读数据!
读写(互斥)
这种既有读又要写的情况下,那么只能等读完再写或者写完再读,否则数据是不准确的。也就是说,读写之间存在互斥。下面通过案例证明。
public class ReadWriteLockTest02 {
public static void main(String[] args) {
ReadWriteLockDemo02 demo = new ReadWriteLockDemo02();
demo.cacheMap.put("data", 1);
// 线程1 读数据
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
demo.read();
}
}
}).start();
// 线程2 写数据
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
demo.write();
}
}
}).start();
}
}
class ReadWriteLockDemo02 {
ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
Lock read = readWriteLock.readLock();
Lock write=readWriteLock.writeLock();
Map cacheMap = new HashMap<>();
public void read() {
read.lock(); // 获取读锁
System.out.println("线程:" + Thread.currentThread().getName() + "开始读数据...");
System.out.println(Thread.currentThread().getName() + "读数据为:" + cacheMap.get("data"));
System.out.println("线程:" + Thread.currentThread().getName() + "结束读数据!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
read.unlock(); // 释放读锁
}
}
public void write() {
write.lock();// 获得写锁
System.out.println(Thread.currentThread().getName() + "开始写数据...");
double data = Math.random();
cacheMap.put("data", data);
System.out.println(Thread.currentThread().getName() + "写数据为:" + data);
System.out.println(Thread.currentThread().getName() + "结束写数据!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
write.unlock();// 释放写锁
}
}
}
执行结果:线程1和线程2交替执行,说明此时互斥。
线程:Thread-0开始读数据...
Thread-0读数据为:1
线程:Thread-0结束读数据!
Thread-1开始写数据...
Thread-1写数据为:0.1469638477677232
Thread-1结束写数据!
线程:Thread-0开始读数据...
Thread-0读数据为:0.1469638477677232
线程:Thread-0结束读数据!
假设将以上代码:read.unlock(); // 释放读锁注释掉,让读锁一直不释放。
执行结果:由于线程1读锁一直不释放,所以线程2写锁就一直阻塞,这也说明了读写是互斥的。
线程:Thread-0开始读数据...
Thread-0读数据为:1
线程:Thread-0结束读数据!
线程:Thread-0开始读数据...
Thread-0读数据为:1
线程:Thread-0结束读数据!
线程:Thread-0开始读数据...
Thread-0读数据为:1
线程:Thread-0结束读数据!
写写(互斥)
到了写锁,我们更加确定这是互斥的了。下面还是通过案例来证明。
public class ReadWriteLockTest03 {
public static void main(String[] args) {
ReadWriteLockDemo03 demo = new ReadWriteLockDemo03();
demo.cacheMap.put("data", 1);
// 线程1 写数据
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
demo.write();
}
}
}).start();
// 线程2 写数据
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
demo.write();
}
}
}).start();
}
}
class ReadWriteLockDemo03 {
ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
Lock write=readWriteLock.writeLock();
Map cacheMap = new HashMap<>();
public void write() {
write.lock();// 获得写锁
System.out.println(Thread.currentThread().getName() + "开始写数据...");
double data = Math.random();
cacheMap.put("data", data);
System.out.println(Thread.currentThread().getName() + "写数据为:" + data);
System.out.println(Thread.currentThread().getName() + "结束写数据!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
// write.unlock();// 释放写锁
}
}
}
执行结果:第一个线程获得锁后,由于锁一直不释放,所以一直都是该线程写数据,其余线程只能阻塞。说明了写写是互斥的。如果将代码:write.unlock();注释取消,则线程1和线程2根据cpu调度来确定执行顺序(有了锁的释放,才有其他线程争抢锁的可能)。
读写锁实现缓存的案例
public class CacheTest {
private Map<String, Object> cache = new HashMap<String, Object>();
private ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
public Object getData(String key) {
// 首先上读锁
rwLock.readLock().lock();
// 首先从缓存中获取
Object value = null;
try {
Thread.sleep(1000);
value = cache.get(key);
if (value == null) {
// 如果缓存中没有数据,那么就从数据库中获取
// 但此时需要上写锁,只需要让一个进程进行写数据
// 首先去除读锁,然后加上写锁
rwLock.readLock().unlock();
// 假设线程1、线程2、线程3都执行到了这里,线程1获得写锁继续往下执行,线程2和线程3阻塞
rwLock.writeLock().lock();
try {
// 注意防止多线程运行到上一步,某个线程写完数据后
// 别的线程就需要看是否有数据再决定是否进行写操作
// 在写之前再读一次,防止最开始的线程都进行写操作</span>
value = cache.get(key);
// 第一个线程写完后,防止后面的线程再次写数据
if (value == null) {
System.out.println("有线程写数据........");
value = "数据库中获取";
// 将数据放入缓存
cache.put(key, value);
System.out.println("数据写完了.......");
// 线程1修改完数据释放写锁,如果这里不加判断,则线程2和线程3还是会继续写入数据
// 加入判断后,线程2和线程3发现value已经有线程1之前写入的值了,就直接获得该值并返回
}
} finally {
rwLock.readLock().lock();// 恢复读锁,锁的重入
rwLock.writeLock().unlock();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
rwLock.readLock().unlock();// 解读锁
}
return value;
}
}
以上案例参考:https://blog.csdn.net/u011535508/article/details/52527894