死磕JUC之读写锁

死磕JUC之读写锁(一)

读写锁是什么?为什么要有读写锁?

java5引入了一系列并发包,其中也包括读写锁,顾名思义,这个工具就是为了应对读与写这两个不同情况而生产的。
很容易想到读的情况是共享的,大家一起读,不会影响各自。而写的时候则必须是独占的,否则就会互相影响。
之所以需要这个工具则是因为在大多数情况下读的情况偏多,支持读锁共享并重入可以提升不少效率。

读写获取锁的情况

情况一:只有读锁,这种情况只要所有线程重入次数和<=65535就没事
情况二:只有写锁,这种情况只能有一个线程获取到写锁,并且这个线程的重入次数得<=65535
情况三:线程获取到读锁,此时不支持获取写锁,只有等到释放读锁才有机会获取写锁,有一种情况,A获取读锁,不释放,A再来获取写锁是不会获取成功的,这种情况我不知道为什么不支持?
情况四:线程获取到写锁,此时只支持本线程获取读锁。

读写状态的设计

读写锁的设计依赖于AQS同步器,AQS解析
AQS有一个至关重要的变量state,在ReentrantLock里面state表示线程的重入次数。
而在读写锁里面state需要维持多个读线程和一个写线程,一个int变量想要表示多种状态,就得按位切割,高16位表示读状态,低16位表示写状态,这也就是为什么上面那个重入次数最大是65535的原因

static final int SHARED_SHIFT   = 16;
static final int SHARED_UNIT    = (1 << SHARED_SHIFT);//读锁的单位,写锁加一就是加一,而读锁加一时则是加SHARED_UNIT
static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;//65535
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;//写锁掩码00..0011..11(16个016个1,目的用&是把高16位消掉)

/** 获取所有线程获取读锁的次数,包括重入  */
static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }
/** 获取写锁的次数  */
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }

公平模式

读写锁的公平与不公平体现在writerShouldBlock和readerShouldBlock上
这是公平模式这两个方法的体现

		final boolean writerShouldBlock() {
            return hasQueuedPredecessors();
        }
        final boolean readerShouldBlock() {
            return hasQueuedPredecessors();
        }

hasQueuedPredecessors是说该节点有没有前驱节点。(头节点是空信息节点,不算前驱)
如果有前驱,此时获取读写锁应该入队列等待,等到前驱拿到锁并通知他,这就是公平的体现,排队。

非公平模式

		//不公平模式下,获取写锁总是false
		final boolean writerShouldBlock() {
            return false; 
        }
        //获取读锁是需要判断队头是不是有一个写锁线程在等待,如果是,那么这次获取读锁应该等待,避免写锁线程一直等待
        final boolean readerShouldBlock() {
            return apparentlyFirstQueuedIsExclusive();
        }

写锁

获取写锁

 protected final boolean tryAcquire(int acquires) {
			//工作流程
			//case1:
			//如果存在读锁,获取写锁失败
			//如果存在写锁,且当前线程不是获取到写锁的线程,获取写锁失败
			//case2
			//如果写锁重入次数>65535,获取失败,抛Error
			
            Thread current = Thread.currentThread();//当前线程
            int c = getState();//state同步状态
            int w = exclusiveCount(c);//写锁状态
            if (c != 0) {//如果存在读锁或者写锁
               	//如果存在读锁 或者存在写锁,且当前线程不是获取到写锁的线程
                if (w == 0 || current != getExclusiveOwnerThread())
                    return false;
                //如果写锁重入次数>65535
                if (w + exclusiveCount(acquires) > MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
                // 写锁重入,这里不用cas是因为这是重入,很安全
                setState(c + acquires);
                return true;
            }
            //c==0才会到这里,这表明此时没有读锁也没有写锁,也就是说这是第一次获取写锁
            //writerShouldBlock在非公平状态下返回false,在公平状态下返回hasQueuedPredecessors()
            //hasQueuedPredecessors()判断有没有前驱,同步队列头节点是空信息节点,这个头节点不算前驱
            //非公平模式下cas失败才会获取写锁失败
            //公平模式下如果有前驱或者cas失败,获取写锁失败
            if (writerShouldBlock() ||
                !compareAndSetState(c, c + acquires))
                return false;
            //第一次获取写锁成功后把独占线程设为自己
            setExclusiveOwnerThread(current);
            return true;
        }

释放写锁

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;
        }

读锁

读锁获取

protected final int tryAcquireShared(int unused) {//unused没有被用到,也无妨,因为是1
             //当前线程
            Thread current = Thread.currentThread();
            //同步状态state
            int c = getState();
            //如果有写锁&&当前线程不是获取写锁的线程
            if (exclusiveCount(c) != 0 &&
                getExclusiveOwnerThread() != current)
                return -1;
                
             //代码运行到这里的情况有,1:存在写锁,且当前线程是获取写锁的线程;2:没有写锁
            int r = sharedCount(c);// 读锁数值
            //readerShouldBlock非公平模式下返回apparentlyFirstQueuedIsExclusive(同步队列第二个节点所持有的线程要获取写锁)
            //公平模式下hasQueuedPredecessors()判断有没有前驱,同步队列头节点是空信息节点,这个头节点不算前驱
            //例如,readerShouldBlock在非公平模式下同步队列第二个节点持有的线程要获取写锁(头节点为空信息节点),
            //如果此时获取读锁应该阻塞,即加入同步队列等待被唤醒,我个人觉得应该是为了防止写锁得不到执行机会这种情况
            //不应该阻塞&&r<65535&&cas成功
            if (!readerShouldBlock() &&
                r < MAX_COUNT &&
                compareAndSetState(c, c + SHARED_UNIT)) {
                if (r == 0) {//第一次获取读锁
                    firstReader = current;//暂存第一次获取读锁的线程
                    firstReaderHoldCount = 1;//第一次获取读锁的线程的持锁次数
                } else if (firstReader == current) {//如果当前线程是第一次获取读锁的线程
                    firstReaderHoldCount++;//第一次获取读锁的线程的持锁次数++
                } else {//非第一次获取读锁的线程
                	// cachedHoldCounter 代表的是最后一个获取读锁的线程的计数器。
                    HoldCounter rh = cachedHoldCounter;
                    // 如果最后一个线程计数器是 null,那么就新建一个 HoldCounter 对象
                    // 或者最后一个线程不是当前线程,那么readHolds.get()取出当前线程的计数器对象
                    if (rh == null || rh.tid != getThreadId(current))
                    	//更新 “最后读取的线程计数器”(也是为了性能考虑)
                        cachedHoldCounter = rh = readHolds.get();
                    else if (rh.count == 0)
                        readHolds.set(rh);//这里set是因为再释放读锁时remove了
                    rh.count++;
                }
                return 1;
            }
            return fullTryAcquireShared(current);
        }

获取读锁的大致流程如下
1:判断是否有写锁
2:有写锁并且当前线程不是获取写锁线程,获取读锁失败
3:没有写锁或者有写锁且当前线程是获取写锁的线程,进入第4步
4:判断以下三个条件:获取写锁不应该阻塞&&读锁<65535&&cas成功
5:如果4判断成立,进6,如果不成立就fullTryAcquireShared
6:如果第一次获取读锁,设置firstReader和firstReaderHoldCount
7:如果当前线程是第一次获取读锁的线程,firstReaderHoldCount ++
8:不是6,7这两种情况,获取最后一个读锁的线程计数器,
如果这个计数器是null,利用readHolds.get();新建一个计数器赋给cachedHoldCounter
如果这个计数器是是当前线程的,计数器+1,
如果不是当前线程的,则获取当前线程的计数器,赋给cachedHoldCounter,计数器+1
流程图如下

在这里插入图片描述

说实话下面这段代码困扰我几小时,刚开始完全不知道他是干嘛的,看起来跟读写锁的核心一点关系都没有,后来看懂了确实是如此。这段代码其实就是为了记录每个线程获取读锁的次数。所以他用了ThreadLocal,首先定义一个HolderCount包含tid和count,再用ThreadLocalHolderCount包装HolderCount,这样一来就可以保存每个线程的tid和count(获取读锁的次数),本来直接用ThreadLocal的get和set就可以简单解决每个线程的获锁次数这个问题,可偏偏doug lea是个追求性能极限的大师,于是他用了几个缓存变量firstReader,firstReaderHoldCount,cachedHoldCounter,即保存第一个线程和最后一个线程的计数器信息,而我们的程序第一个线程和最后一个线程极有可能会大量重复执行,这样我们就不用每次从ThreadLocal的map里查找,直接用缓存变量就好了。

				//第一个线程只会走if 和else if
				if (r == 0) {
                    firstReader = current;
                    firstReaderHoldCount = 1;
                } else if (firstReader == current) {
                    firstReaderHoldCount++;
                } else {//其他线程走这里
                    HoldCounter rh = cachedHoldCounter;
                    // 如果最后一个线程计数器是 null,那么就新建一个 HoldCounter 对象
                    // 或者最后一个线程不是当前线程,那么readHolds.get()取出当前线程的计数器对象
                    if (rh == null || rh.tid != current.getId())
                        cachedHoldCounter = rh = readHolds.get();
                    else if (rh.count == 0)
                        readHolds.set(rh);
                    rh.count++;//如果是最后一个线程,一般来说直接跳到这里
                }

接下来我们来证明用几个缓存变量比直接从map里拿效率高,先看以下代码

public class TestHolderCount {
	
	static final class HoldCounter {
        int count = 0;
        final long tid = Thread.currentThread().getId();
		@Override
		public String toString() {
			return "HoldCounter [count=" + count + ", tid=" + tid + "]";
		}
        
     }
	 static final class ThreadLocalHoldCounter extends ThreadLocal<HoldCounter> {
        public HoldCounter initialValue() {
           return new HoldCounter();
        }
	 }
	 //这上面的代码全是读写锁的源码
	 final static ThreadLocalHoldCounter readHolds = new ThreadLocalHoldCounter();
	 //看这种方式去保存每个线程的获锁次数
	 public static void lockShared(){
		 if(readHolds.get().tid==Thread.currentThread().getId()){
			 readHolds.get().count++;
		 }
	 }
	 public static void main(String[] args) {
		 lockShared();
		 lockShared();
		 new Thread(new Runnable() {
			@Override
			public void run() {
				lockShared();
				lockShared();
				lockShared();
				System.out.println(readHolds.get());
			}
		}).start();
		 new Thread(new Runnable() {
				@Override
				public void run() {
					lockShared();
					System.out.println(readHolds.get());
				}
			}).start();
		 System.out.println(readHolds.get());
	 }
}

结果,可以看出来完全符合需求

HoldCounter [count=3, tid=9]
HoldCounter [count=2, tid=1]
HoldCounter [count=1, tid=10]

把main函数修改为如下

public static void main(String[] args) {
		 long start = System.currentTimeMillis();
		 for(int i=0;i<100000;i++){
			 lockShared();
		 }
		 System.out.println(System.currentTimeMillis()-start);
		 System.out.println(readHolds.get());
	 }

结果

90
HoldCounter [count=10000, tid=1]

再看doug lea优化后的代码

	 private static Thread firstReader = null;
     private static int firstReaderHoldCount;
     private static HoldCounter cachedHoldCounter;
     static int r = 0;
	 public static void lockShared(){
		 Thread current = Thread.currentThread();
		 if(r==0){
			 firstReader = current;
			 firstReaderHoldCount = 1;
		 }else if (firstReader == current) {
             firstReaderHoldCount++;
         } else {
             HoldCounter rh = cachedHoldCounter;
             if (rh == null || rh.tid != current.getId())
                 cachedHoldCounter = rh = readHolds.get();
             else if (rh.count == 0)
                 readHolds.set(rh);
             rh.count++;
         }
		 r++;
	 }
	public static void main(String[] args) {
		 long start = System.currentTimeMillis();
		 for(int i=0;i<100000;i++){
			 lockShared();
		 }
		 System.out.println(System.currentTimeMillis()-start);
		 System.out.println(firstReaderHoldCount);
		 
	 }

结果 ,4.5倍的效率,这里只用了firstReader测试,其实只要不是频繁的创建线程,效率都有大幅提升

20
100000

写的感觉都快跑题了,再回到fullTryAcquireShared

	//进这个方法的情况可能有
	//1.获取读锁应该阻塞
	//2.r>=65535
	//3.cas失败
	final int fullTryAcquireShared(Thread current) {
            HoldCounter rh = null;
            for (;;) {//死循环直到返回
                int c = getState();
                if (exclusiveCount(c) != 0) {//有写锁
                    if (getExclusiveOwnerThread() != current)//写锁线程不是当前线程
                        return -1;//获锁失败
                        //如果写锁线程是当前线程呢?没必要在判断readerShouldBlock了,直接去cas即可
                } else if (readerShouldBlock()) {//同步队列有优先级比这次获取读锁大的节点存在,获取读锁应该加入到同步队列队尾
                    //如果进入了这里,说明这次获取非重入的读锁应该加入到aqs同步队列,也就是说非重入的情况应该返回-1
                    if (firstReader == current) {//当前线程是第一个线程
                        //进入这里,肯定是重入第一个线程的情况,所以啥也不用做
                    } else {
                        if (rh == null) {
                            rh = cachedHoldCounter;//rh是最后一个线程的计数器
                            //如果rh==null或者当前线程不是最后一个线程
                            //情况一:rh==null,新建一个计数器,新建的计数器count肯定=0,所以会被remove,且return-1,
                            //为什么这种情况要remove且返回-1,因为进入到这里的代码如果是非重入的情况都得加到同步队列队尾
                            //情况二:rh!=null&&当前线程不是最后一个线程,拿到当前线程的计数器赋值给rh,
                            //如果rh.count==0,remove,return-1,count==0说明当前线程没有持有读锁,也是非重入的情况
                            //情况三:当前线程==最后一个线程,此时rh就是最后一个线程的计数器
                            //如果rh.count==0,remove,return-1,count==0说明最后一个线程没有持有读锁,也是非重入的情况
                            //情况四:如果情况二三里的count不为0,那么就是重入的情况,就可以考虑cas获取锁了
                            if (rh == null || rh.tid != current.getId()) {
                                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");
                    //到这里就可以做cas操作获取锁,代码和上面几乎一样
                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 != current.getId())
                            rh = readHolds.get();
                        else if (rh.count == 0)
                            readHolds.set(rh);
                        rh.count++;
                        cachedHoldCounter = rh; // cache for release
                    }
                    return 1;
                }
            }
        }

这个方法就是readerShouldBlock这个比较复杂,readerShouldBlock的意思是这次获取读锁应该考虑别人的优先级,比如在非公平模式下,应该考虑队头是不是有一个想要获取写锁的线程在等待,如果是,那么这次获取读锁应该阻塞,并进入同步队列的队尾,为什么这种情况获取读锁应该阻塞,我想大概是因为不让写锁线程等太久,在这里readerShouldBlock成立的话,里面的代码就需要拦截非重入获取读锁的线程,让他乖乖入队列等待,而重入的线程当然不需要等待。

读锁释放

读锁释放过程相对获取过程简单的多

	protected final boolean tryReleaseShared(int unused) {
            Thread current = Thread.currentThread();
            if (firstReader == current) {//当前线程是第一个线程
                if (firstReaderHoldCount == 1)
                    firstReader = null;
                else
                    firstReaderHoldCount--;
            } else {
                HoldCounter rh = cachedHoldCounter;//最后一个线程计数器
                if (rh == null || rh.tid != getThreadId(current))
                    rh = readHolds.get();
                int count = rh.count;
                if (count <= 1) {
                    readHolds.remove();//1的时候remove,下面再--就变成0了
                    if (count <= 0)
                        throw unmatchedUnlockException();
                }
                --rh.count;
            }
            //上面是对几个缓存变量做修改
            for (;;) {
                int c = getState();
                int nextc = c - SHARED_UNIT;//减去一个读锁单位,其实就是高16位减1
                if (compareAndSetState(c, nextc))
                    return nextc == 0;//return true意味着没有读锁也没有写锁
            }
        }

锁降级

锁降级这一块了解的不多,摘录一段源码

public class TestReadWriteLock {
    Object data;
    volatile boolean cacheValid;
    final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();

    void processCachedData() {
      rwl.readLock().lock();
      if (!cacheValid) {//这里被改为flase后,只有读锁了
          //得到写锁之前必须释放读锁,不释放读锁,无法活得写锁
        rwl.readLock().unlock();
        rwl.writeLock().lock();
        try {
          if (!cacheValid) {//只有一个线程改数据
            data = 1;
            cacheValid = true;
          }
         // 写锁释放前获取读锁,这样才是降级
          rwl.readLock().lock();
        } finally {
          rwl.writeLock().unlock();
        }
      }
      try {
          System.out.println(data);
      } finally {
        rwl.readLock().unlock();
      }

    }
}

总结

这篇关于读写锁的博文比我想象中的难写多了,我本来以为我研究过AQS写个读写锁博客应该挺简单的,可是java6为了实现获取每个读线程占锁次数这一功能,doug lea又用了几个变量来优化性能,刚开始看云里雾里,几度想放弃,好在坚持下来了,源码学习的过程注定是孤独的,贵在坚持。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值