java并发-ReentrantReadWriteLock读写锁

一、概念

Java常见的多是排他锁,这些锁在同一时刻只允许一个线程进行访问,而读写锁在同一时刻可以允许多个读线程访问,但是在写线程访问时,所有的读线程和其他写线程均被阻塞。读写锁维护了一对锁,一个读锁和一个写锁,通过分离读锁和写锁,使得并发性相比一般的排他锁有了很大提升,读写锁能有效提高读比写多的场景下的程序性能,比排它锁好。

Lock比传统线程模型中的synchronized方式更加面向对象,与生活中的锁类似,锁本身也应该是一个对象。两个线程执行的代码片段要实现同步互斥的效果,它们必须用同一个Lock对象。

读写锁:分为读锁和写锁,多个读锁不互斥,读锁与写锁互斥,这是由jvm自己控制的,你只要上好相应的锁即可。如果你的代码只读数据,可以很多人同时读,但不能同时写,那就上读锁;如果你的代码修改数据,只能有一个人在写,且不能同时读取,那就上写锁。总之,读的时候上读锁,写的时候上写锁!如果只是使用Lock,那么读和读的线程之前也会互斥。

二、读写锁特性

ReadWriteLock仅定义了获取读锁和写锁的两个方法,即readLock()和writeLock()方法。

public interface ReadWriteLock {
    /**
     * Returns the lock used for reading.
     *
     * @return the lock used for reading
     */
    Lock readLock();

    /**
     * Returns the lock used for writing.
     *
     * @return the lock used for writing
     */
    Lock writeLock();
}

ReadWriteLock管理一组锁,一个是只读的锁,一个是写锁。Java并发库中ReetrantReadWriteLock实现了ReadWriteLock接口并添加了可重入的特性。它的特性可以总结为三个:

(1)公平性选择

非公平模式(默认)
当以非公平初始化时,读锁和写锁的获取的顺序是不确定的。非公平锁主张竞争获取,可能会延缓一个或多个读或写线程,但是会比公平锁有更高的吞吐量。

公平模式
当以公平模式初始化时,线程将会以队列的顺序获取锁。当当前线程释放锁后,等待时间最长的写锁线程就会被分配写锁;或者有一组读线程组等待时间比写线程长,那么这组读线程组将会被分配读锁。

(2)重入性

什么是可重入锁,不可重入锁呢?"重入"字面意思已经很明显了,就是可以重新进入。可重入锁,就是说一个线程在获取某个锁后,还可以继续获取该锁,即允许一个线程多次获取同一个锁。比如synchronized内置锁就是可重入的,如果A类有2个synchornized方法method1和method2,那么method1调用method2是允许的。显然重入锁给编程带来了极大的方便。假如内置锁不是可重入的,那么导致的问题是:1个类的synchornized方法不能调用本类其他synchornized方法,也不能调用父类中的synchornized方法。与内置锁对应,JDK提供的显示锁ReentrantLock也是可以重入的

(3)锁降级

读写锁支持锁降级,遵循按照获取写锁,获取读锁再释放写锁的次序,写锁能够降级成为读锁,不支持锁升级.

不支持所升级代码:

/**
 *Test Code 1
 **/
package test;

import java.util.concurrent.locks.ReentrantReadWriteLock;

public class Test1 {

    public static void main(String[] args) {
        ReentrantReadWriteLock rtLock = new ReentrantReadWriteLock();
        rtLock.readLock().lock();
        System.out.println("get readLock.");
        rtLock.writeLock().lock();
        System.out.println("blocking");
    }
}

Result:

  java并发-ReentrantReadWriteLock读写锁_第1张图片

结论:上面的测试代码会产生死锁,因为同一个线程中,在没有释放读锁的情况下,就去申请写锁,这属于锁升级,ReentrantReadWriteLock是不支持的

支持锁降级demo:

/**
 *Test Code 2
 **/
package test;

import java.util.concurrent.locks.ReentrantReadWriteLock;

public class Test2 {

    public static void main(String[] args) {
        ReentrantReadWriteLock rtLock = new ReentrantReadWriteLock();  
        rtLock.writeLock().lock();  
        System.out.println("writeLock");  
          
        rtLock.readLock().lock();  
        System.out.println("get read lock");  
    }
}

 Result:

java并发-ReentrantReadWriteLock读写锁_第2张图片


结论:ReentrantReadWriteLock支持锁降级,上面代码不会产生死锁。这段代码虽然不会导致死锁,但没有正确的释放锁。从写锁降级成读锁,并不会自动释放当前线程获取的写锁,仍然需要显示的释放,否则别的线程永远也获取不到写锁。

三、写锁的获取与释放

写锁是一个支持重进入的排它锁。如果当前线程已经获取了写锁,则增加写状态。如果当前线程在获取写锁时,读锁已经被获取(读状态不为0)或者该线程不是已经获取写锁的线程,则当前线程进入等待状态。

如果存在读锁,则写锁不能被获取,原因在于:读写锁要确保写锁的操作对读锁可见,如果允许读锁在已被获取的情况下对写锁的获取,那么正在运行的其他读线程就无法感知到当前写线程的操作。因此只有等待其他读线程都释放了读锁,写锁才能被当前线程所获取,而写锁一旦被获取,则其他读写线程的后续访问均被阻塞。
        写锁的释放比较简单。 每次释放均减少写状态,当写状态为0时表示写锁已被释放,从而等待的读写线程能够继续访问读写锁,同时前次写线程的修改对后续读写线程可见。    
 

四、读锁的获取与释放

读锁不是独占式锁,即同一时刻该锁可以被多个读线程获取也就是一种共享式锁。在没有其他写线程访问(或者写状态为0)时,读锁总会成功的被获取,而所做的也只是(线程安全的)增加读状态。如果当前线程已经获取了读锁,则增加读状态。如果当前线程在获取读锁时,写锁已被其他线程获取,则进入等待状态。

五、读写锁应用

代码示例:创建3个读线程,3个写线程:

   

package cn.itcast.heima;
 
import java.util.Random;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
 
public class ReadWriteLockTest {
	public static void main(String[] args) {
		final Queue3 q3 = new Queue3();
		//同时创建3个读取线程,3个写入线程
		for(int i=0;i<3;i++)
		{
			new Thread(){
				public void run(){
					while(true){
						q3.get();//读取数据						
					}
				}
			}.start();
 
			new Thread(){
				public void run(){
					while(true){
						q3.put(new Random().nextInt(10000));
					}
				}			
 
			}.start();
		}
	}
}
 
class Queue3{
	private Object data = null;//共享数据,只能有一个线程能写该数据,但可以有多个线程同时读该数据。
	ReadWriteLock rwl = new ReentrantReadWriteLock();//创建读写锁对象
	//读的方法
	public void get(){
		rwl.readLock().lock();//读的时候上读锁
		try {
			System.out.println(Thread.currentThread().getName() + " be ready to read data!");
			Thread.sleep((long)(Math.random()*1000));
			System.out.println(Thread.currentThread().getName() + "have read data :" + data);			
		} catch (InterruptedException e) {
			e.printStackTrace();
		}finally{
			rwl.readLock().unlock();
		}
	}
	//写的方法
	public void put(Object data){
		rwl.writeLock().lock();//写的时候上写锁
		try {
			System.out.println(Thread.currentThread().getName() + " be ready to write data!");					
			Thread.sleep((long)(Math.random()*1000));
			this.data = data;		
			System.out.println(Thread.currentThread().getName() + " have write data: " + data);					
		} catch (InterruptedException e) {
			e.printStackTrace();
		}finally{
			rwl.writeLock().unlock();
		}
	}
}

API文档中对ReentrantReadWriteLock示例代码:

/*
 * 示例用法。下面的代码展示了如何利用重入来执行升级缓存后的锁降级(为简单起见,省略了异常处理):
 * 当有许多读的线程访问一个数据实体时,如果有一个线程发现该数据实体为空,该线程就需要将读锁释放,上写锁,写入数据
 * 然后在释放写锁前重新上读锁
 */
class CachedData {
	Object data;
	volatile boolean cacheValid;//判断实体中是否有值
	ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
 
	void processCachedData() {
		rwl.readLock().lock();
		if (!cacheValid) {
			// Must release read lock before acquiring write lock
			rwl.readLock().unlock();
			rwl.writeLock().lock();
			// Recheck state because another thread might have acquired
			//   write lock and changed state before we did.
			if (!cacheValid) {
				data = ...//加载数据
						cacheValid = true;
			}
			// Downgrade by acquiring read lock before releasing write lock
			rwl.readLock().lock();
			rwl.writeLock().unlock(); // Unlock write, still hold read
		}
		use(data);
		rwl.readLock().unlock();
	}
}

 

面试题:请设计一个缓存系统

缓存系统就是可以装很多个对象。要拿对象别直接找数据库,而是访问缓存系统。当缓存系统的数据被访问时,就应该检查内部是否有该数据,如果有,就直接返回,如果没有就查询数据库,然后把数据存入内存中,下次就直接返回该数据不需要再查数据库了。
 

package cn.itcast.heima;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class CacheDemo {
	//定义一个Map,存储多个对象
	private Map cache = new HashMap();
	public static void main(String[] args) {}
	private ReadWriteLock rwl = new ReentrantReadWriteLock();
	//获取对象的方法,当多个线程来读(即获取对象时)不需要互斥,只有写的时候才需要互斥,因此需要使用读写锁
	public  Object getData(String key){
		rwl.readLock().lock();//先上读锁,这样多个线程都可以来读
		Object value = null;
		try{
			value = cache.get(key);
			//如果线程读的时候发现数据为空,那么就把读锁释放,禁止数据的读取,然后上写锁
			if(value == null){
				rwl.readLock().unlock();
				rwl.writeLock().lock();
				try{
					//再次判断数据是否存在,因为当一个线程获取写锁完成了数据填充并释放了写锁,另外一个线程也可能获取了写锁,会重复填充数据。
					if(value==null){
						value = "aaaa";//实际是去queryDB();即查询数据库
					}
				}finally{
					rwl.writeLock().unlock();//数据填充完,释放写锁
				}
				rwl.readLock().lock();//恢复为读锁
			}
		}finally{
			rwl.readLock().unlock();
		}
		return value;
	}
}

总结:

1.Java并发库中ReetrantReadWriteLock实现了ReadWriteLock接口并添加了可重入的特性

2.ReetrantReadWriteLock读写锁的效率明显高于synchronized关键字

3.ReetrantReadWriteLock读写锁的实现中,读锁使用共享模式;写锁使用独占模式,换句话说,读锁可以在没有写锁的时候被多个线程同时持有,写锁是独占的

4.ReetrantReadWriteLock读写锁的实现中,需要注意的,当有读锁时,写锁就不能获得;而当有写锁时,除了获得写锁的这个线程可以获得读锁外,其他线程不能获得读锁

你可能感兴趣的:(java多线程与并发)