版权声明
本文版权归作者所有,如有转载请与作者联系并注明出处http://blog.csdn.net/Iangao/archive/2008/11/08/3256407.aspx
3.2 读写问题
3.2.1 读写问题分析
当某一资源有可能被多个线程同时访问,而访问又可以分为读写两种时,将会遇到读写问题这一讨论。下面我们详细分析一下其中的主要问题。
1) 读写锁
当一个资源执行读操作时,它的值是不会改变的,也就是说,我们可以允许同时一间有多个读线程同时对资源执行读操作,但这段期间是不允许对资源进行修改的。然而写操作会给把资源的数据进行一些修改,这样在修改期间就不允许有其它的读写操作与之并行,所以我们可以得出同一时间只允许有一个线程对资源执行写操作。我们把开始读到读结束这段期间对资源的访问称为读临界区(读锁),把开始写到写结束这段时间对资源的访问称为写临界区(写锁).
通过上面的分析我们可以对读写问题做如下总结:
- 对存在读写问题的资源加以控制需要两个锁协同工作来完成,我们把这样一组锁称为读写锁
- 同一时间,只允许读锁和写锁中的一个锁定对资源的访问。
- 读锁锁定期间,不允许有写操作线程对资源访问,但允许其他读操作线程对资源访问。
- 写操作期间,其他的无论读写操作线程都不允许对资源进行问题
2) 读写优先
一面我们讨论一下读写的执行优先级问题。读锁期间,当系统到来一个读请求时,如果有写操作在等待,是否还读它继续进入临界区呢?如果进入的话,就有可能导致写操作长时间无法得到响应,然而通常我们会更希望写操作更快的得到响应。另一种情况是当写操作结束后,是更优先处理写等待还是更优先处理读等待呢?根据不同的需求我们可能会做出不同的决策。下面我们只针对如下一种常见策略详细分析读写问题的解决:
- 在有写进程执行的情况下,后续读写操作全部阻塞.
- 在有读进程执行的情况下,后续写操作全部阻塞,读操作的阻塞与否取决于是否存在写阻塞(参看下一规则)
- 在有写阻塞进程存在的情况下,我们阻塞后续读进程的并发操作,优先处理写进程。
- 当写操作结束后,优先唤醒写进程,如果没有写阻塞,那么我们唤醒读进程
3.2.2 读写问题的简单实现
1. 简单实现
/** * 简单读写锁 * @author iangao */ public class SimpleReadWriteLock { Mutex resource=new Mutex(); // 资源锁 Condition readers=new Condition(resource); // 读锁 Condition writers=new Condition(resource); // 写锁 int readerCount=0; // 并发读数 boolean writting=false; // 写标识 /** * 开始读, 如果有写等待,就先不处理读了 */ public void readLock() throws InterruptedException{ resource.p(); // [在写||有写等待], 则等待 if(writting||writers.queue()>0) readers.await(); while(writting) readers.await(); // 读计数[加并发] readerCount++; // 唤醒下条并发读[队列中](写后可能会有多条读等待) readers.signal(); resource.v(); } /** * 读结束 */ public void readUnlock() throws InterruptedException{ resource.p(); // 读计数[减并发] readerCount--; // 全(并发)读完, 唤醒写 // a. 有写等待(被读阻塞),有读等待(被写等待阻塞) // b. 有写等待(被读阻塞),无读等待 // c. 无写等待,无读等待 if(readerCount==0) writers.signal(); resource.v(); } /** * 开始写 */ public void writeLock() throws InterruptedException{ resource.p(); // [在写 || 在读] , 则在writers队列等待... while(writting||readerCount!=0) writers.await(); // 开始写 writting=true; resource.v(); } /** * 写结束 */ public void writeUnlock() throws InterruptedException{ resource.p(); // 写结束 writting=false; // 优先唤醒writers队列 // a. 有写等待(被写阻塞), 有读等待(被写阻塞) // b. 有写等待(被写阻塞), 无读等待 // c. 无写等待, 无读等待 if(writers.queue()>0) writers.signal(); else readers.signal(); resource.v(); } } |
3.2.3 读写问题的测试
/** * 简单读写锁测试类 * @author iangao */ public class SimpleReaderWriterLockTest { public static void main(String[] args){ new ThreadsTest(){ /** * 共享资源类 * @author iangao */ class Resource { SimpleReadWriteLock lock=new SimpleReadWriteLock(); private int seq=0; /** * 读资源 */ void read(long mills) throws InterruptedException{ int id=seq++; try{ output("["+id+"]:准备读..."); lock.readLock(); // 随机模拟读一段时间 output("["+id+"]:读", random(0,(int)mills)); }finally{ output("["+id+"]:读完"); lock.readUnlock(); } } /** * 写资源 */ void write(long mills) throws InterruptedException{ int id=seq++; try{ output("["+id+"]:准备写..."); lock.writeLock(); // 随机模拟写一段时间 output("["+id+"]:写",random(500,(int)mills)); }finally{ output("["+id+"]:写完"); lock.writeUnlock(); } } } Resource resource=new Resource(); // 创建共享资源 /** * 读线程操作 */ void reader(long mills) throws InterruptedException { for(int i=0; i<3; i++){ resource.read(mills); sleep(100); } } /** * 写线程操作 */ void writer(long mills) throws InterruptedException { for(int i=0; i<3; i++){ resource.write(mills); sleep(100); } } /** * 启动五组读写线程 */ void runInThread1() throws InterruptedException{ name("读1"); reader(1000); // 1秒内的读 } void runInThread2() throws InterruptedException{ name("读2"); reader(2000); // 2秒内的读 } void runInThread3() throws InterruptedException{ name("读3"); sleep(1000); reader(10); // 0.01秒内的读 } void runInThread4() throws InterruptedException{ name("写1"); writer(1000); // 1秒内的写 } void runInThread5() throws InterruptedException{ name("写2"); writer(2000); // 2秒内的写 } }.execute(5); } } | 执行结果: [读1]: [0]:准备读... [读2]: [1]:准备读... [写1]: [2]:准备写... [写2]: [3]:准备写... [读2]: [1]:读... (0.684秒) [读1]: [0]:读... (0.909秒) [读2]: [1]:读完 [读2]: [4]:准备读... [读1]: [0]:读完 [写1]: [2]:写... (1.316秒) [读3]: [5]:准备读... [读1]: [6]:准备读... [写1]: [2]:写完 [写2]: [3]:写... (0.72秒) [写1]: [7]:准备写... [写2]: [3]:写完 [写1]: [7]:写... (0.851秒) [写2]: [8]:准备写... [写1]: [7]:写完 [写2]: [8]:写... (0.79秒) [写1]: [9]:准备写... [写2]: [8]:写完 [写1]: [9]:写... (1.483秒) [写2]: [10]:准备写... [写1]: [9]:写完 [写2]: [10]:写... (0.931秒) [写2]: [10]:写完 [读2]: [4]:读... (1.974秒) [读3]: [5]:读... (0.0090秒) [读1]: [6]:读... (0.35秒) [读3]: [5]:读完 [读3]: [11]:准备读... [读3]: [11]:读... (0.0060秒) [读3]: [11]:读完 [读3]: [12]:准备读... [读3]: [12]:读... (0.0080秒) [读3]: [12]:读完 [读1]: [6]:读完 [读1]: [13]:准备读... [读1]: [13]:读... (0.023秒) [读1]: [13]:读完 [读2]: [4]:读完 [读2]: [14]:准备读... [读2]: [14]:读... (0.817秒) [读2]: [14]:读完 |
3.2.4 小节
由以上两节的分析我们可以看出,根据不同的读写策略我们可以写出不同的读写锁.我们可以看出上面实现的只是一种最化的读写锁,如果要在读写锁的基础上再做条件变量操作是无法实现的,因为它需要读锁和写锁分别以Lock锁接口的形式程现才行.如果有这类需求的朋友根据以上述的原理可以很容易重构实现出来.在此就不再继续讨论了.
3.2.5 J2SE5.0中读写锁的实现
在J2SE5.0中我们可以调用java.util.concurrent.locks.ReadWriteLock来实现读写操作,其中分别用ReadWriteLock.getReadLock().lock()和ReadWriteLock.getReadLock().unlock()来完成读的加锁和解锁,用
ReadWriteLock.getWriteLock().lock()和ReadWriteLock.getWriteLock().unlock()来完成写的加锁和解锁.