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

你可能感兴趣的:(多线程,exception,bean,String,object,null)