ReentrantReadWriteLock读写锁详解
一、读写锁简介
现实中有这样一种场景:对共享资源有读和写的操作,且写操作没有读操作那么频繁。在没有写操作的时候,多个线程同时读一个资源没有任何问题,所以应该允许多个线程同时读取共享资源;但是如果一个线程想去写这些共享资源,就不应该允许其他线程对该资源进行读和写的操作了。
针对这种场景,JAVA的并发包提供了读写锁ReentrantReadWriteLock,它表示两个锁,一个是读操作相关的锁,称为共享锁;一个是写相关的锁,称为排他锁,描述如下:
线程进入读锁的前提条件:
没有其他线程的写锁,
没有写请求或者有写请求,但调用线程和持有锁的线程是同一个
线程进入写锁的前提条件:
没有其他线程的读锁
没有其他线程的写锁
而读写锁有以下三个重要的特性:
二、源码解读
1、内部类
读写锁实现类中有许多内部类,我们先来看下这些类的定义:
public class ReentrantReadWriteLock implements ReadWriteLock, java.io.Serializable
读写锁并没有实现Lock接口,而是实现了ReadWriteLock。并发系列中真正实现Lock接口的并不多,除了前面提到过的重入锁(ReentrantLock),另外就是读写锁中为了实现读锁和写锁的两个内部类:
public static class ReadLock implements Lock, java.io.Serializable
public static class WriteLock implements Lock, java.io.Serializable
另外读写锁也设计成模板方法模式,通过继承队列同步器,提供了公平与非公平锁的特性:
static abstract class Sync extends AbstractQueuedSynchronizer
final static class NonfairSync extends Sync
final static class FairSync extends Sync
2、读写状态的设计
同步状态在前面重入锁的实现中是表示被同一个线程重复获取的次数,即一个整形变量来维护,但是之前的那个表示仅仅表示是否锁定,而不用区分是读锁还是写锁。而读写锁需要在同步状态(一个整形变量)上维护多个读线程和一个写线程的状态。
读写锁对于同步状态的实现是在一个整形变量上通过“按位切割使用”:将变量切割成两部分,高16位表示读,低16位表示写。
假设当前同步状态值为S,get和set的操作如下:
1、获取写状态:
S&0x0000FFFF:将高16位全部抹去
2、获取读状态:
S>>>16:无符号补0,右移16位
3、写状态加1:
S+1
4、读状态加1:
S+(1<<16)即S + 0x00010000
在代码层吗的判断中,如果S不等于0,当写状态(S&0x0000FFFF),而读状态(S>>>16)大于0,则表示该读写锁的读锁已被获取。
3、写锁的获取与释放
protected final boolean tryAcquire(int acquires) { Thread current = Thread.currentThread(); int c = getState(); int w = exclusiveCount(c); if (c != 0) { // (Note: if c != 0 and w == 0 then shared count != 0) if (w == 0 || current != getExclusiveOwnerThread()) return false; if (w + exclusiveCount(acquires) > MAX_COUNT) throw new Error("Maximum lock count exceeded"); } if ((w == 0 && writerShouldBlock(current)) || !compareAndSetState(c, c + acquires)) return false; setExclusiveOwnerThread(current); return true; }
1、c是获取当前锁状态;w是获取写锁的状态。
2、如果锁状态不为零,而写锁的状态为0,则表示读锁状态不为0,所以当前线程不能获取写锁。或者锁状态不为零,而写锁的状态也不为0,但是获取写锁的线程不是当前线程,则当前线程不能获取写锁。
写锁是一个可重入的排它锁,在获取同步状态时,增加了一个读锁是否存在的判断。
写锁的释放与ReentrantLock的释放过程类似,每次释放将写状态减1,直到写状态为0时,才表示该写锁被释放了。
4、读锁的获取与释放
1 protected final int tryAcquireShared(int unused) { 2 Thread current = Thread.currentThread(); 3 int c = getState(); 4 if (exclusiveCount(c) != 0 && 5 getExclusiveOwnerThread() != current) 6 return -1; 7 if (sharedCount(c) == MAX_COUNT) 8 throw new Error("Maximum lock count exceeded"); 9 if (!readerShouldBlock(current) && 10 compareAndSetState(c, c + SHARED_UNIT)) { 11 HoldCounter rh = cachedHoldCounter; 12 if (rh == null || rh.tid != current.getId()) 13 cachedHoldCounter = rh = readHolds.get(); 14 rh.count++; 15 return 1; 16 } 17 return fullTryAcquireShared(current); 18 }
1、读锁是一个支持重进入的共享锁,可以被多个线程同时获取。
2、在没有写状态为0时,读锁总会被成功获取,而所做的也只是增加读状态(线程安全)
3、读状态是所有线程获取读锁次数的总和,而每个线程各自获取读锁的次数只能选择保存在ThreadLocal中,由线程自身维护。
读锁的每次释放均减小状态(线程安全的,可能有多个读线程同时释放锁),减小的值是1<<16。
5、锁降级
锁降级指的是写锁降级为读锁:把持住当前拥有的写锁,再获取到读锁,随后释放先前拥有的写锁的过程。
而锁升级是将读锁变成写锁,但是ReentrantReadWriteLock不支持这种方式。
我们先来看锁升级的程序:
1 ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); 2 rwl.readLock().lock(); 3 System.out.println("get readLock"); 4 rwl.writeLock().lock(); 5 System.out.println("get writeLock");
这种线获取读锁,不释放紧接着获取写锁,会导致死锁!!!
1 ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); 2 rwl.writeLock().lock(); 3 System.out.println("get writeLock"); 4 rwl.readLock().lock(); 5 System.out.println("get readLock");
这个过程跟上面的刚好相反,程序可以正常运行不会出现死锁。但是锁降级并不会自动释放写锁。仍然需要显示的释放。
由于读写锁用于读多写少的场景,天然的使用于实现缓存,下面看一个简易的实现缓存的DEMO:
1 import java.util.HashMap; 2 import java.util.concurrent.locks.ReadWriteLock; 3 import java.util.concurrent.locks.ReentrantReadWriteLock; 4 5 6 public class CachedTest 7 { 8 volatile HashMap<String,String> cacheMap = new HashMap<String,String>(); 9 10 ReadWriteLock rwLock = new ReentrantReadWriteLock(); 11 12 public String getS(String key) 13 { 14 rwLock.readLock().lock(); 15 String value = null; 16 try 17 { 18 if(cacheMap.get(key) == null) 19 { 20 rwLock.readLock().unlock(); 21 rwLock.writeLock().lock(); 22 try 23 { 24 //这里需要再次判断,防止后面阻塞的线程再次放入数据 25 if(cacheMap.get(key) == null) 26 { 27 value = "" + Thread.currentThread().getName(); 28 cacheMap.put(key, value); 29 System.out.println(Thread.currentThread().getName() + "put the value" + value); 30 } 31 } 32 finally 33 { 34 //这里是锁降级,读锁的获取与写锁的释放顺序不能颠倒 35 rwLock.readLock().lock(); 36 rwLock.writeLock().unlock(); 37 } 38 } 39 } 40 finally 41 { 42 rwLock.readLock().unlock(); 43 } 44 return cacheMap.get(key); 45 } 46 }
1、业务逻辑很好理解,一个线程进来先获取读锁,如果map里面没有值,则释放读锁,获取写锁,将该线程的value放入map中。
2、这里有两次value为空的判断,第一次判断很好理解,第二次判断是防止当前线程在获取写锁的时候,其他的线程阻塞在获取写锁的地方。当当前线程将vaule放入map之后,释放写锁。如果这个位置没有value的判断,后续获得写锁的线程以为map仍然为空,会再一次将value值放入map中,覆盖前面的value值,显然这不是我们愿意看见的。
3、在第35行的位置,这里处理的锁降级的逻辑。按照我们正常的逻辑思维,因为是先释放写锁,再获取读锁。那么锁降级为什么要这么处理呢?答案是为了保证数据的可见性,因为如果当前线程不获取读锁而是直接释放写锁,如果该线程在释放写锁与获取读锁这个时间段内,有另外一个线程获取的写锁并修改了数据,那么当前线程无法感知数据的变更。如果按照锁降级的原则来处理,那么当前线程获取到读锁之后,会阻塞其他线程获取写锁,那么数据就不会被其他线程所改动,这样就保证了数据的一致性。