是什么
解决线程安全我们用ReentrantLock就可以了,但是对于读多写少的情况下我们继续使用它会有损性能,因为在没有写但有持续读数据的情况下,加锁是没有必要的,所以ReentrantReadWriteLock为了解决这个问题应运而生,它满足这样的规则:读读共享、读写互斥、写写互斥。更进一步的解释,它内部维护了两种锁即写锁和读锁。在没有线程获取写锁的时候,多个线程可以同时获取读锁。如果有线程获取写锁,则其他线程请求获取读锁的时候会阻塞挂起。
怎么用
ReentrantReadWriteLock API 上的使用与ReentrantLock 基本一致。这边我们以读多写少的 场景下来测试对比两者之间的性能 并同时了解ReentrantReadWriteLock 是如何使用的。 具体实现如下:
ReentrantLock性能测试代码
public class ReentrantLockDemo {
Lock lock = new ReentrantLock();
private volatile Integer value;
private static AtomicLong readTime = new AtomicLong();
private static AtomicLong writeTime = new AtomicLong();
public void add(Integer value) {
lock.lock();
this.value = value;
lock.unlock();
}
public Integer get() {
try {
lock.lock();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
return this.value;
} finally {
lock.unlock();
}
}
}
运行方法
ReentrantLockDemo demo = new ReentrantLockDemo();
CyclicBarrier cyclicBarrier = new CyclicBarrier(160);
CountDownLatch latch = new CountDownLatch(160);
// 10个写线程
for (Integer i = 0; i < 10; i++) {
new Thread(() -> {
try {
cyclicBarrier.await();
long start = System.nanoTime();
demo.add(4);
long duration = System.nanoTime() - start;
writeTime.addAndGet(duration);
latch.countDown();
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
// 150 个读线程
for (int i = 0; i < 150; i++) {
new Thread(
() -> {
try {
cyclicBarrier.await();
long start = System.nanoTime();
demo.get();
long duration = System.nanoTime() - start;
readTime.addAndGet(duration);
latch.countDown();
} catch (Exception e) {
e.printStackTrace();
}
}
).start();
}
latch.await();
System.out.println("ReentrantLockDemo 平均读耗时:" + String.valueOf(readTime.get() / 150) + "ns");
System.out.println("ReentrantLockDemo 平均写耗时:" + String.valueOf(writeTime.get() / 10) + "ns");
ReentrantReadWriteLock性能测试代码
public class ReentrantReadWriteLockDemo {
ReadWriteLock lock = new ReentrantReadWriteLock();
private Lock readLock = lock.readLock();
private Lock writeLock = lock.writeLock();
private static AtomicLong readTime = new AtomicLong();
private static AtomicLong writeTime = new AtomicLong();
private volatile Integer value;
public void add(Integer value) {//(4)
writeLock.lock();
this.value = value;
writeLock.unlock();
}
public Integer get() {//(5)
try {
readLock.lock();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
return this.value;
} finally {
readLock.unlock();
}
}
}
运行方法
ReentrantReadWriteLockDemo demo = new ReentrantReadWriteLockDemo();
CyclicBarrier cyclicBarrier = new CyclicBarrier(160);// (1)
CountDownLatch latch = new CountDownLatch(160);//(2)
// 10个写线程
for (Integer i = 0; i < 10; i++) {
new Thread(() -> {
try {
cyclicBarrier.await();
long start = System.currentTimeMillis();
demo.add(2);
long duration = System.currentTimeMillis() - start;
writeTime.addAndGet(duration);
latch.countDown();
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
// 150 个读线程
for (int i = 0; i < 150; i++) {
new Thread(
() -> {
try {
cyclicBarrier.await();
long start = System.currentTimeMillis();
demo.get();
long duration = System.currentTimeMillis() - start;
readTime.addAndGet(duration);
latch.countDown();
} catch (Exception e) {
e.printStackTrace();
}
}
).start();
}
latch.await();//(3)
System.out.println("ReentrantReadWriteLock 平均读耗时:" + String.valueOf(readTime.get() / 150) + "ns");
System.out.println("ReentrantReadWriteLock 平均写耗时:" + String.valueOf(writeTime.get() / 10) + "ns");
运行后的性能对比:
ReentrantLockDemo 平均读耗时:37990465770ns
ReentrantLockDemo 平均写耗时:501314688ns
ReentrantReadWriteLock 平均读耗时:1001ns
ReentrantReadWriteLock 平均写耗时:353ns
通过测试结果可知在读多写少的场景下,ReentrantReadWriteLock的性能明显大于 ReentrantLock。两者的性能测试代码基本一致,我们着重讲解ReentrantReadWriteLock的代码。代码(1)处使用了CyclicBarrier 任务栅栏,它保证10个写线程和150读线程同时并发进行操作。代码(2)、(3)处 使用CountDownLatch保证 所有线程执行完毕后,再输出读写的耗时。代码(4)处获取写锁保证同一时刻只有一个线程对数据进行写操作。代码(5)处获取读锁,保证在没有写线程进行时,多个读线程能同时读取资源,不会被阻塞挂起。其中将线程睡眠500毫秒的意义在于模拟读数据耗时的场景,有助于进行两者的性能测试对比。
看到这里大家可能会有此疑问:写数据加锁,能保证不会出现写入脏数据的问题,这无可厚非,但读数据的时候我为什么要加读锁?不加读锁不也是能读到最新的数据么?如果你这样想那就大错特错了,它会造成数据不一致性的问题。什么意思呢?假设某线程需要在 [T1,T10] 时间段才能将事情处理完,在T1时间获取 value 值为 2 ,但在T3时刻另一线程对value进行了更新,导致该线程在T10时刻 获取到的value值与 T1 时刻获取的不一样。(类比于数据库中的不可重复读)
哪里用
对于大数据量并发访问缓存的情景下可以采用。
书写技术文章是一个循序渐进的过程,所以我不能保证每句话、每行代码都是对的,但至少能保证不复制、不粘贴,每篇文章都是自己对技术的认识、细心斟酌总结出来的。乔布斯说:我们在这个星球上的时间都很短,很少有机会去做几件真正伟大的事情,同时要做得好,我必须要趁我还年轻的时候完成这些事。
其实我想说的是,我是一枚程序员,我只想在有限的时间内尽可能去沉淀我这一生中所能沉淀下来的东西