前言
在前面的一篇文章里,我曾经讨论了volatile修饰变量的使用。当时,经过各种分析和比较,发现在如果只有一个单线程写但是有多个线程读数据的情况下,volatile变量是一个适用的选项。在这部分,我们可以探讨另外一个选项。那就是ReentrantReadWriteLock。
应用场景
记得以前在一些社区讨论的时候,就看到有人提出过这么一些让人觉得比较纠结的场景。比如说有两个线程,他们之间共享一块数据,在一个线程写数据和一个线程读数据的时候,怎么样保证数据和逻辑的正确性。是否需要同步和加锁呢?还是完全没有必要?实际上,在前面关于volatile的介绍里已经基本上解决了。对于有多个读取数据的线程和单个写数据线程的场景。在我们看来这是一个比较理想的情况,因为对于读操作来说它本身不会带来任何的副作用,我们希望所有的这种操作能够并行。而对于写操作来说,一旦它发生作用,那么其他读数据的线程必须和它互斥,这样才能保证程序的正确性。
在这种情况下,ReentrantReadWriteLock就算是一个比较理想的选择。它本身就定义了两个锁,一个读锁,一个写锁。在所有读数据的线程来说,他们都通过获取读锁来获得数据。这个锁对于读线程来说都是并行的,他们不会互斥。而对于写锁来说,一次只有一个线程能够获得。当它获得的时候,会和其他线程互斥。这样,我们在使用他们的时候,在需要读取数据的地方加上读锁,在需要写数据的地方加上写锁就能够满足基本的要求了。
下面是一个使用ReentrantReadWriteLock的示例:
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class PricesInfo {
private double price1;
private double price2;
private ReadWriteLock lock;
public PricesInfo() {
price1 = 1.0;
price2 = 2.0;
lock = new ReentrantReadWriteLock();
}
public double getPrice1() {
lock.readLock().lock();
double value = price1;
lock.readLock().unlock();
return value;
}
public double getPrice2() {
lock.readLock().lock();
double value = price2;
lock.readLock().unlock();
return value;
}
public void setPrices(double price1, double price2) {
lock.writeLock().lock();
this.price1 = price1;
this.price2 = price2;
lock.writeLock().unlock();
}
}
这部分是定义我们需要操作的数据对象,在getPrice的两个方法中,都添加了读锁。在setPrices方法里增加了写锁。
接着,我们再定义读数据的线程Reader和写数据的线程Writer:
public class Reader implements Runnable {
private PricesInfo pricesInfo;
public Reader(PricesInfo pricesInfo) {
this.pricesInfo = pricesInfo;
}
@Override
public void run() {
for(int i = 0; i < 10; i++) {
System.out.printf("%s: Price 1: %f\n",
Thread.currentThread().getName(), pricesInfo.getPrice1());
System.out.printf("%s: Price 2: %f\n",
Thread.currentThread().getName(), pricesInfo.getPrice2());
}
}
}
public class Writer implements Runnable {
private PricesInfo pricesInfo;
public Writer(PricesInfo pricesInfo) {
this.pricesInfo = pricesInfo;
}
@Override
public void run() {
for(int i = 0; i < 3; i++) {
System.out.printf("Writer: Attempt to modify the prices.\n");
pricesInfo.setPrices(Math.random() * 10, Math.random() * 8);
System.out.printf("Writer: Prices have been modified.\n");
try {
Thread.sleep(2);
} catch(InterruptedException e) {
e.printStackTrace();
}
}
}
}
在测试程序里,我们定义了5个读线程和一个写线程,通过运行他们我们可以来查看运行的结果:
/**
* Main class of the Example. Create and start two initialization tasks
* and wait for their finish
*
*/
public class Main {
/**
* Main method of the class. Create and star two initialization tasks
* and wait for their finish
* @param args
*/
public static void main(String[] args) {
PricesInfo pricesInfo = new PricesInfo();
Reader[] readers = new Reader[5];
Thread[] threadsReader = new Thread[5];
for(int i = 0; i < 5; i++) {
readers[i] = new Reader(pricesInfo);
threadsReader[i] = new Thread(readers[i]);
}
Writer writer = new Writer(pricesInfo);
Thread threadWriter = new Thread(writer);
for(int i = 0; i < 5; i++) {
threadsReader[i].start();
}
threadWriter.start();
}
}
如果我们去分析程序运行的结果,会发现只要是在执行写操作结束后,所有读线程获得的数据都会是一致的。这样就保证了正确性。
和volatile的比较
使用读写锁的方式在单个写线程加多个读线程的情况下,其实差别不大。在使用volatile变量的时候,因为每次操作修改的结果对于全局都是可见的。那么在只有一个写线程的情况下,只有这个线程可以唯一修改数据。不会存在有几个写线程而产生的竞争条件。对于有多个写线程的情况下,volatile变量就不能保证数据的一致性了。而ReentrantReadWriteLock却可以有锁的机制保证互斥。它同时也尽可能保证了足够大的并行性。
和synchronized的比较
synchronized的修饰一般限制这个区域是不可重入的。每次只有一个线程可以访问。这种强烈的互斥性使得每次不管是读数据还是写数据都只能有一个线程可以操作。在希望有多个读线程可以并行执行的情况下,它并不是一个理想的选择。
总结
ReentrantReadWriteLock是一个解决单线程写和多线程读的理想方法。它采用类似于读写分离的思路设定了读锁和写锁。对于这两个锁的访问保证尽可能大的读并行和写互斥。另外,在一定的条件下写锁可以转换成读锁,而读锁却不能转换成写锁。
参考资料
http://stackoverflow.com/questions/6637170/reentrantreadwritelock-vs-synchronized
http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/locks/ReentrantReadWriteLock.html