Java高并发编程---读写锁

 前面提到的锁基本都是排他锁,就是在同一时刻只允许一个线程访问的锁。现在有一个新的概念:读写锁。这种锁分离了读和写操作,因而允许在同一时刻多个读线程访问,而只能有一个写线程访问,这一模式使得并发性相比一般的排他锁有了很大的提升。
 除了保证写操作对读操作的可见性以及并发性的提升之外,读写锁能够简化读写交互场景的编程方式。
 一般情况下,读写锁的性能都会比排它锁好,因为大多数场景读是多于写的。在读多于写的情况下,读写锁能够提供比排它锁更好的并发性和吞吐量。Java并发包提供读写锁的实现是ReentrantReadWriteLock

ReentrantReadWriteLock特性
在这里插入图片描述

读写锁的接口与示例

 ReadWriteLock仅定义了获取读锁和写锁的两个方法,即readLock()方法和writeLock()方法,而其实现——ReentrantReadWriteLock,除了接口方法之外,还提供了一些便于外界监控其内部工作状态的方法。
在这里插入图片描述
读写锁的使用示例代码

public class Cache {
	static Map<String, Object> map = new HashMap<String, Object>();
	static ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
	static Lock r = rwl.readLock();
	static Lock w = rwl.writeLock();
	// 获取一个key对应的value
	public static final Object get(String key) {
			r.lock();
			try {
					return map.get(key);
			} finally {
					r.unlock();
			}
	}
	// 设置key对应的value,并返回旧的value
	public static final Object put(String key, Object value) {
			w.lock();
			try {
					return map.put(key, value);
			} finally {
					w.unlock();
			}
	}
	// 清空所有的内容
	public static final void clear() {
			w.lock();
			try {
					map.clear();
			} finally {
					w.unlock();
			}
	}
}

读写锁的实现分析

写锁的获取与释放
 写锁是一个支持重进入的排它锁。如果当前线程已经获取了写锁,则增加写状态。如果当前线程在获取写锁时,读锁已经被获取(读状态不为0)或者该线程不是已经获取写锁的线程,则当前线程进入等待状态。

ReentrantReadWriteLock的tryAcquire方法

protected final boolean tryAcquire(int acquires) {
	Thread current = Thread.currentThread();
	int c = getState();
	int w = exclusiveCount(c);
	if (c != 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;
	}
	if (writerShouldBlock() || !compareAndSetState(c, c + acquires)) {
			return false;
	}
	setExclusiveOwnerThread(current);
	return true;
}

 该方法除了重入条件(当前线程为获取了写锁的线程)之外,增加了一个读锁是否存在的判断。如果存在读锁,则写锁不能被获取,原因在于:读写锁要确保写锁的操作对读锁可见,如果允许读锁在已被获取的情况下对写锁的获取,那么正在运行的其他读线程就无法感知到当前写线程的操作。因此,只有等待其他读线程都释放了读锁,写锁才能被当前线程获取,而写锁一旦被获取,则其他读写线程的后续访问均被阻塞。

读锁的获取与释放
 读锁是一个支持重进入的共享锁,它能够被多个线程同时获取,在没有其他写线程访问(或者写状态为0)时,读锁总会被成功地获取,而所做的也只是(线程安全的)增加读状态。如果当前线程已经获取了读锁,则增加读状态。如果当前线程在获取读锁时,写锁已被其他线程获取,则进入等待状态。

ReentrantReadWriteLock的tryAcquireShared方法

protected final int tryAcquireShared(int unused) {
	for (;;) {
		int c = getState();
		int nextc = c + (1 << 16);
		if (nextc < c)
				throw new Error("Maximum lock count exceeded");
		if (exclusiveCount(c) != 0 && owner != Thread.currentThread())
				return -1;
		if (compareAndSetState(c, nextc))
				return 1;
	}
}

 在tryAcquireShared(int unused)方法中,如果其他线程已经获取了写锁,则当前线程获取读锁失败,进入等待状态。如果当前线程获取了写锁或者写锁未被获取,则当前线程(线程安全,依靠CAS保证)增加读状态,成功获取读锁。

锁降级
 这里所说的锁降级并不是我们平时说的一次降级,因为锁出于效率的考虑,锁可以升级但不能降级,目的是为了提高获得锁和释放锁的效率,因为自旋会消耗CPU,为了避免无用的自旋,一般不能降级。
 如果当前线程拥有写锁,然后将其释放,最后再获取读锁,这种分段完成的过程不能称之为锁降级。锁降级是指把持住(当前拥有的)写锁,再获取到读锁,随后释放(先前拥有的)写锁的过程。

processData方法

public void processData() {
	readLock.lock();
	if (!update) {
			// 必须先释放读锁
			readLock.unlock();
			// 锁降级从写锁获取到开始
			writeLock.lock();
			try {
			if (!update) {
			// 准备数据的流程(略)
			update = true;
			}
			readLock.lock();
			} finally {
			writeLock.unlock();
			}
			// 锁降级完成,写锁降级为读锁
	}
	try {
			// 使用数据的流程(略)
	} finally {
			readLock.unlock();
	}
}

 当数据发生变更后,update变量(布尔类型且volatile修饰)被设置为false,此时所有访问processData()方法的线程都能够感知到变化,但只有一个线程能够获取到写锁,其他线程会被阻塞在读锁和写锁的lock()方法上。当前线程获取写锁完成数据准备之后,再获取读锁,随后释放写锁,完成锁降级。
 为了保证数据的可见性,如果当前线程不获取读锁而是直接释放写锁,假设此刻另一个线程(记作线程T)获取了写锁并修改了数据,那么当前线程无法感知线程的数据更新。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值