《Java并发编程实践》五(1):显示锁

本书最后一部分:“并发高级主题“,其内容如下:

  • 第13章:显式锁ReentrantLock;
  • 第14章:构建自定义同步器;
  • 第15章:非阻塞同步器;
  • 第16章:java内存模型

ReentrantLock

java 5之前,java语言唯一的线程同步手段就是 synchronized 和 volatile;Java 5增加了ReentrantLock,它不是java监视锁的替代品,而是在后者不满足需求时,提供更丰富的功能。

java为锁定义了一个通用的接口Lock,提供了相关锁操作方法如下:

public interface Lock {
	void lock();
	void lockInterruptibly() throws InterruptedException;
	boolean tryLock();
	boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException;
	void unlock();
	Condition newCondition();
}

除了lock&unlock操作外,还提供了可中断加锁、尝试加锁、限时加锁操作(Condition下章讨论)。所有Lock实现类,提供了与监视锁相同的内存可见性保证(内存可见性在16章详细介绍),但在加锁、调度、性能特性方面各有不同。

ReentrantLock实现了Lock接口,它的加锁语义和监视器锁是一致的,它们之间的相同点如下:

  • ReentrantLock和监视器锁都是可重入的;
  • ReentrantLock.lock相当于进入synchronized代码块;
  • ReentrantLock.unlock相当于离开synchronized代码块。

不同点在于ReentrantLock提供的额外功能:

  • 线程调用ReentrantLock.lockInterruptibly加锁时如果被阻塞,可响应interrupt;
  • ReentrantLock.tryLock尝试加锁而不阻塞线程(限时版本不永久阻塞,可响应interrupt),可避免死锁;
  • 可以跨代码块使用ReentrantLock(一个方法加锁、另一个解锁),场景更加丰富。

ReentrantLock的这些新功能,使得它可以胜任某些监视器锁无法胜任的同步需求。

可中断加锁

在第7章讨论”异步任务取消“这个话题时,我们学习到,Excecutor框架通过java中断来取消正在执行中的任务,但成功与否取决于任务是否响应中断。而java监视器锁是不会响应中断,因此任务一旦使用了监视器锁,理论上存在不可中断的风险,ReentrantLock.lockInterruptibly则避免了此种风险。

还有就是当线程陷入死锁,如果死锁的是ReentrantLock.lockInterruptibly操作,还可以通过外部来中断该线程。要是监视器锁陷入死锁,就只有重启进程一条路了。

除非该锁有可能被长时间持有,否则,没有必要担心监视器锁不可中断的特性。

轮询&限时加锁

ReentrantLock.tryLock加锁失败立即返回而不陷入阻塞,提供了一种主动避免死锁的方式。下面的示例通过该特性来改进账户转账功能:

public boolean transferMoney(Account fromAcct, Account toAcct,DollarAmount amount,long timeout,TimeUnit unit)
			throws InsufficientFundsException, InterruptedException {
	long fixedDelay = getFixedDelayComponentNanos(timeout, unit);
	long randMod = getRandomDelayModulusNanos(timeout, unit);
	long stopTime = System.nanoTime() + unit.toNanos(timeout);
	while (true) {
		if (fromAcct.lock.tryLock()) {
			try {
				if (toAcct.lock.tryLock()) {
					try {
						if (fromAcct.getBalance().compareTo(amount) < 0)
							throw new InsufficientFundsException();
						else {
							fromAcct.debit(amount);
							toAcct.credit(amount);
							return true;
						}
					} finally {
						toAcct.lock.unlock();
					}
				}
			} finally {
				fromAcct.lock.unlock();
			}
		}
		if (System.nanoTime() > stopTime)
			return false;
		NANOSECONDS.sleep(fixedDelay + rnd.nextLong() % randMod);
	}
}

上面代码的要点如下(避免死锁的技巧):

  • 使用ReentrantLock.tryLock而不是监视器锁来保护账号对象;
  • 由于tryLock在失败后立即返回,所以通过while循环来不断重试,知道超时(timeout参数);
  • 先执行fromAcct.lock.tryLock(),再执行toAcct.lock.tryLock(),两者都成功才执行转账;如果第二个锁获取失败,立即释放第一个锁;
  • 如果加锁失败,sleep一段随机的时间再重试(否则可能陷入上一章介绍的所谓“活锁“)

跨代码块加锁解锁

java监视器锁只能用在同一个代码块内,在更复杂的场景下,可能并不满足需求。第十一章展示的锁拆分&锁分离技术,一般来说,需要显式锁才能实现。

监视器锁 VS ReentrantLock

性能

在java 5引入ReentrantLock时,它的性能比监视器锁明显要好。对于同步机制而言,锁在发生竞争时的性能表现是程序可伸缩性的一个关键因素;如果处理锁竞争耗费时间更多,用于务执行的时间就更少,而且进一步加大锁竞争发生概率,使程序在高并发场景下的性能急剧下降。

不过Java 6对监视器进行了大幅优化,使得监视器锁和ReentrantLock的性能相差无几。由于监视器锁是JVM内置的,每代JVM都可能会对监视器锁进行优化,而且JVM还能动态进行锁粗化等运行时优化(对显式锁JVM很难进行运行时优化),因此Java7以上版本的监视器锁的性能已经超过ReentrantLock。

复杂性

transferMoney所展示的ReentrantLock用法想必令大家印象深刻,相比synchronized代码块,使用ReentrantLock代码的复杂度要大得多,也更难维护。更加重要的是,ReentrantLock必须手动释放,如果忘记或由于异常没有释放,就会造成死锁。

综上,除非需要ReentrantLock的额外功能,应该优先使用监视器锁。

锁的公平性

所谓锁的公平性是指,当多个线程都在等待同一把锁时,哪个线程优先获得锁的规则。如果能保证按着线程请求锁的顺序来获得锁,那么锁是公平的,否则锁是不公平的。不管锁是否公平的,等待锁的线程会形成一个队列,非公平锁并非刻意制造乱序,而是当某个线程尝试加锁时,发现锁是恰好是空闲的,会绕过队列直接成功加锁。因此,实际上不公平锁大体上也是公平的,只是不保证而已。

java监视器锁是不公平的,ReentrantLock可以指定公平性;但是公平锁的性能会差很多,可以推演一下A,B,C线程竞争同一个锁的场景:

  • 假设线程A当前拥有锁,线程B正在等待锁;
  • 下一个时刻线程C尝试获取锁,此刻刚好A释放了锁;
  • 如果锁是非公平的,C加锁成功,继续运行,C释放锁后,B线程被唤醒并获得锁;
  • 如果锁是公平的,C被挂起,B被唤醒,B加锁成功且运行完毕后释放锁,C再被唤醒并运行;

上面的分析揭示了,公平锁加锁过程相对更复杂,且导致额外的两次上下文切换;在高并发的情况下,上面的场景发生频率是很高的,所以公平锁对性能伤害不可忽视。

绝大多数情况下,只要所有线程最终能获得锁,绝对的公平性并没有那么重要;只有那些持有锁时间较长、加锁频率不高,且确实需要公平性的场景,才需要公平锁。

ReadWriteLock

ReentrantLock实现了标准的互斥锁,但是在有些场景下,并不需要如此强的互斥性。在对数据状态”读多写少“,且只需要将”写-写“、”写-读“串行化的场景,可使用ReadWriteLock。

ReadWriteLock是一个接口:

public interface ReadWriteLock {
	Lock readLock();
	Lock writeLock();
}

ReadWriteLock实际包含一个读锁,一个写锁,读锁和写锁之间存在同步交互;ReadWriteLock的具体实现需要考虑以下几个方面:

  • 写锁释放倾向性:当一个写锁释放时,如果同时有写线程和读线程在等待,优先策略如何定?
  • 读锁抢占:如果当前有一个读锁,且有一个线程尝试对写锁加锁;此时又来一个线程尝试加读锁,应该立即成功还是等待?
  • 重入性:读锁和写锁都是重入的吗?
  • 降级:一个线程持有写锁,是否能继续获取读锁?如果允许,相当于将写锁降级为读锁;
  • 升级:一个读锁是否能够升级为读锁?

ReentrantReadWriteLock是ReadWriteLock的实现者,与ReentrantLock一样,它也是可重入的,也可以指定公平性。如果ReentrantReadWriteLock是公平的,那么严格按照线程加锁的顺序来,否则效率优先。ReentrantReadWriteLock支持锁降级,但不支持升级,因为升级容易产生死锁。

ReadWriteLock在并发读频繁,并发写不频繁,且持有锁的时间相对较长的情况下能替代互斥锁改善性能;否则的话,由于ReadWriteLock自身更复杂一些,性能可能更差。在使用时,需要通过测试来对比,如果ReadWriteLock并没有优势,还是用ReentrantLock或监视器锁更好。

总结

相对监视器锁,显式的锁能够提供更多的功能,适应更灵活的场景。ReentrantLock与监视器锁语义一致,提供了几个额外功能:尝试加锁、可中断加锁、限时加锁、公平锁,且能够跨代码块加锁解锁。ReentrantLock并不是用来取代监视器锁的,仅在后者不满足需求时才考虑ReentrantLock。

ReadWriteLock是java定义的另一种显式锁,它在”读多写少“的场景下,可能提高程序可伸缩性,需要测试来证明效果。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值