java并发编程——读写锁ReentrantReadWriteLock

读写锁

  • 同时允许多个读线程获取这个锁(读锁)
    读写锁允许多个读线程同时获取到读锁(共享模式),但是当写线程获取到了写锁时其他任何读写操作(同一个线程依旧可以继续获取读锁,详细看下文,锁降级)均会被阻塞(排他模式)。很多场景是读大大多于写操作的,所以使用读写锁能更大程度的增强程序的吞吐量。

  • 按位切割维护两把锁
    一个private volatile int state变量维护了两把锁:读锁、写锁
    private volatile int state是一个int变量,有32bit:高16位维护读锁,低16位维护写锁。

  • 使用场景
    用于读多于写的场景,读写锁能够比排他锁提供更好的并发性、吞吐量!
    使用示例:

/**
 * @author zhangsh
 *
 *         读操作大于写操作的场景,使用读写锁提高吞吐量
 */
public class Cache {
	static Map<String, Object> map = new HashMap<String, Object>();

	static ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();

	static Lock readLock = readWriteLock.readLock();

	static Lock writeLock = readWriteLock.writeLock();

	public static final Object get(String key) {
		readLock.lock();
		try {
			return map.get(key);
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			readLock.unlock();
		}
		return null;
	}

	public static final void put(String key, Object value) {
		writeLock.lock();
		try {
			map.put(key, value);
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			writeLock.unlock();
		}
	}

	public static final void remove(String key) {
		writeLock.lock();
		try {
			map.remove(key);
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			writeLock.unlock();
		}
	}
	public static final void clear() {
		writeLock.lock();
		try {
			map.clear();
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			writeLock.unlock();
		}
	}
}

当多个线程操作这个集合(Map)时,一个写线程获取到写锁是会阻塞其他线程,当写操作完成时,其他读操作的线程继续执行,保证了写操作的可见性,避免脏读。

这里写图片描述

这里写图片描述

ReentrantReadWriteLock中有三个关键的成员变量:


    /**
     * 静态内部类
     * 
     * 组合sync,用sync实现读锁
     */
    private final ReentrantReadWriteLock.ReadLock readerLock;

    /**
     * 静态内部类
     * 
     * 组合sync,用sync实现写锁
     */
    private final ReentrantReadWriteLock.WriteLock writerLock;

    /** 
     *abstract static class Sync  AQS的资料sync实现底层同步器.
     * 
     * Sync有两个子类,公平同步器FairSync;非公平同步器NonFairSync
     */
    final Sync sync;

ReentrantReadWriteLock.sync

/**
	 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.
		 * 
		 * 以下是读写锁的计数器提取后常量、函数。
		 * 
		 * 锁的state状态(int 32bit)被逻辑上,通过位运算,分为两个无符号short数(16bit):
		 * 这低16bit代表互斥的排他锁的持有状态(写锁),
		 * 高16bit代表共享锁的持有状态(读锁)。
		 * 
		 * 
		 */

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

.........
		static final class HoldCounter {
			int count = 0;
			// Use id, not reference, to avoid garbage retention
			final long tid = getThreadId(Thread.currentThread());
		}

		/**
		 */
		static final class ThreadLocalHoldCounter extends ThreadLocal<HoldCounter> {
			public HoldCounter initialValue() {
				return new HoldCounter();
			}
		}

		/**
		 * 用来记录每个获取读锁的线程id以及线程重入情况。
		 * 当前线程持有的读锁的重入次数,每个线程获取的读锁状态保存在ThreadLocal中
		 * 仅在构造器和readObject时完成初始化。当一个读锁的重入数量为0时清除
		 */
		private transient ThreadLocalHoldCounter readHolds;

		/**
		 * 共享锁的计数器缓存(记录最后一个获取读锁成功的线程情况:线程ID,线程重入次数)
		 */
		private transient HoldCounter cachedHoldCounter;

		/**
		 *用來引用首个获取共享读锁的线程(CAS 0——>1).
		 */
		private transient Thread firstReader = null;
		private transient int firstReaderHoldCount;

ReentrantReadWriteLock特性

  • 公平性非公平性可选择
    使用Sync的两个子类FairSync、NonfairSync,实现获取锁的公平性、非公平性。
    /**
     * Nonfair version of Sync
     */
    static final class NonfairSync extends Sync {
        private static final long serialVersionUID = -8159625535654395037L;

        final boolean writerShouldBlock() {
            return false; // writers can always barge  不去考虑FIFO规则,直接CAS尝试
        }

        final boolean readerShouldBlock() {
//头结点之后是否存在一个独占式的结点(写操作),如果存在就要阻塞进入排队中
//也就是在线程获取读锁之前,如果有写锁等待,那么会阻塞
        return apparentlyFirstQueuedIsExclusive();
        }
    }
    /**
     * Fair version of Sync
     */
    static final class FairSync extends Sync {
        private static final long serialVersionUID = -2274990926593161451L;

        final boolean writerShouldBlock() {
            return hasQueuedPredecessors();//判断头结点后继结点是否是当前线程结点。也就是不许插队,尝试插队就阻塞。
        }

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

总结:
在ReentrantReadWriteLock中实现了公平锁与非公平锁,主要区别就是:当一个线程尝试获取公平锁时,会检查这个结点是否是头结点的后继者,也就是不允许插队,插队就阻塞!

与ReentrantLock中的公平锁、非公平锁类似,默认使用非公平锁。非公平锁有更高的吞吐率,更低的线程切换消耗!

  • 可重入
    一个线程获取读锁后,这个线程可以再次获取这个读锁,但是这个线程不能获取对应写锁。
    一个线程获取写锁后,这个线程可以再次获取这个写锁,同时它也可以获取对应的读锁。

  • 锁降级
    见后文。

写锁的获取

与ReentrantLock类似,继承AQS,实现如下模板方法:
protected boolean tryAcquire(int arg);
protected boolean tryRelease(int arg);
protected int tryAcquireShared(int arg);
protected boolean tryReleaseShared(int arg);
protected boolean isHeldExclusively() ;

写锁属于排他锁,自然需要实现tryAcquire、tryRelease
ReentrantReadWriteLock.WriteLock.lock()

		public void lock() {
			sync.acquire(1);//调用sync父类AQS.acquire
		}

AbstractQueuedSynchronizer.acquire(int arg)
AQS请参看AQS文章AQS详解

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

ReentrantReadWriteLock.Sync.tryAcquire(int acquires)

        /**
         * 写锁的获取
         * @param acquires
         * @return
         */
        protected final boolean tryAcquire(int acquires) {
            /*
             * 1.当读锁被其他线程占用则失败;写锁被其他线程占用则失败。(写锁的排他性决定了获取成功的前提是:读写锁都空闲)
             * 当读锁或写锁任意一个被占有时,读锁被任意线程持有,写锁的获取失败 ;
             * 当读锁或写锁任意一个被占有时,读锁未被占用,写锁被占用,并且占用写锁的是其他线程,该线程获取锁失败.
             * 也就是说一个线程试图获取锁时,如果有任何一个线程已经占用了写锁,或者读锁,则失败。看来,写锁的获取完全是排他的获取。
             * 
             * 2.如果计数超过阈值,则失败(重入次数检查) 
             * 
             * 
             * 3.如果1、2通过,则判断是否满足公平要求,然后使用CAS获取写锁
             */
            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)
                // 如果c!=0,w==0则读锁被获取了,或者写锁持有者不是当前线程;失败。也就是体现写锁的排他性
                if (w == 0 || current != getExclusiveOwnerThread())//第二个判断考虑了锁降级
                    return false;
                if (w + exclusiveCount(acquires) > MAX_COUNT)// 写锁 重入的次数超过阈值
                    throw new Error("Maximum lock count exceeded");
                // Reentrant acquire
                setState(c + acquires);// 写锁重入
                return true;// true成功
            }
            // 读写锁完全free:
            // 1.若是公平锁,writerShouldBlock保证了当前线程之前没有等待结点,从而保证FIFO。若是非公平锁,直接忽略这个判断
            // 2.CAS更新状态
            if (writerShouldBlock() || !compareAndSetState(c, c + acquires))
                return false;
            setExclusiveOwnerThread(current);
            return true;
        }

总结:
1.根据state的高低位,确认读锁写锁占用情况,满足条件尝试CAS获取锁:

读写锁中至少一个被占用:
(1)读锁被占用,返回false,继续执行AQS同步队列等待逻辑。

(2)写锁被其他线程占用,返回false,继续执行AQS同步队列等待逻辑。

(3)写锁被当前线程占用,检查重入限制,通过检查再次重入,state++,返回TRUE;

读写锁均空闲:
若是公平模式,则考虑是否插队,插队返回FALSE;
没有插队或者是非公平模式,则CAS获取写锁;成功则返回TRUE,否则返回FALSE;

2.失败则进入AQS同步队列逻辑。参看AQS文章。

tryLock()方法与lock()方法中的tryAcquire执行逻辑非常类似,就是只去尝试获取锁,失败直接返回不会进入AQS同步队列中


写锁的释放

ReentrantReadWriteLock.WriteLock.unlock()

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

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

ReentrantReadWriteLock.Sync.tryRelease(int releases)

		protected final boolean tryRelease(int releases) {
			if (!isHeldExclusively())// !当前锁是否排他模式被占有?
				throw new IllegalMonitorStateException();
			int nextc = getState() - releases;// state计数器准备释放一次release
			boolean free = exclusiveCount(nextc) == 0;// 获取排他模式下的计数器个数
			if (free)// 当前写锁是否空闲
				setExclusiveOwnerThread(null);
			setState(nextc);
			return free;
		}

总结
锁的释放都大同小异,就是释放同步状态,并唤醒后继者。


读锁的获取释放

当写锁没有被其他非当前线程持有时,读锁通过每次增加值,达到多个线程获取锁的共享效果。同时维护读锁被获取到的数量与读锁被某个线程重入的数量(ThreadLocal)。

ReentrantReadWriteLock.ReadLock.lock()

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

AbstractQueuedSynchronizer.acquireShared(int arg)

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

ReentrantReadWriteLock.Sync.tryAcquireShared(int unused)

protected final int tryAcquireShared(int unused) {
			/*
			 * 此方法实现用于读锁。
			 * 1.写锁被其他线程占有,失败
			 * 2.当前是否满足排队策略?如果满足排队策略,并且不需要等待,那么CAS.注意此步骤没有检查重入性获取。
			 * 3.如果2因为CAS失败或者饱和异常或者没有资格,那么绑定版本号循环再试
			 */
			Thread current = Thread.currentThread();
			int c = getState();
			// 如果:写锁被占用 && 当前线程不是占有锁的线程——>写锁被其他线程占用时,读锁的获取被阻塞
			if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current)
				return -1;// -1表示失败
			int r = sharedCount(c);
			// 如果:读线程不需要被阻塞(当头结点的后继结点为独占模式 将会阻塞,为了不产生脏读,要考虑之前进入的写操作) &&
			// 读锁提取数没有饱和 && CAS(注意此处 c+SHARED_UNIT)
			if (!readerShouldBlock() && r < MAX_COUNT && compareAndSetState(c, c + SHARED_UNIT)) {
				if (r == 0) {// 读锁的提取数为0(当前线程占用之前,读锁未被占用)
					firstReader = current;// 当前线程称为第一个读线程
					firstReaderHoldCount = 1;
				} else if (firstReader == current) {// (这个锁已经被其他读线程共享了)当前线程是否是第一个共享读写线程
					firstReaderHoldCount++;// 首读线程计数+1,代表重入!
				} else {// 读锁之前已经被共享了,当前线程不是获取读锁的首个线程
					HoldCounter rh = cachedHoldCounter;
					if (rh == null || rh.tid != getThreadId(current))// cachedHoldCounter不为空||
																		// 当前线程不是缓存的线程
						cachedHoldCounter = rh = readHolds.get();
					// 不为空且是当前线程,并且cahce的锁记录为0,说明当前仅有首线程获取到了锁(可能其他线程已经释放了读锁)
					else if (rh.count == 0)
						readHolds.set(rh);
					rh.count++;// 锁重入情况
				}
				return 1;
			}
			// CAS失败后再次尝试读锁获取
			return fullTryAcquireShared(current);
		}

总结
1.当写锁被其他线程占用,返回false,进入AQS同步队列中等待(doAcquireShared)。
2.如果写锁没有被其他线程占用,满足公平性要求,低于共享锁最大共享次数,尝试CAS获取共享锁。
3.CAS获取共享锁成功,记录当前线程、重入次数等信息,返回
4.CAS获取共享锁失败,进入fullTryAcquireShared(),循环重试获取锁。

另外:对于读锁,每个线程各自获取读锁的次数只能选择保存在ThreadLocal中


锁降级:写锁降级为读锁

public void processData() {
	readLock.lock();
	if (!update) {// 如果未更新完成
		readLock.unlock();// 必须先释放读锁
		writeLock.lock();// 锁降级:1.获取写锁

		try{
		if (!update) {
			// 准备数据....略
			update = true;
		}
		readLock.lock();// 锁降级:2.获取读锁。写锁获取后这线程可以继续获取读锁,反之不行
		}catch (Exception e) {
			e.printStackTrace();
		}finally {
			writeLock.unlock(); 锁降级:3.释放写锁
		}
	}
	
	try{
		// 读取更新后的数据.........
	}finally{
		readLock.unlock();
	}

}

}

降级:
同一个线程下,获取写锁——>获取读锁——>释放写锁(降级为读锁完成)——>释放读锁。
降级的目的:
为了保证吞吐性,已经可见性,线程A先获取写锁,更新数据,接着需要读取更新后的数据。在这个“更新数据”与 "读取更新后的数据"之间不允许其他线程对数据进行修改,通过直接降级为读锁阻塞了其他线程对数据的更新。同时读锁更大程度的提供了吞吐性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值