读书人

ReadWrite读写锁与保守锁 浅谈

发布时间: 2012-09-20 09:36:50 作者: rapoo

ReadWrite读写锁与传统锁 浅谈

JDK5 之后,不但有了Lock,还有了ReadWriteLock,比之前的Synchronized丰富多了。而这几者有什么关联呢,各自应用的场景是什么呢?

先通过下面的小示例来,比较下传统的synchronized与读写锁readwtirelock的,在处理同一缓存对象池是的小区别:

import java.util.Map;import java.util.TreeMap;import java.util.concurrent.locks.ReentrantReadWriteLock;public class ReadWriteLockDemoForCache {public static void main(String[] args){ReadWriteLockDemoForCache demo = ReadWriteLockDemoForCache.getInstance();Object bean1 = demo.getBeanForBadWay("xxx"); //得到缓存中的对象Object bean2 = demo.getBeanForGoodWay("yyy");}private static final ReadWriteLockDemoForCache INSTANCE = new ReadWriteLockDemoForCache();private ReadWriteLockDemoForCache(){}public static ReadWriteLockDemoForCache getInstance(){return INSTANCE;}private Map<String,Object> cache = new TreeMap<String,Object>();/** * 用传统的方式来,实现缓存对象区中,取数据。 * 优先:与整个方法同步比起来,要优越了好多,第一层判断有数据时,可直接并发的返回数据。 * 缺点:当线程一进入到同步块第二层判断后,线程一睡了几毫秒被剥削执行权几毫秒(或在执行查对象中哪里都可只要是还没有cache.put时)。 * 此时正好一线程执行到 cache.get时,发现obj为空,也进入了第一层判断,在第二层判断外等着。 * 这样的话,会造成 另一些线程不必要的进入同步块。 * @param key * @return */public Object getBeanForBadWay(String key){Object obj = cache.get(key);if(null == obj){synchronized(this){obj = cache.get(key); //需要重新获取是否已经有对象了。if(null == obj){ //需要双重判断,有可能多个线程是要的是多一对象,同时进了一层判断。而二层判断可以有效的避免创建多个对象。Object reVal = queryForDB(key); //伪代码从数据库中查找对象。obj = reVal;cache.put(key, reVal); //将对象缓存起来}}}//doSomeElse user bean //伪代码,可使用use Beanreturn obj;}private ReentrantReadWriteLock rrwl = new ReentrantReadWriteLock();/** *  * 稍微比上面的传统方式来的要优越一点。因为若多线程中,一线程进入需要排它的互斥正在写数据时。 * 其实还是希望最上面的读数据不要进来读,因为可能在没有写入数据前,读出来也是空的,还查进二层判断,等被再次被排它性的互斥,效率会便低点。 * 用了读写锁,就可以很好的解决这个问题,当白排它生的互斥在写数据时,读缓存线程会等着,至到被写入后再去读。 * @param key * @return */public Object getBeanForGoodWay(String key){Object obj = null ;//后面再来一个key,执行第一语时,卡住了要等着。因为线程一正在put进去,之后它才有read权,发现已经有了就不会再进第二层判断了。//这个就是比上面有优越的地方,上面的那种方法此后面的Key是可能在其它线程写时也判断出为空,而进了第二层循环等着,等完获得锁再又去判断了一次。rrwl.readLock().lock(); try{//多线程可以并发的读取缓存中的对象。但是在读取时,不可操作obj为null,查数据库put进去后,再可以读。obj  = cache.get(key); if(null == obj){rrwl.readLock().unlock(); //若对象是空的,则需要将读锁释放掉,进行写锁。rrwl.writeLock().lock(); //这里为什么再加个判断,是因为。若线程A进入了写锁,而之前线程B也早进了第一层判断。//只不过被线程A先进了写锁。A根据需要创建完后,B肯定要再检查次是否有对象了,有了就不创建对象了。obj = cache.get(key);if(null ==obj){Object reVal = queryForDB(key); //伪代码从数据库中查找对象。obj = reVal;cache.put(key, reVal); //将对象缓存起来}rrwl.readLock().lock(); //锁降级,可以在写锁完蛋前加个读锁。称之为读写锁。这里也必须加个读锁才能在双重判断都进入时,有unlcok可执行。rrwl.writeLock().unlock();//严格的话,要try finally,用finally把writeLock.unLock包围起来的。}//doSomeElse user bean}finally{rrwl.readLock().unlock();}return obj ;}private Object queryForDB(String key) {//伪代码从数据库中,查找key对应的 对象。return null;}}

通过上面的分析,可以清晰的明白,在对同一重入缓存的处理方法,读写锁还是比单独的双重synchronized要优越那么一点。主要表现在,读锁可以对读锁代码块中进行并发访问而对写锁是排斥的。 写锁呢是对所有的读锁或写锁排斥的,它是独占式的排斥。这个在某此应用下可谓好处多多啊。

比如下面的应用:

import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;import java.util.concurrent.locks.ReadWriteLock;import java.util.concurrent.locks.ReentrantReadWriteLock;/** * BankCard类,为信用卡类。有卡号和初始的金额 1w元。 * Consumer类,为儿子类,只是去消费信用卡金额。 * Consumer2类,为父母类,只是去查看信用卡金额。 *  * 从生活应用中,应该的需求是当消费信用卡时,尚在消费中未消费完,肯定是不允许查看余额的。 * 而一旦在查看余额时开始,到查看余额结束之前都是不允许再进行消费的。 *  * 查看余额的多个线程,可以并发的查看余额。但消费时,只能独占式的消费,只有一个线程消费完了,另一个线程才能去消费或查看余额。 * 绝对不允许,有消费的同时,进行再次消费或查看余额的。因为这种情况很容易产生数据丢失或查看了一个错误的数据。 * @author chen * */public class ParentReadWriteLock {public static void main(String[] args) {BankCard bc = new BankCard();ReadWriteLock lock = new ReentrantReadWriteLock();ExecutorService pool = Executors.newCachedThreadPool();Consumer cm1 = new Consumer(bc, lock);Consumer2 cm2 = new Consumer2(bc, lock , 1);Consumer2 cm3 = new Consumer2(bc, lock , 2);pool.execute(cm2);pool.execute(cm3);pool.execute(cm1);}}class BankCard {private String cardid = "XZ456789";private int balance = 10000;public String getCardid() {return cardid;}public void setCardid(String cardid) {this.cardid = cardid;}public int getBalance() {return balance;}public void setBalance(int balance) {this.balance = balance;}}/** * @说明 儿子类,只消费 */class Consumer implements Runnable {BankCard bc = null;ReadWriteLock lock = null;Consumer(BankCard bc, ReadWriteLock lock) {this.bc = bc;this.lock = lock;}public void run() {try {while(true){  //也就是当获取到写锁要去写时,读锁则获取不到,不允许执行读锁套起来的代码lock.writeLock().lock(); System.out.print("儿子要消费,现在余额:" + bc.getBalance() + "\t");bc.setBalance(bc.getBalance() - 2000);//Thread.sleep(5 * 1000);  //即使在这里释放了CPU执行权,但下面的读取依旧没有权限。因为加了读锁System.out.println("儿子消费2000元,现在余额:" + bc.getBalance());lock.writeLock().unlock(); Thread.sleep(2 * 1000);}} catch (Exception e) {e.printStackTrace();}}}/** * @说明 父母类,只监督 */class Consumer2 implements Runnable {BankCard bc = null;int type = 0;ReadWriteLock lock = null;Consumer2(BankCard bc, ReadWriteLock lock,int type) {this.bc = bc;this.lock = lock;this.type = type;}public void run() {try {while(true){lock.readLock().lock(); if(type==2){System.out.println("父亲准备要去查余额了呢++");Thread.sleep(5 * 1000);  //当读锁获取到了执行权执行上面语句后即使这里睡了5秒,写锁也不具备执行权,依然要等读锁执行完了。再抢执行权System.out.println("父亲要查询,现在余额:" + bc.getBalance());}else{System.out.println("老妈准备要去查余额了呢----"); //这里可以测试出读锁是可以支持多线程并发操作的//可以执行看出,老爸进来查余额时,老妈也可以进来查余额。Thread.sleep(5 * 1000);System.out.println("老妈要查询,现在余额:" + bc.getBalance());}lock.readLock().unlock();  //若将这里的unlock注释掉,即注释读锁的释放,则以后写锁代码块则永不能执行。                      //因为没有释放读锁,对JVM意味依然还在读,写是不能进入的。同理注释上面的写的unlock,读也不能再执行了。Thread.sleep(1 * 1000);}} catch (Exception e) {e.printStackTrace();}}}


至于 Lock与synchronized的区别及应用场景,请参见上遍http://blog.csdn.net/chenshufei2/article/details/7894992这里不再重复了。

总结一下读写锁:

1..重入方面其内部的WriteLock可以获取ReadLock,但是反过来ReadLock想要获得WriteLock则永远都不要想.

2.WriteLock可以降级为ReadLock,顺序是:先获得WriteLock再获得ReadLock,然后释放WriteLock,这时候线程将保持Readlock的持有.反过来ReadLock想要升级为WriteLock则不可能,为什么?参看1,呵呵.

3.ReadLock可以被多个线程持有并且在作用时排斥任何的WriteLock,而WriteLock则是完全的互斥.这一特性最为重要,因为对于高读取频率而相对较低写入的数据结构,使用此类锁同步机制则可以提高并发量.你可以把ReadLock想成狼,而WriteLock是狮子。狼是群体的,分肉是可以的。狮子一般都是独居的,不给分肉不给同居的。哈哈。但狼群们是排拆狮子的。不让会狮子一起参与分肉呢。

4.不管是ReadLock还是WriteLock都支持Interrupt,语义与ReentrantLock一致.

5.WriteLock支持Condition并且与ReentrantLock语义一致,而ReadLock则不能使用Condition,否则抛出UnsupportedOperationException异常.

读写锁应用的另一个场景可以为一个集合加上读写锁,就假设是Map吧,所有线程可以进来查数据,即get(String key)或 String[] allKeys()。而查数据时,不允许再put 进数据,要加锁,加个读锁。可以并发的查看数据呢。
而 在put(key,value)时,则是排它共享的,put的同时既不允许进行读更也不允许有put,所以要加个写锁。

读书人网 >编程

热点推荐