理解:
ReadWriteLock 维护了一对相关的锁,一个用于只读操作,另一个用于写入操作。只要没有 writer,读取锁可以由多个 reader 线程同时保持。写入锁是独占的。
所有 ReadWriteLock 实现都必须保证 writeLock 操作的内存同步效果也要保持与相关 readLock 的联系。也就是说,成功获取读锁的线程会看到写入锁之前版本所做的所有更新。
与互斥锁相比,读-写锁允许对共享数据进行更高级别的并发访问。虽然一次只有一个线程(writer 线程)可以修改共享数据,但在许多情况下,任何数量的线程可以同时读取共享数据(reader 线程),读-写锁利用了这一点。从理论上讲,与互斥锁相比,使用读-写锁所允许的并发性增强将带来更大的性能提高。在实践中,只有在多处理器上并且只在访问模式适用于共享数据时,才能完全实现并发性增强。
与互斥锁相比,使用读-写锁能否提升性能则取决于读写操作期间读取数据相对于修改数据的频率,以及数据的争用——即在同一时间试图对该数据执行读取或写入操作的线程数。例如,某个最初用数据填充并且之后不经常对其进行修改的 collection,因为经常对其进行搜索(比如搜索某种目录),所以这样的 collection 是使用读-写锁的理想候选者。但是,如果数据更新变得频繁,数据在大部分时间都被独占锁,这时,就算存在并发性增强,也是微不足道的。更进一步地说,如果读取操作所用时间太短,则读-写锁实现(它本身就比互斥锁复杂)的开销将成为主要的执行成本,在许多读-写锁实现仍然通过一小段代码将所有线程序列化时更是如此。最终,只有通过分析和测量,才能确定应用程序是否适合使用读-写锁。
尽管读-写锁的基本操作是直截了当的,但实现仍然必须作出许多决策,这些决策可能会影响给定应用程序中读-写锁的效果。这些策略的例子包括:
- 在 writer 释放写入锁时,reader 和 writer 都处于等待状态,在这时要确定是授予读取锁还是授予写入锁。Writer 优先比较普遍,因为预期写入所需的时间较短并且不那么频繁。Reader 优先不太普遍,因为如果 reader 正如预期的那样频繁和持久,那么它将导致对于写入操作来说较长的时延。公平或者“按次序”实现也是有可能的。
- 在 reader 处于活动状态而 writer 处于等待状态时,确定是否向请求读取锁的 reader 授予读取锁。Reader 优先会无限期地延迟 writer,而 writer 优先会减少可能的并发。
- 确定是否重新进入锁:可以使用带有写入锁的线程重新获取它吗?可以在保持写入锁的同时获取读取锁吗?可以重新进入写入锁本身吗?
- 可以将写入锁在不允许其他 writer 干涉的情况下降级为读取锁吗?可以优先于其他等待的 reader 或 writer 将读取锁升级为写入锁吗?
当评估给定实现是否适合您的应用程序时,应该考虑所有这些情况。
下面来看一个栗子:
public class ThreadReadAndWriteLockDemo {
public static void main(String[] args) {
final ReadWriteOutput rw = new ReadWriteOutput();
for(int i=0;i<3;i++){
//读
new Thread(new Runnable() {
@Override
public void run() {
rw.readAction();
}
}).start();
//写
new Thread(new Runnable() {
@Override
public void run() {
String data = (Math.random()*1000)+"";
rw.writeAction(data);
}
}).start();
}
}
}
class ReadWriteOutput{
private Object data;//共享数据,只能一个线程写,可多个线程读
ReadWriteLock wrl = new ReentrantReadWriteLock();
//读操作
public void readAction(){
/*wrl.readLock().lock();*/
System.out.println(Thread.currentThread().getName()+",I ready read data:"+data);
try {
Thread.sleep((long) (Math.random()*1000));
System.out.println(Thread.currentThread().getName()+",I finished reading data:"+this.data);
} catch (InterruptedException e) {
e.printStackTrace();
}finally{
/*wrl.readLock().unlock();*/
}
}
//写操作
public void writeAction(String obj){
/*wrl.writeLock().lock();*/
this.data = obj;
System.out.println(Thread.currentThread().getName()+",I ready write data:"+obj);
try {
Thread.sleep((long)Math.random()*1000);
System.out.println(Thread.currentThread().getName()+",I finished writing data:"+this.data);
} catch (InterruptedException e) {
e.printStackTrace();
} finally{
/*wrl.writeLock().unlock();*/
}
}
}
运行结果如下:
从上图看到,从第4行开始看,Thread-5还没有写完,Thread-1有插队进来写了,(如果这两个用户在写时,刚好查询了数据,写完然后改变有关于这个的数据,比如转账:用户A查到有:500,这时他要转账给B:100,这时c刚好要转给A:200,最后A的钱是:700,正确来说A经过这一操作后的余额应该是:600才对)这分明就是不正确的,会造成数据脏读。。
程序改善如下:
public class ThreadReadAndWriteLockDemo {
public static void main(String[] args) {
final ReadWriteOutput rw = new ReadWriteOutput();
for(int i=0;i<3;i++){
//读
new Thread(new Runnable() {
@Override
public void run() {
rw.readAction();
}
}).start();
//写
new Thread(new Runnable() {
@Override
public void run() {
String data = (Math.random()*1000)+"";
rw.writeAction(data);
}
}).start();
}
}
}
class ReadWriteOutput{
private Object data;//共享数据,只能一个线程写,可多个线程读
ReadWriteLock wrl = new ReentrantReadWriteLock();
//读操作
public void readAction(){
wrl.readLock().lock();
System.out.println(Thread.currentThread().getName()+",I ready read data:"+data);
try {
Thread.sleep((long) (Math.random()*1000));
System.out.println(Thread.currentThread().getName()+",I finished reading data:"+this.data);
} catch (InterruptedException e) {
e.printStackTrace();
}finally{
wrl.readLock().unlock();
}
}
//写操作
public void writeAction(String obj){
wrl.writeLock().lock();
this.data = obj;
System.out.println(Thread.currentThread().getName()+",I ready write data:"+obj);
try {
Thread.sleep((long)Math.random()*1000);
System.out.println(Thread.currentThread().getName()+",I finished writing data:"+this.data);
} catch (InterruptedException e) {
e.printStackTrace();
} finally{
wrl.writeLock().unlock();
}
}
}
这个运行结果是正确的,当进行写操作的时候,写完后才进行另一个操作。。。
什么时候适合用读写锁:
适用于多度少写的逻辑过程,经常对其进行查询的操作,如果用在这种逻辑处理,读写锁绝对是不二之选,有着很高的并发性能。 最终,只有通过分析和测量,才能确定应用程序是否适合使用读-写锁。
注意:有关于unlock()的操作必须要放在finally块语句里面。