当线程“读取”实例的状态时,实例的状态不会发生变换,实例的状态尽在执行线程的时候“写入”操作时才会发生变化,是实例的状态来看,起有着本质的区别。在将要说的设计模式中,读取和写入是相互分开的,在进行相应的操作之前必须获取其对应的锁。所以多个线程可以同时读取,但是当在写入的时候不允许读取操作,一般来说,执行互斥处理会降低程序的性能,但是如果吧针对写入的互斥处理和针对读取的互斥处理分开来考虑,则可以提高程序的性能。
结合下面的示例程序来体会一下:
首先设计一下数据类,Data类:
public class Data {
private final char[] buffer;
private final ReadWriteLock readWriteBlock = new ReadWriteLock();
public Data(int size){
this.buffer = new char[size];
for(int i=0;i<buffer.length;i++){
buffer[i]='*';
}
}
public char[] read() throws InterruptedException{
readWriteBlock.readLock();
try{
return doRead();
}finally{
readWriteBlock.readUnlock();
}
}
public void write(char c) throws InterruptedException{
readWriteBlock.writeLock();
try{
doWrite(c);
}finally{
readWriteBlock.writeUnlock();
}
}
private char[] doRead(){
char[] newBuf = new char[buffer.length];
for(int i=0;i<buffer.length;i++){
newBuf[i]=buffer[i];
}
slowly();
return newBuf;
}
private void doWrite(char c){
for(int i=0;i<buffer.length;i++){
buffer[i]=c;
slowly();
}
}
private void slowly(){
try{
Thread.sleep(50);
}catch(InterruptedException e){
e.printStackTrace();
}
}
}按照上面的设计,则需要在read()方法和write()方法中使用before/After模式来进行逻辑设计,同时需要在已进入方法的时候就获取相对应的锁,当方法正常结束或者出现异常而结束的时候必须释放锁,所以设计了ReadLockLock类的readLock和writeLock,并且对于方法中需要的互斥处理则在ReadWriteLock类中实现。ReadWriteLock类设计如下:
/*
* 设置读写锁的操作设置
* 1.首先考虑读与写的互斥操作
* 2.考虑写同写之间的互斥操作
* 3.使用多种模式结合,singletonThreadException
* 和immutable模式,以及GuardSuspension模式
*/
public final class ReadWriteLock {
private int readingReaders = 0;
private int waitingWriters = 0;
private int writingWriters = 0;
private boolean preferWriter = true;
public synchronized void readLock() throws InterruptedException{
while (writingWriters > 0|| (preferWriter && waitingWriters>0)){
wait();
}
readingReaders++;
}
public synchronized void readUnlock(){
readingReaders--;
preferWriter = true;
notifyAll();
}
public synchronized void writeLock() throws InterruptedException{
waitingWriters++;
try{
while(readingReaders > 0|| writingWriters > 0){
wait();
}
}finally{
waitingWriters--;
}
writingWriters++;
}
public synchronized void writeUnlock(){
writingWriters--;
preferWriter = false;
notifyAll();
}
}可以发现,相应的互斥处理都在此类中进行了实现,在Data类中就不再需要使用synchronized来修饰对应的方法了。此类解决了开头所描述的问题,同时为了提高程序的生存性,我们定义了一个字段,preferWriter,来限定表示写入优先。
设计下面的ReadThread类:
public class ReadThread extends Thread {
private final Data data;
public ReadThread(Data data){
this.data = data;
}
public void run(){
try{
while(true){
char[] readBuf = data.read();
System.out.println(Thread.currentThread().getName()+" reads "+String.valueOf(readBuf));
}
}catch(InterruptedException e){
e.printStackTrace();
}
}
}设计WriteThread类:
public class WriterThread extends Thread {
private static final Random random = new Random();
private final Data data;
private final String filler;
private int index = 0;
public WriterThread(Data data,String filler){
this.data = data;
this.filler = filler;
}
public void run(){
try{
while(true){
char c = nextChar();
data.write(c);
Thread.sleep(random.nextInt(3000));
}
}catch(InterruptedException e){
e.printStackTrace();
}
}
private char nextChar(){
char c = filler.charAt(index);
index++;
if(index >= filler.length()){
index = 0;
}
return c;
}
}最后通过一个main函数启动看一下:
public class ReadWriteLockMain {
public static void main(String[] args) {
Data2 data = new Data2(10);
new ReadThread(data).start();
new ReadThread(data).start();
new ReadThread(data).start();
new ReadThread(data).start();
new ReadThread(data).start();
new ReadThread(data).start();
new WriterThread(data,"ABCDEFGHIJKLMNOPQRSTUVWXYZ").start();
new WriterThread(data,"abcdefghijklmnopqrstuvwxyz").start();
}
}执行结果如下:
此模式的要点:
这个模式利用了读取线程之间不会冲突的特性,由于读取不会改变共享资源的状态,所以彼此之间可以不必进行互斥处理,多个reader可以同时进行工作,故提高了程序的性能。但是呢,性能的提高并不是绝对的,它也是仅适用于某些情况下的:
1适合读取任务繁重的操作,如果利用第一节所说的singleThreadExecution模式即便是读取操作也会是每次只能运行一个线程,如果read的内容比较大,很耗费试时间时,使用本节课所讲的模式是一个不错的选择。
2.适合读取比写入频率高的情况,如果写入的频率较小,那么可以很好的体现此模式的优点,从而能够让多个读取的线程同时读取,如果写入的频率较高的化,读取的线程则都处于等待状态,此模式的优势也就无法体现了。
在这里还要讨论一下锁的含义:
Java中的每个实例都持有一个锁,但是一个锁不可以由两个以上的线程同时获取,这种结构是java编程规范决定的,java的虚拟机也是这么实现的,这是java从一开始就提供的所谓的“物理锁”,程序并不能改变这种锁的运行。
而示例中提到的“用于读取的锁”和“用于写入的锁”所指的锁与使用synchronized获取的锁是不一样的,这是开发人员自己实现的一种结构,并不是java编程规范所要求的,这就是所谓的“逻辑锁”。开发人员可以修改改变锁的运行。
借助java.util.concurrent.locks包重建Data类
public class Data {
private final char[] buffer;
private final ReadWriteLock lock = new ReentrantReadWriteLock(true);
private final Lock readLock = lock.readLock();
private final Lock writeLock = lock.writeLock();
public Data(int size){
this.buffer = new char[size];
for(int i=0;i<buffer.length;i++){
buffer[i]='*';
}
}
public char[] read() throws InterruptedException{
readLock.lock();
try{
return doRead();
}finally{
readLock.unlock();
}
}
public void write(char c) throws InterruptedException{
writeLock.lock();
try{
doWrite(c);
}finally{
writeLock.unlock();
}
}
private char[] doRead(){
char[] newBuf = new char[buffer.length];
for(int i=0;i<buffer.length;i++){
newBuf[i]=buffer[i];
}
slowly();
return newBuf;
}
private void doWrite(char c){
for(int i=0;i<buffer.length;i++){
buffer[i]=c;
slowly();
}
}
private void slowly(){
try{
Thread.sleep(50);
}catch(InterruptedException e){
e.printStackTrace();
}
}
} 运行结果同原结果一致,但是代码的书写量少了很多,而且不必再建ReadWriteLock类,直接使用java.util.concurrent.locks.ReadWriteLock类即可。同时,值得注意的是,在实现的时候必须考虑共享资源的安全性,充分的考虑互斥性,不要忘记释放锁获取的实例锁。
本文介绍了一种利用读写锁提高程序并发性能的设计模式。该模式允许多个读取线程同时访问共享资源,但写入操作时禁止读取。通过自定义逻辑锁或使用Java并发库,有效提升了读密集型应用的效率。
9708

被折叠的 条评论
为什么被折叠?



