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 Bean return 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,所以要加个写锁。