ReadWriteLock也是一个接口,提供了readLock和writeLock两种锁的操作机制,一个资源可以被多个线程同时读,或者被一个线程写,但是不能同时存在读和写线程。
使用场合
假设在程序中定义一个共享的数据结构用作缓存,它大部分时间提供读服务(例如:查询和搜索),而写操作占有的时间很少,但是写操作完成之后的更新需要对后续的读服务可见。
在没有读写锁支持的(Java 5 之前)时候,如果需要完成上述工作就要使用Java的等待通知机制,就是当写操作开始时,所有晚于写操作的读操作均会进入等待状态,只有写操作完成并进行 通知之后,所有等待的读操作才能继续执行(写操作之间依靠synchronized关键字进行同步),这样做的目的是使读操作都能读取到正确的数据,而不会出现脏读。改用读写锁实现上述功能,只需要在读操作时获取读锁,而写操作时获取写锁即可,当写锁被获取到时,后续(非当前写操作线程)的读写操作都会被 阻塞,写锁释放之后,所有操作继续执行,编程方式相对于使用等待通知机制的实现方式而言,变得简单明了。
特性
ReentrantReadWriteLock的实现里面有以下几个特性
**1、公平性:非公平锁(默认)。**读线程之间没有锁操作,所以读操作没有公平性和非公平性。写操作时,由于写操作可能立即获取到锁,所以会推迟一个或多个读操作或者写操作。非公平锁的吞吐量要高于公平锁。(公平锁概念:公平锁利用AQS的CLH队列,释放当前保持的锁时,优先为等待时间最长的那个写操作分配写入锁)
**2、重入性:**读写锁允许读线程和写线程按照请求锁的顺序重新获取读取锁或者写入锁。只有写线程释放了锁,读线程才可以获取重入锁,写线程获取写入锁后可以再次获取读取锁,但是读线程获取读取锁后却不能获取写入锁。
**3、锁降级:**写线程获取写入锁后可以获取读取锁,然后释放写入锁,这样就从写入锁变成了读取锁,从而实现锁降级特性,经典cache案例使用了锁降级
**4、锁升级:**读取锁是不能直接升级为写入锁的。因此获取一个写入锁需要先释放所有的读取锁,如果有两个读取锁试图获取写入锁,且都不释放读取锁时,就会发生死锁
**5、锁获取中断:**读取锁和写入锁都支持获取锁期间被中断
**6、条件变量:**写入锁提供了条件变量的支持,但是读取锁却不允许获取条件变量,否则会得到一个UnsupportedOperationExcetpion异常
**7、重入锁:**读取锁和写入锁的数量最大分别只能是65535
读写锁机制
读-读不互斥
读-写互斥
写-写互斥
示例代码:
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadWriteLockTest {
private double data = 0;
ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
public void get(){
try {
rwl.readLock().lock();
System.out.println("----Thread:"+Thread.currentThread().getName()+"----read first value:"+data);
Thread.sleep(1000);
System.out.println("----Thread:"+Thread.currentThread().getName()+"----read second value:"+data);
rwl.readLock().unlock();
} catch (Exception e) {
// TODO: handle exception
}
}
public void put(){
try {
rwl.writeLock().lock();
data = Math.random();
System.out.println("----Thread:"+Thread.currentThread().getName()+"----write first value:"+data);
Thread.sleep(100);
rwl.writeLock().unlock();
} catch (Exception e) {
// TODO: handle exception
}
}
}
public class MainReadWritLockTest {
/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
final ReadWriteLockTest rwlt = new ReadWriteLockTest();
for(int i=0; i<5;i++){
new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
rwlt.get();
}
}).start();
}
for(int i=0; i<2;i++){
new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
rwlt.put();
}
}).start();
}
}
}
运行代码,我们可以很明显观察到,5个线程读取时,没有互斥。读完后写线程才开始执行,说明读读不互斥,读写有互斥。
运行结果:
锁升级降级示例
读写锁经典案例,防缓存系统
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadWriteLockTest {
ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
Map<String,Object> cache = new HashMap<String,Object>();
public Object get(String key){
Object value = null;
try {
rwl.readLock().lock();
value = cache.get(key);
if(null==value){
rwl.readLock().unlock();//释放都锁,获取写锁
try {
rwl.writeLock().lock();
//获取写锁后再次判断对象是否为null,方式下一个等待的写线程进入后直接获取数据去
value = cache.get(key);
if(null==value){
System.out.println(Thread.currentThread().getName());
value="aaaaa";//实际操作代码从数据库中查询得到的对象
cache.put(key, value);
}
//自身锁降级为都锁
rwl.readLock().lock();
} catch (Exception e) {
// TODO: handle exception
}finally{
rwl.writeLock().unlock();//释放写锁
}
}
} catch (Exception e) {
// TODO: handle exception
}finally{
rwl.readLock().unlock();
}
return value;
}
}
import java.util.HashMap;
import java.util.Map;
public class MainReadWritLockTest {
Map<String,Object> cache = new HashMap<String,Object>();
/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
final ReadWriteLockTest rwlt = new ReadWriteLockTest();
for(int i=0; i<10;i++){
new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
Object value= rwlt.get("id");
System.out.println(Thread.currentThread().getName()+"--"+value);
}
}).start();
}
}
}
上述示例中,cache组合了一个非线程安全的HashMap作为缓存的实现,同时使用读写锁的读锁和写锁来保证cache是线程安全的。在读操作 get(String key)方法中,需要获取读锁,这使得并发访问该方法时不会被阻塞。写操作put(String key, Object value)方法,在更新HashMap时必须提前获取写锁,当写锁被获取后,其他线程对于读锁和写锁的获取均被阻塞,而只有写锁被释放 之后,其他读写操作才能继续。