同时也要更新数据到缓存中,因此必然用到读写锁。下面我们先看个例子。
public class Cache {
private final ReentrantReadWriteLock rwlock = new ReentrantReadWriteLock();
Map cache = new HashMap(); //缓存的容器,来存储数据
private int num = 1024;
public Cache(){
cache.put(1,(int)Math.random()*10+"");
cache.put(2,(int)Math.random()*10+"");
cache.put(3,(int)Math.random()*10+"");
}
public void put(Integer key){
int value = (int) (Math.random()*1000);
cache.put(key,value+"");
}
public Object get(Integer key){
ReadLock rl = rwlock.readLock(); //读锁
WriteLock wl = rwlock.writeLock(); //写锁
Object obj = null;
try {
rl.lock();//获取读锁
obj = cache.get(key);
if(obj==null){
//缓存不存在
rl.unlock();
try{
wl.lock(); //相当于锁升级,从读锁升级到写锁
this.put(key); //如果数据不在缓存中,添加到缓存,当然这里应该有个策略,定时清理缓存,否则所有的数据都到缓存中去了
}catch(Exception e){
e.printStackTrace();
}finally{
obj = cache.get(key);
wl.unlock();//
rl.lock();//说明读锁已经释放,需要再次获取锁 锁降级,从写锁降级到读锁
}
}
} catch (Exception e) {
e.printStackTrace();
}finally{
rl.unlock();
}
return cache.get(key);
}
下面这个是实现的一个线程,为了能够得到返回结果,这里实现了Callable接口,
public class Task implements Callable {
private Cache cache;
public Task(Cache cache) {
this.cache = cache;
}
@Override
public String call() throws Exception {
Integer key = (int) (Math.random()*100);
String value =null;
while(key%3!=0){
key = (int) (Math.random()*10);
value = (String) cache.get(key);
System.out.println("id="+Thread.currentThread().getId()+",name="+Thread.currentThread().getName()+",key="+key+",value="+value);
}
if(value==null){
value = (String) cache.get(key);
}
return value;
}
}
@Test
public void testRWLock() throws InterruptedException, ExecutionException{
ExecutorService service = Executors.newFixedThreadPool(5);
List> futures = new ArrayList>();
Cache cache = new Cache();
for(int i=0;i<3;i++){
Future future = service.submit(new Task(cache));
futures.add(future);
}
System.out.println("pring futures...");
for(int i=0;i f = futures.get(i);
System.out.println("return:"+f.get()+",is done ="+f.isDone());
}
System.err.println("shutdown ... ");
}
这里用的启动了3个线程,每个线程最多执行的次数是3次,我们看下输出结果:
thread-id:9,key=78,value=835 //线程9只执行了一次,直接输出值
thread-id:10,key=5,value=899
id=10,name=pool-1-thread-2,key=5,value=899
thread-id:10,key=4,value=101
id=10,name=pool-1-thread-2,key=4,value=101
thread-id:10,key=4,value=101
id=10,name=pool-1-thread-2,key=4,value=101
thread-id:10,key=4,value=101
id=10,name=pool-1-thread-2,key=4,value=101
thread-id:10,key=7,value=222
id=10,name=pool-1-thread-2,key=7,value=222
thread-id:10,key=6,value=519
id=10,name=pool-1-thread-2,key=6,value=519 //线程10,执行了6次
thread-id:11,key=66,value=901 //线程11只执行了一次,输出对应的值
pring futures...
return:835,is done =true
return:519,is done =true
return:901,is done =true
shutdown ...
经过上面的之后,我们的cache缓存从之前的1,2,3后又添加了5,4,7,6等key,这就是读写所最常见的适用场景了。那么读写所到底是怎么实现的呢,接下来学习一下读写锁的源码。
这个类中共有5个内部类,其中包含了读锁和写锁类,然后有锁又分为公平锁和非公平锁,实现了不同的策略,具体的如下图。
和上一篇的基本差不多,sync继承了AQS类,在ReentrantLock中,该类是通过一个int类型保存锁的数量和状态,并进行修改。但是由于这里包含了读锁和写锁,那么这种保存方法明显不合适,这里采用了一个非常巧妙的方法,用高位和地位来分别保存读锁和写锁的数量,那么读锁和写锁占用的位数分别是16位,因此锁的最多数量分别是65535。下面这个几个方法和运算规则。
static final int SHARED_SHIFT = 16;
static final int SHARED_UNIT = (1 << SHARED_SHIFT);
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
/** Returns the number of shared holds represented in count */
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
/** Returns the number of exclusive holds represented in count */
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
从上面先看下shard_unit这个变量得到的左移16位得到值1对应的二进制就是
0000 0000 0000 0000 0000 0000 0000 0001 左移16位
0000 0000 0000 0001 0000 0000 0000 0000 这个shared_unit值
看下max_count (1< 0000 0000 0000 0000 1111 1111 1111 1111 ---> 十进制 ==2^16-1 =65535 这是最大锁的次数 exclusive_mask (1< 0000 0000 0000 0000 1111 1111 1111 1111 ---> 十进制 ==2^16-1 =65535 同样得到独占所的数量 在看看下面的这两个方法sharedCount() 共享的数量 c>>> shared_shift 假设线程锁的状态1,有一个线程持有共享锁 0000 0000 0000 0011 0000 0000 0000 000 >>>16 ==0000 0000 0000 0000 0000 0000 0000 011 得到的锁的数量是3个,共享锁的数量,在看看独占所 0000 0000 0000 0000 0000 0000 0000 001 & 0000 0000 0000 0000 1111 1111 1111 1111 经过&运算之后,高位全部置0,只保留低位,这就计算出来了独占锁的数量。上面的逻辑就实现了高位保存共享锁,低位保存独占锁,一个非常巧妙的方法。 与ReentrantLock不同的是,这里加了一个静态类HoldCounter,用来保存每个线程持有读锁的数量,而ThreadLocalHoldCounter则采用了继承的方式,这样可以重写iniitalvalue方法。用来报错当前线程持有的可重入读锁的数量。接下来继续看下读写锁具体的执行流程。 上面的步骤总结一下: 1. 判断是否有写锁,有,直接失败 2. 没有写锁,获取共享锁的数量,并且更新状态,如果是第一个获取,则设置成firstreader,如果不是,并且还是自己,则重入 次数加1操作,如果而已不是自己,则当前持有者改成自己,并更新状态 3.如果失败,则进入重试方法,如下 尝试获取共享锁失败,这里的逻辑总结一下 1. 如果当前已经存在线程占用了写锁,并且不是当前线程,直接返回失败 2. 如果公平策略中要求读线程阻塞,那只有一种情况会继续获取共享锁,就是重入操作。同样也分为两种情况,当前线程是第一个获取共享锁的线程,直接进入获取锁的操作,如果不是第一个,则需要判断的holdcounter的次数,只要不为0,则说明这个是一次重入操作,则尝试获取,rh.count==0意味着这是个新的线程在尝试获取共享锁,由于需要保证公平性,则尝试获取失败 3. 如果公平测了不要阻塞线程,则直接获取锁。 然后如果获取失败的话,需要进入等待对列中,执行方法doAcquireShared()这个方法,这个和之前的逻辑大致上是一致的这里就不重复去写了。 释放锁操作 上面是获取共享锁的过程,接下来看下释放锁的操作,具体的就是调用unlock方法,进入同样的逻辑。 这里调用的也是Sync下面的tryAcquire方法 再回到上个方法,如果获取写锁失败的话,需要添加到等待队列中,执行addWaiter()方法把,当前线程变成node节点 然在执行acquireQueue方法,添加到队列中,同时进行线程park,这个和前面讲到的ReentrantLock方法逻辑基本是上一样的,这里不再重复讲解,请参考前面的方法。 这里总结下写锁的获取逻辑,相对比较简单: 1. 首先是获取当前锁的状态,先获取锁的状态c,在获取独占锁的数量w,判断当前是否存在读锁 2. 如果c!=0,说明肯定有锁,在用w来确定是读锁还是写锁,如果w==0,则都是读锁,如果w!=0,则是有写锁存在,如果是由其他线程持有写锁,则当前线程也不能成功。 3. 如果c==0,则说明不存在锁,如果是公平策略,则进入同步队列,如果是非公平的,则会尝试去获取锁 4. 如果获取锁失败,则执行addWaiter方法,生成等待节点,并且投放到等待队列中。 下面看看怎么去释放写锁 static final class HoldCounter {
int count = 0;
// Use id, not reference, to avoid garbage retention
final long tid = Thread.currentThread().getId();
}
static final class ThreadLocalHoldCounter
extends ThreadLocal
@Test
public void testRWLockV3(){
ReentrantReadWriteLock rwlock = new ReentrantReadWriteLock();
try {
rwlock.readLock().lock();
} catch (Exception e) {
e.printStackTrace();
}finally {
rwlock.readLock().unlock();
}
}
public void lock() {
sync.acquireShared(1);
}
以上的流程都非常简单,直接获取到读锁,然后调用lock方法进行获取锁,这里面可以看到先去调用tryAcquireShared()方法来获取读锁,以共享模式去获取锁,至少先去获取一次,如果有写锁,直接返回失败。下面看下代码里面的注释
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread(); //获取当前线程
int c = getState(); //获取当前的锁状态
if (exclusiveCount(c) != 0 && //持有写锁,且不是的当前线程,直接返回失败
getExclusiveOwnerThread() != current)
return -1; //证明同一个时刻,只能有一个线程获取写锁,进行写操作
int r = sharedCount(c); //获取读锁的数量
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) { //说明可以获取读锁,修改高16位的值
if (r == 0) { //当前线程第一个占有读锁
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) { //否则重入次数加入,说明已经第二次获取到读锁了
firstReaderHoldCount++;
} else {
HoldCounter rh = cachedHoldCounter; //重新更新重入锁,并且设置成当前线程
if (rh == null || rh.tid != getThreadId(current))
cachedHoldCounter = rh = readHolds.get(); //如果缓存中的变量不是自己,则从readHOlds中重新获取
else if (rh.count == 0) //这里使用cachedHoldCOunter变量的作用是尽量减少map的查找
readHolds.set(rh);
rh.count++;
}
return 1;
}
return fullTryAcquireShared(current); //获取重入锁失败进行重试
}
final int fullTryAcquireShared(Thread current) {
HoldCounter rh = null;
for (;;) {
int c = getState();
if (exclusiveCount(c) != 0) {
if (getExclusiveOwnerThread() != current)
return -1; //这个逻辑还是一样的,判断是否有写锁,如果有,则直接返回失败
// else we hold the exclusive lock; blocking here
// would cause deadlock.
} else if (readerShouldBlock()) {
if (firstReader == current) {
// assert firstReaderHoldCount > 0;
} else {
if (rh == null) {
rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current)) {
rh = readHolds.get();
if (rh.count == 0)
readHolds.remove();
}
}
if (rh.count == 0)
return -1;
}
}
if (sharedCount(c) == MAX_COUNT) //读锁的数量达到最大了,
throw new Error("Maximum lock count exceeded");
if (compareAndSetState(c, c + SHARED_UNIT)) { //更新高位的读锁的数量
if (sharedCount(c) == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
if (rh == null)
rh = cachedHoldCounter; //这里的逻辑和上面的一样
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
cachedHoldCounter = rh; // cache for release
}
return 1;
}
}
}
这里比较核心的地方就是tryReleaseShared()这个方法,下面看下这个方法的源码
public void unlock() {
sync.releaseShared(1);
}
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
这里是释放锁的逻辑,相对比较简单,把高位状态减掉1,同时把当前线程持有锁的计数减一操作,在释放的过程中,有可能其他线程也在释放锁,所以修改状态是有可能失败的,这里放到一个无线循环里面,保证一定要修改成功。好了上面是读锁的获取过程,相对比较简单。下面看下写锁的获取过程和释放过程,同样是先获取WriteLock对象,然后调用lock方法获取写锁。
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
if (firstReader == current) { //当前线程是第一个获取到读锁的,释放的时候直接把firstReader置为null即可
// assert firstReaderHoldCount > 0;
if (firstReaderHoldCount == 1)
firstReader = null;
else
firstReaderHoldCount--; //否则则进行减一操作
} else {
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current)) //如果cache为null,或者不等于当前线程,直接重新获取
rh = readHolds.get();
int count = rh.count;
if (count <= 1) { //进行减一操作,并且删除
readHolds.remove();
if (count <= 0) //如果没有持有读锁,就进行释放,直接抛出异常,就是说获取锁和释放所必须是成对出现的
throw unmatchedUnlockException();
}
--rh.count;
}
for (;;) {
int c = getState();
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc)) //高16位进行减一操作
// Releasing the read lock has no effect on readers,
// but it may allow waiting writers to proceed if
// both read and write locks are now free.
return nextc == 0; //使用多次循环,可能有多个读锁同时在进行释放操作,循环保证释放成功。
}
}
@Test
public void testRWLockV3(){
ReentrantReadWriteLock rwlock = new ReentrantReadWriteLock();
try {
rwlock.writeLock().lock();
} catch (Exception e) {
e.printStackTrace();
}finally {
rwlock.writeLock().unlock();
}
}
public void lock() {
sync.acquire(1);
}
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState(); //获取当前锁的状态
int w = exclusiveCount(c); //获取独占锁定数量,就是低16位的值
if (c != 0) { //说明有当前有锁存在
// (Note: if c != 0 and w == 0 then shared count != 0)
if (w == 0 || current != getExclusiveOwnerThread()) //说明有读锁存在,
return false; //如果c!=0 && w!=0 则说明当前有线程持有写锁
if (w + exclusiveCount(acquires) > MAX_COUNT) //超过持有写锁的最大数量
throw new Error("Maximum lock count exceeded");
// Reentrant acquire
setState(c + acquires); //获取写锁,直接返回
return true;
}
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires)) //说明不存在任何锁,进行状态修改,如果修改失败,直接返回
return false;
setExclusiveOwnerThread(current); //把获取写锁的线程设置成当前线程
return true;
}
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
这里可以看到,也是去调用tryRelease方法释放锁,释放成功之后,进行唤醒后继线程
public void unlock() {
sync.release(1);
}
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
写锁释放和之前的独占锁差不多,不同的就是把低16位状态减一操作,如果为0,说明读锁可用,如果不为0,说明当前线程任然持有写锁。总的来说,写锁的获取和释放的过程都比较简单。
protected final boolean tryRelease(int releases) {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
int nextc = getState() - releases; //状态进行减一操作
boolean free = exclusiveCount(nextc) == 0;
if (free)
setExclusiveOwnerThread(null);
setState(nextc);
return free;
}