JDK1.8源码学习篇三——读写锁ReentrantReadWriteLock学习笔记

一、引言

    之前学习了java锁的相关概念,从最开始的大家使用是synchronized关键字,这个重量级锁,性能非常的低下,但是在jdk1.6之后经过优化之后, 性能大幅提升。但是在jdk1.5上新增加的锁lock性能和功能都大幅提升,被大家广泛采用。在上一篇文章中也学习了关于同步的一些基础构建AQS,同时在此基础上也学习了一下java同步中常用到的独占锁ReentrantLock的源码,从宏观上深入了解了一下独占锁的原理。独占锁,在使用的过程中 同一时间,只允许一个线程去获取锁,其他线程只能在等待状态,这样做的并发的安全性是提高了,但是在某些场景下,性能也降低了。举个例子,在日常生活中,读取数据的操作远远高于写操作的频率,但是如果都采用独占锁的,只能一个等待一个进行等待获取锁,然后读取数据。因此jdk中又提出了共享锁的概念,比如读锁,多个线程可以获取读锁,同一个时间去读操作,这个类就是ReentrantReadWriteLock。这是一个读写锁,分别包含读锁和写锁。读写锁在同一个是时刻允许多个线程访问,因此读锁是共享锁,但是在写锁只允许一个线程访问,是排他锁。

 二、ReentrantReadWriteLock详解

    
    在jdk1.5的并发包中,总共提出两种锁的类型,一种是共享锁,一种是排他锁。比如之前学习的ReentrantLock就是排他锁,在同一时刻,只能一个线程拥有锁,其他线程如果想要获取锁只能进入等待队列中,等待当前线程释放锁,唤醒下一个等待的线程。而共享锁指的是,在某一时刻可以有多个线程来获取锁,这种场景在生活中比较常见,比如读,多个线程可以同一时刻去读取数据,但是却不能同时写。ReentrantReadWriteLock其实是一种混合形式的锁,即包含了共享锁页包含了排他锁。这样做的目的是为了适应实际场景中一种业务,对于同一块数据,或者同一个对象,即存在需要读取的时候,也存在需要修改的时候,最常见的是缓存(cache)。如果对于这种场景,如果只用共享锁,允许多个线程读取或者写入数据,那么数据操作的原子性就无法得到保证在多并发的情况下,数据往往就是混乱的;而如果只是用排他锁,即在某一个时刻,只能允许某个线程读取操作,这样的的确是可以保证数据的原子性操作,但是却极大的降低了性能,因为很多时候多个线程可以一起读取的。所以在兼容以上场景下,即保证数据操作的原子性,又要保证性能的情况下,ReentrantReadWriteLock这种混合锁就显得非常重要了。读写锁也可以成为共享锁,毕竟读是可以同时操作的,一般的“读-读”可以共享锁,“读-写”、“写-写”都只能是排他锁。
 
  1. ReentrantReadWriteLock特性 
    先说说读写锁的特性:
    非公平锁:由于读线程之间不存在竞争性,所有没啥公平和非公平的区分,但是写操作,如果获取到写锁,其他线程和读锁就必须等待。
    公平锁:利用AQS的CLH队列,释放当前保持的锁(读锁或者写锁)时,优先为等待时间最长的那个写线程分配写入锁,当前提是写线程的等待时间要比所有读线程的等待时间要长。同样一个线程持有写入锁或者有一个写线程已经在等待了,那么试图获取公平锁的(非重入)所有线程(包括读写线程)都将被阻塞,直到最先的写线程释放锁。如果读线程的等待时间比写线程的等待时间还有长,那么一旦上一个写线程释放锁,这一组读线程将获取锁。
     重入锁: 读写锁允许读线程和写线程按照请求锁的顺序重新获取读锁或者写锁,当然只有写线程释放了锁,读线程才能获取锁,这就是重入性。
     降级锁:比如说写线程获取到写锁,写完之后,释放了写入锁,这样就变成了读锁,实现了锁的降级。
     锁升级:读锁是不能直接变成写锁的,因此要先释放读锁,然后获取写锁,这样就变成了锁升级。
     锁数量:读写所采用的是32位的二进制来保存锁的数量,其中高位保存读锁,低位报错血锁,因此锁的数量最大只能65535.

  2. 读写锁的使用
     说了这么多的读写锁的概念,下面先看看读写锁的使用场景,在经常使用到的缓存cache中就要用到读写锁,因为大量线程都要从缓存中读取数据,但是

     同时也要更新数据到缓存中,因此必然用到读写锁。下面我们先看个例子。

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,这就是读写所最常见的适用场景了。那么读写所到底是怎么实现的呢,接下来学习一下读写锁的源码。

 三、ReentrantReadWriteLock源码学习

这个类中共有5个内部类,其中包含了读锁和写锁类,然后有锁又分为公平锁和非公平锁,实现了不同的策略,具体的如下图。

JDK1.8源码学习篇三——读写锁ReentrantReadWriteLock学习笔记_第1张图片

和上一篇的基本差不多,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,只保留低位,这就计算出来了独占锁的数量。上面的逻辑就实现了高位保存共享锁,低位保存独占锁,一个非常巧妙的方法。

 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 {
            public HoldCounter initialValue() {
                return new HoldCounter();
            }
        }

与ReentrantLock不同的是,这里加了一个静态类HoldCounter,用来保存每个线程持有读锁的数量,而ThreadLocalHoldCounter则采用了继承的方式,这样可以重写iniitalvalue方法。用来报错当前线程持有的可重入读锁的数量。接下来继续看下读写锁具体的执行流程。

@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);
        }
 public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0)
            doAcquireShared(arg);
    }
以上的流程都非常简单,直接获取到读锁,然后调用lock方法进行获取锁,这里面可以看到先去调用tryAcquireShared()方法来获取读锁,以共享模式去获取锁,至少先去获取一次,如果有写锁,直接返回失败。下面看下代码里面的注释
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);  //获取重入锁失败进行重试
        }

上面的步骤总结一下:

1. 判断是否有写锁,有,直接失败

2. 没有写锁,获取共享锁的数量,并且更新状态,如果是第一个获取,则设置成firstreader,如果不是,并且还是自己,则重入

  次数加1操作,如果而已不是自己,则当前持有者改成自己,并更新状态

3.如果失败,则进入重试方法,如下

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

尝试获取共享锁失败,这里的逻辑总结一下

1. 如果当前已经存在线程占用了写锁,并且不是当前线程,直接返回失败

2. 如果公平策略中要求读线程阻塞,那只有一种情况会继续获取共享锁,就是重入操作。同样也分为两种情况,当前线程是第一个获取共享锁的线程,直接进入获取锁的操作,如果不是第一个,则需要判断的holdcounter的次数,只要不为0,则说明这个是一次重入操作,则尝试获取,rh.count==0意味着这是个新的线程在尝试获取共享锁,由于需要保证公平性,则尝试获取失败

3. 如果公平测了不要阻塞线程,则直接获取锁。

然后如果获取失败的话,需要进入等待对列中,执行方法doAcquireShared()这个方法,这个和之前的逻辑大致上是一致的这里就不重复去写了。

释放锁操作

上面是获取共享锁的过程,接下来看下释放锁的操作,具体的就是调用unlock方法,进入同样的逻辑。

  public  void unlock() {
     sync.releaseShared(1);
   }

public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }
这里比较核心的地方就是tryReleaseShared()这个方法,下面看下这个方法的源码
  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;   //使用多次循环,可能有多个读锁同时在进行释放操作,循环保证释放成功。
            }
        }
这里是释放锁的逻辑,相对比较简单,把高位状态减掉1,同时把当前线程持有锁的计数减一操作,在释放的过程中,有可能其他线程也在释放锁,所以修改状态是有可能失败的,这里放到一个无线循环里面,保证一定要修改成功。好了上面是读锁的获取过程,相对比较简单。下面看下写锁的获取过程和释放过程,同样是先获取WriteLock对象,然后调用lock方法获取写锁。
@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();
    }

这里调用的也是Sync下面的tryAcquire方法

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

再回到上个方法,如果获取写锁失败的话,需要添加到等待队列中,执行addWaiter()方法把,当前线程变成node节点

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

然在执行acquireQueue方法,添加到队列中,同时进行线程park,这个和前面讲到的ReentrantLock方法逻辑基本是上一样的,这里不再重复讲解,请参考前面的方法。

这里总结下写锁的获取逻辑,相对比较简单:

1. 首先是获取当前锁的状态,先获取锁的状态c,在获取独占锁的数量w,判断当前是否存在读锁

2. 如果c!=0,说明肯定有锁,在用w来确定是读锁还是写锁,如果w==0,则都是读锁,如果w!=0,则是有写锁存在,如果是由其他线程持有写锁,则当前线程也不能成功。

3. 如果c==0,则说明不存在锁,如果是公平策略,则进入同步队列,如果是非公平的,则会尝试去获取锁

4. 如果获取锁失败,则执行addWaiter方法,生成等待节点,并且投放到等待队列中。

下面看看怎么去释放写锁

 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;
    }
这里可以看到,也是去调用tryRelease方法释放锁,释放成功之后,进行唤醒后继线程
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;
        }
写锁释放和之前的独占锁差不多,不同的就是把低16位状态减一操作,如果为0,说明读锁可用,如果不为0,说明当前线程任然持有写锁。总的来说,写锁的获取和释放的过程都比较简单。



 



你可能感兴趣的:(java,编程语言,server)