ReentrantLock 与Synchronized 对比分析

ReentrantLock概念、公平性

什么是再入?
它表示当一个线程试图获取一个它已经获取的锁时,这个获取动作就自动成功,这是对锁获取粒度的一个概念,也就是锁的持有是以线程为单位,而不是基于调用次数。Java锁实现强调再入性是为了和pthread的行为进行区分。
再入锁可以设置公平性,我们可以在创建再入锁时选择是否公平。

ReentrantLock fairLock = new RenntrantLock(true);

这里所指的公平性,是指在竞争场景中,当公平性为真时,会倾向于将锁赋予等待时间最久的线程。公平性是减少线程饥饿(个别线程长期等待,但始终无法获取) 情况发生的一个办法。
如果使用Synchronized,我们无法进行公平性的选择,其永远都是不公平的,这也是主流操作系统调度的选择,在通用场景中,公平性未必有想象中的那么重要,Java默认的调度策略很少导致“饥饿发生”。与此同时如果要保证公平性,则会引入额外开销,自然导致一定的吞吐量下降。所以建议只有当程序确实有公平性需要时,才有必要指定它。

日常编码时,为保证锁释放,每一个lock()动作,都建议立即对应一个try-catch-finally。

ReentrantLock fairLock = new ReentrantLock(true);// 这里是演示创建公平锁,一般情况不需要。
	try {
		// do something
	} fnally {
		fairLock.unlock();
	}

ReentrantLock 中的重要方法

void lock();// 获取锁,不可被中断,及时当前线程中断,线程一直阻塞,直到得到锁

void lockInterrupted();// 获取锁,优先相应中断,抛出异常

boolean tryLock();// 尝试获取锁,成功true,失败false

boolean tryLock(long time, TimeUnit unit);// 超时返回false

void unlock();// 释放锁

Condition newCondition(); // 返回当前线程的Condition,可多次调用

// 以上为lock接口定义的标准方法

int getQueueLength();// 多少线程在等待

boolean hasQueueLength(); // 是否有线程在等待抢锁

int getHoldCount();// 当前线程是否抢到锁 0:没有

boolean isLock();// 查询是否有线程持有锁

Boolean isFair();// 是否为公平锁

对比

  1. ReentrantLock相比 Synchronized,因为可以像普通对象一样使用,所以可以利用其提供的各种遍历方法,进行精细的同步操作,甚至实现Synchronized难以表达的用例:
  • 带超时的获取锁尝试。
  • 可以判断是否有线程,或者某个特定线程在排队等待获取锁。
  • 可以相应中断请求。
  1. 另外ReentrantLock的实现在API、而Synchronized的实现在JVM层
  2. Synchronized在性能上在低竞争场景中,可能优于ReentrantLock

条件变量 Condition

这里特别强调 java.util.concurrent.condition,如果说ReentrantLock是 Synchronized的替代选择,Condition则是将wait()、notify()、notifyAll()等操作转化为相应的对象,将复杂晦涩的同步操作转变为直观可控的对象行为。
ReentrantLock中Condition使用

void await();// condition线程进入阻塞状态,调用signal()或signalAll()唤醒,允许中断,若有线程中断则抛出异常,必须先获取锁

void awaitUninterruptibly();// 线程进入阻塞状态,可唤醒,不允许中断,如果在阻塞是有线程中断,继续等待唤醒。

long awaitNanos(long nanosTimeout);// 设置阻塞时间,返回值大于0表示被唤醒,其他与await()类似

boolean await(long time,TimeUnit unit);// 被唤醒true

void signal();// 唤醒指定线程

void signalAll();// 唤醒所有线程

条件变量最典型的应用场景就是标准类库中的ArrayBlockingQueue等。
我们参考源码,首先通过再入锁获取条件变量:

	/** Condition for waiting takes */
	private fnal Condition notEmpty;
	/** Condition for waiting puts */
	private fnal Condition notFull;
	
public ArrayBlockingQueue(int capacity, boolean fair) {
	if (capacity <= 0)
		throw new IllegalArgumentException();
	this.items = new Object[capacity];
	lock = new ReentrantLock(fair);
	notEmpty = lock.newCondition();
	notFull = lock.newCondition();
}

两个条件变量是使用同一个再入锁创建出来的,然后使用在特定的操作中,如下面的take方法,判断和等待条件满足:

public E take() throws InterruptedException {
	fnal ReentrantLock lock = this.lock;
	lock.lockInterruptibly();
	try {
		while (count == 0)
			notEmpty.await();
		return dequeue();
	} fnally {
	lock.unlock();
	}
}

当队列为空时,试图take的线程的正确行为应该是等待入队发生,而不是直接返回,这是BlockingQueue的语义,使用条件notEmpty就可以优雅的实现这一逻辑。
那么如何保证入队后继续take呢?请看enqueue实现:

private void enqueue(E e) {
	// assert lock.isHeldByCurrentThread();
	// assert lock.getHoldCount() == 1;
	// assert items[putIndex] == null;
	fnal Object[] items = this.items;
	items[putIndex] = e;
	if (++putIndex == items.length) putIndex = 0;
		count++;
	notEmpty.signal(); // 通知等待的线程,非空条件已经满足
}

通过signal/await的组合,完成了条件判断和通知等待线程,非常顺畅就完成了状态流转。注意: signal和await的成对调用,不然如果只有await,线程会一直等到被打断。
从性能的角度,Synchronized早期实现比较低效,对比ReentrantLock,大多数场景性能都相差较大。但是在Java6中,进行了非常多的改进,在高竞争的情况下,ReentrantLock还是具有一定的优势的。但是在大多数场景下,无需纠结于性能,还是考虑代码书写结构的便利性和可维护性等。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值