ReadWriteLock源码分析

版权声明:https://blog.csdn.net/Burgess_Lee https://blog.csdn.net/Burgess_Lee/article/details/89492903

基于jdk1.8进行分析的。

ReadWriteLock也就是我们常说的是读写锁。解释过来就是:

  • 当写操作时,其它线程无法读取或写入数据,
  • 当读操作时,其它线程无法写数据,但却可以读取数据。

前面介绍的ReentrantLock是独占锁,这样的话,也就意味着在并发量比较大的情况下,还是效率比较低,因为操作中读/读、读/写、写/写都不能同时发生,从某种程度上降低了程序的执行效率。对ReentrantLock不了解的朋友,请移步这里

读写锁(ReadWriteLock):分为读锁(ReadLock)和写锁(WriteLock)。多个读锁不互斥,读锁与写锁互斥,这是由jvm自己控制的,你只要上好相应的锁即可。如果你的代码只读数据,可以很多线程同时读,但不能同时写,那就上读锁;如果你的代码修改数据,只能有一个人在写,且不能同时读取,那就上写锁。(此处大致列一下,看后期结论!!!)下面我们看一下ReadWriteLock接口的代码。

public interface ReadWriteLock {
    Lock readLock();

    Lock writeLock();
}

定义了两个接口规范,readLock和writeLock。通过源码我们看到ReadWriteLock有两个实现类,分别是:

  1. ReentrantReadWriteLock
  2. ReadWriteLockView

ReadWriteLockView类,我们看下源码:

    final class ReadWriteLockView implements ReadWriteLock {
        public Lock readLock() { return asReadLock(); }
        public Lock writeLock() { return asWriteLock(); }
    }

这个类是在StampedLock的一个内部类。

而ReentrantReadWriteLock则是ReadWriteLock的具体实现。说先整理一下关于关于该类使用的一些基本知识点。

1.重入方面其内部的WriteLock可以获取ReadLock,但是反过来ReadLock想要获得WriteLock则永远都不要想。

2.WriteLock可以降级为ReadLock,顺序是:先获得WriteLock再获得ReadLock,然后释放WriteLock,这时候线程将保持Readlock的持有。反过来ReadLock想要升级为WriteLock则不可能。

3.ReadLock可以被多个线程持有并且在作用时排斥任何的WriteLock,而WriteLock则是完全的互斥。这一特性最为重要,因为对于高读取频率而相对较低写入的数据结构,使用此类锁同步机制则可以提高并发量。

4.不管是ReadLock还是WriteLock都支持Interrupt,语义与ReentrantLock一致。

5.WriteLock支持Condition并且与ReentrantLock语义一致,而ReadLock则不能使用Condition,否则抛出UnsupportedOperationException异常。

6.此锁最多支持 65535 个递归写入锁和 65535 个读取锁。试图超出这些限制将导致锁方法抛出 Error。

然后我们看一个DEMO演示。

package demo;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * ReadWriteLock测试例程
 * @ClassName:   ReadWriteLockDemo  
 * @Description: TODO
 * @author       BurgessLee
 * @date         2019年4月24日  
 *
 */
public class ReadWriteLockDemo {
	private static ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
	private static Lock readLock = readWriteLock.readLock();
	private static Lock writeLock = readWriteLock.writeLock();
	private static Map<String,Object> map = new HashMap<String,Object>();
	
	public static void main(String[] args) {
		System.out.println(Thread.currentThread().getName());
		for(int i = 0; i < 10; i++) {
			map.put("key"+i, i+1);
		}
		try {
			Thread.sleep(3000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		
		//读读
		new Thread(new Runnable() {
		
			@Override
			public void run() {
				get(map,0);
			}
		}).start();
		new Thread(new Runnable() {
			
			@Override
			public void run() {
				get(map,0);
			}
		}).start();
		
		//读写 写读互斥
//		new Thread(new Runnable() {
//		
//			@Override
//			public void run() {
//				get(map,0);
//			}
//		}).start();
//		new Thread(new Runnable() {
//			
//			@Override
//			public void run() {
//				put(map,0);
//			}
//		}).start();
		
		//写写互斥
//		new Thread(new Runnable() {
//		
//			@Override
//			public void run() {
//				put(map,0);
//			}
//		}).start();
//		new Thread(new Runnable() {
//			
//			@Override
//			public void run() {
//				put(map,0);
//			}
//		}).start();
	}
	
	public static Integer get(Map<String,Object> map, int i) {
		System.out.println("get ..................");
		Integer getValue = -1;
		if(map.size() < 0) {
			//pass
			return getValue;
		}else {
			System.out.println(Thread.currentThread().getName() + "-上读锁");
			readLock.lock();
			getValue = (Integer) map.get("key" + i);
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
//			readLock.unlock();
			System.out.println(Thread.currentThread().getName() + "-释放读锁");
		}
		return getValue;
	}
	
	public static Integer put(Map<String,Object> map, int i) {
		System.out.println("put ..................");
		Integer putValue = -1;
		if(map.size() > 50) {
			return putValue;
		}else {
			System.out.println(Thread.currentThread().getName() + "-上写锁");
			writeLock.lock();
			map.put("key"+i, i+1);
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			putValue = i+1;
//			writeLock.unlock();
			System.out.println(Thread.currentThread().getName() + "-释放写锁");
		}
		return putValue;
	}
	
}

上面代码中演示了读读锁不互斥,读写,写读,以及写写都是互斥的。过程的话可以执行粘贴到本地运行一下就知道了。可以看到,代码下半部分都是将释放锁的过程直接注释掉了。以上就是DEMO演示部分。源码分析明天继续。

本来应该昨天更新,但是临时出了一些状况,所以今天继续。No more words. Show code!

我们看一下关于ReadWriteLock的源码。上面已经粘贴过了,此处不在贴出。声明了两个方法分别是readLock和writeLock两个规范。该接口有两个实现分别是ReadWriteLockView和ReentrantReadWriteLock,前者是StampedLock的一个内部类,后者才是具体的实现类,所以我们看一下ReentrantReadWriteLock。

通过源码结构我们可以看出内部有以下几个内部类,分别是:

  1. Sync 继承自AQS,之前在分析ReentrantLock的时候也有类似的结构
  2. NonFairSync Sync的子类,非公平性质的
  3. FairSync Sync的子类,公平性之的
  4. ReadLock 读锁类,当我们读取数据的时候需要加读锁
  5. WriteLock 写锁类,当我们写数据的时候需要加写锁

下面我们看具体的源码。

构造方法

ReentrantReadWriteLock() 

    public ReentrantReadWriteLock() {
        this(false);
    }

默认构造方法,通过调用this()实现,我们继续看。

ReentrantReadWriteLock(boolean fair)

    public ReentrantReadWriteLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
        readerLock = new ReadLock(this);
        writerLock = new WriteLock(this);
    }

同样通过我们分析ReentrantLock的经验我们可以看出,同样在构造方法中可以指定一个布尔值,分别对应公平锁和非公平锁的实现,此处并且多了ReadLock和WriteLock 的实例化,并且构造参数中都传入了this。

内部类

Sync

 abstract static class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = 6317671515068378041L;

        /*
         * Read vs write count extraction constants and functions.
         * Lock state is logically divided into two unsigned shorts:
         * The lower one representing the exclusive (writer) lock hold count,
         * and the upper the shared (reader) hold count.
         */

        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;
        //more code....

NonfairSync 

    static final class NonfairSync extends Sync {
        private static final long serialVersionUID = -8159625535654395037L;
        final boolean writerShouldBlock() {
            return false; // writers can always barge
        }
        final boolean readerShouldBlock() {
            /* As a heuristic to avoid indefinite writer starvation,
             * block if the thread that momentarily appears to be head
             * of queue, if one exists, is a waiting writer.  This is
             * only a probabilistic effect since a new reader will not
             * block if there is a waiting writer behind other enabled
             * readers that have not yet drained from the queue.
             */
            return apparentlyFirstQueuedIsExclusive();
        }
    }

FairSync  

    static final class FairSync extends Sync {
        private static final long serialVersionUID = -2274990926593161451L;
        final boolean writerShouldBlock() {
            return hasQueuedPredecessors();
        }
        final boolean readerShouldBlock() {
            return hasQueuedPredecessors();
        }
    }

通过三个内部类,我们看的出来,后两个都是继承自第一个类也就是Sync,后两个代码都很简洁,也没有构造方法,所以默认调用的是父类的构造方法,也就是:

    Sync() {
            readHolds = new ThreadLocalHoldCounter();
            setState(getState()); // ensures visibility of readHolds
        }

核心函数

通过DEMO中演示的代码我们可以看出,通过实例化ReentrantReadWriteLock类调用readLock和writeLock方法分别来获取读锁和写锁。那么我们看一下对应的源码实现。

public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }
public ReentrantReadWriteLock.ReadLock  readLock()  { return readerLock; }

可以分别返回的就是readerLock和writerLock,那么这两个东西不难猜测,一定是ReentrantReadWriteLock的成员属性。我们看一下声明。

    /** Inner class providing readlock */
    private final ReentrantReadWriteLock.ReadLock readerLock;
    /** Inner class providing writelock */
    private final ReentrantReadWriteLock.WriteLock writerLock;

通过源码注释部分的内容,可以看出来,都是通过自己内部类的实例,而在ReentrantReadWriteLock构造方法中,我们可以看到当实例化ReentrantReadWriteLock的实例的时候,对应的ReadLock和WriteLock也进行了实例化,并且传入了this。关于读锁和写锁,在上锁和释放锁的时候都是通过调用的是lock方法和unlock方法实现的,下面我们进入正题部分,逐个分析读锁和写锁的具体实现。

WriteLock

lock()

        public void lock() {
            sync.acquire(1);
        }

可以看到是直接使用的是sync的acquire方法,传入参数是1,所以根据之前分析AQS的源码经验来看,此处也是需要考虑重入的情况。

具体实现中调用的是acquire方法,源码如下:

    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

此时arg=1,并且我们看到了传入参数Node.EXCLUSIVE,也就是此时是独占锁的实现。然后分别调用tryAcquire和acquireQueued以及addWriter方法。

    //AQS未实现,希望子类继承实现,否则抛出异常
    protected boolean tryAcquire(int arg) {
        throw new UnsupportedOperationException();
    }
    //加入到同步队列中
    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;
    }
    //自旋,再次尝试获取锁
    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

上面代码中只有tryAcquire是AQS希望子类继承实现的,现在我们看一下具体实现。

        //尝试获取锁的方法
        protected final boolean tryAcquire(int acquires) {
            //记录当前线程
            Thread current = Thread.currentThread();
            //记录当前状态
            int c = getState();
            //重入数量
            int w = exclusiveCount(c);
            // 当前state不为0
            //如果写锁状态为0,说明读锁此时被占用返回false
            //如果写锁状态不为0且写锁没有被当前线程持有返回false
            if (c != 0) {
                //重入次数为0,且独占锁线程不是当前线程
                if (w == 0 || current != getExclusiveOwnerThread())
                    return false;
                //数据校验
                if (w + exclusiveCount(acquires) > MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
                //考虑重入情况,此时当前线程持有独占锁,重入次数更新
                setState(c + acquires);
                return true;
            }
            //state为0,尝试直接cas
            if (writerShouldBlock() ||
                !compareAndSetState(c, c + acquires))
                return false;
            //设置独占锁为当前线程
            setExclusiveOwnerThread(current);
            return true;
        }

代码中注释内容已经很清晰了,此处不再赘述,我们看里面调用到的方法,比较关心的也就是writerShouldBlock()

在Sync中的代码中如下:

abstract boolean writerShouldBlock();

所以我们看子类NonfairSync实现。

        final boolean writerShouldBlock() {
            return false; // writers can always barge
        }

没错,我们看到了直接返回false,注释内容也写的很清楚,写线程总是互斥的。下面我们来看释放锁的实现。

unlock()

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

调用sync的release实现的。

release(int arg)

    public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

参数此时是1,调用的是AQS中的release方法。主要用来释放锁,考虑重入情况。我们看一下源码中涉及调用到的其他方法。

    //需要子类继承实现,否则抛出异常
    protected boolean tryRelease(int arg) {
        throw new UnsupportedOperationException();
    }

 我们看一下具体的tryRelease方法的实现。

        protected final boolean tryRelease(int releases) {
            //不是当前线程抛出异常
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            //是当前线程,考虑重入情况
            int nextc = getState() - releases;
            boolean free = exclusiveCount(nextc) == 0;
            if (free)//如果释放完,state为0,此时释放锁,并设置独占锁线程为null
                setExclusiveOwnerThread(null);
            setState(nextc);
            return free;
        }
        //判断独占锁线程是不是当前线程
         protected final boolean isHeldExclusively() {
            return getExclusiveOwnerThread() == Thread.currentThread();
        }

下面我们来看一下ReadLock的lock和unlock方法的实现。

        public void lock() {
            sync.acquireShared(1);
        }

可以看到是通过sync调用acquireShared方法实现的,也就意味着这把锁是共享锁的实现。下面我们看acquireShared源码实现。

    public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0)
            doAcquireShared(arg);
    }

里面涉及到用到其他方法,我们继续往下看。

    //需要子类继承实现,否则抛出异常。
    protected int tryAcquireShared(int arg) {
        throw new UnsupportedOperationException();
    }

具体实现如下:

        protected final int tryAcquireShared(int unused) {
            //记录当前线程
            Thread current = Thread.currentThread();
            //当前线程状态
            int c = getState();
            //持有当前独占锁线程数目不为0,并且持有独占锁线程不是当前线程
            if (exclusiveCount(c) != 0 &&
                getExclusiveOwnerThread() != current)
                //返回值
                return -1;
            int r = sharedCount(c);//获取持有共享锁数量
            //如果公平策略没有要求阻塞且重入数没有到达最大值,则直接尝试CAS更新state
            if (!readerShouldBlock() &&
                r < MAX_COUNT &&
                compareAndSetState(c, c + SHARED_UNIT)) {
                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();
                    else if (rh.count == 0)
                        readHolds.set(rh);
                    rh.count++;
                }
                return 1;
            }
            //用来处理CAS没成功的情况,逻辑和上面的逻辑是类似的,就是加了无限循环
            return fullTryAcquireShared(current);
        }

 真正处理动作是通过doAcquireShared(int arg),方法实现的。

     private void doAcquireShared(int arg) {
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head) {
                    int r = tryAcquireShared(arg);
                    if (r >= 0) {
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        if (interrupted)
                            selfInterrupt();
                        failed = false;
                        return;
                    }
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

这段方法的执行逻辑和之前分析AQS类似,此处不再赘述。想具体了解,可以移步这里

下面我们看具体释放锁的源码。

unlock()

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

通过调用releaseShared实现,我们继续往下看。

    public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }

上面整个方法是用来释放共享锁的。我们继续看调用到的方法。

    //需要子类实现,否则抛出异常
    protected boolean tryReleaseShared(int arg) {
        throw new UnsupportedOperationException();
    }
        //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)) {
                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();
                    else if (rh.count == 0)
                        readHolds.set(rh);
                    rh.count++;
                }
                return 1;
            }
            return fullTryAcquireShared(current);
        }
    //执行释放共享锁具体实现动作
    private void doReleaseShared() {
        for (;;) {
            Node h = head;
            if (h != null && h != tail) {
                int ws = h.waitStatus;
                if (ws == Node.SIGNAL) {
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;            // loop to recheck cases
                    unparkSuccessor(h);
                }
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                // loop on failed CAS
            }
            if (h == head)                   // loop if head changed
                break;
        }
    }

所以这么来看,当一个线程持有写锁,因为是独占锁,所以此时另外一个线程想持有该资源的写锁是不可以的,但是当获取读锁的时候,通过源码我们看到,如果当前持有写锁的线程,如果不是当前同一个线程,那么此时也是可以获取到的。同理对于读锁是一个共享锁,那么当另外一个线程想要获取该资源的读锁时,是可以获取的,但是写锁是不可以的,因为写锁获取前加了判断。所以最后总结起来就是:

  1. 读不可以写,源码中上来第一行判断就是,如果持有读锁,想要获取写锁,那么直接失败
  2. 读可以读
  3. 写不可以写
  4. 写可以读,但是当获取读锁不是当前线程,那么直接获取失败

下面还有一个问题就是关于锁升级以及降级的问题。

通过上面的结论来看,当一个线程获取读锁,那么可能有其他线程也在读,所以不能升级为写锁。如果当一个线程获取了写锁,那么该线程此时持有该资源的读写锁,因此可以继续让它获取读锁,当它同时获取了写锁和读锁后,还可以先释放写锁继续持有读锁,这样一个写锁就“降级”为了读锁。

关于公平锁实现的读写锁的过程,此处不再赘述,有想要了解的朋友,可以自己去看一下,跟非公平锁实现起来只是有些细节稍微不同,其他过程都类似。

以上就是全部内容,如果有不对的地方还请指正。

猜你喜欢

转载自blog.csdn.net/Burgess_Lee/article/details/89492903