本文参考:
https://github.com/Snailclimb/JavaGuide/blob/master/docs/java/synchronized.md
https://www.javadoop.com/post/AbstractQueuedSynchronizer-2
java并发编程艺术
本篇关于细节上的源码就不仔细探究了,以后再来搞把
java5以后提供了Lock接口以及相关实现类实现锁的功能,提供了与synchronized关键字类似的同步功能,
synchronized关键字是是依赖于 JVM 实现的,而Lock接口相关实现类依赖于API实现(AQS)。
AQS解析看这篇文章 https://blog.csdn.net/wmh1152151276/article/details/89398854
本文重点介绍ReentrantLock(可重入锁)实现类。
正确使用方式
Lock lock = new ReentrantLock();
lock.lock();
try {
// 业务逻辑
} finally {
lock.unlock();
}
注意两点
- 在finally块中释放锁,目的是保证在获取到锁之后,最终能够被释放。
- 不要将获取锁的过程写在try块中,因为如果在获取锁(自定义锁的实现)时发生了异常,异常抛出的同时,也会导致锁无故释放。
synchronized 和 Lock实现类ReentrantLock的对比
-
使用synchronized会自动隐式的获取和释放锁,简化了同步的管理
ReenTrantLock需要显式的获取和释放锁,缺少了便捷性,但是拥有了锁获取与释放的可操作性和灵活性 -
两者都是可重入锁,并且默认都是非公平锁,ReenTrantLock还提供了公平锁,可在构造方法中指定
-
Lock接口中提供了 synchronized 不具备的主要几个特性 (相当于ReentrantLock和synchronized对比)
- 获取锁过程中可被中断 通过lock.lockInterruptibly()来实现,当获取到锁或因为获取锁被阻塞时,可响应中断抛出异常。
- 尝试非阻塞的获取锁并可以超时获取 通过boolean tryLock()实现非阻塞的获取锁,获取成功或者获取失败都直接返回结果,不会阻塞等待再次获取。通过boolean tryLock(time, unit) 指定因为获取锁失败而等待的时间
-
ReentrantLock提供Condition来实现和wait notify类似的等待通知功能。 使用condition可以实现有选择的通知,在使用notify/notifyAll()方法进行通知时,被通知的线程是由 JVM 选择的。
这里列出lock接口的Api 然后说明ReentrantLock实现这些功能的细节
public interface Lock {
//获取锁,调用该方法将会获取锁,当锁获取后,从该方法返回
void lock();
//可中断地获取锁,和lock()方法的不同之处在于该方法会响应中断,即在锁的获取过程中可以中断当前线程
void lockInterruptibly() throws InterruptedException;
//尝试非阻塞的获取锁,调用该方法后会立刻返回,如果能够获取则返回true,否则返回false
boolean tryLock();
//超时地获取锁 1、当前线程在超时时间内成功获取锁。2、当前线程在超时时间内被中断。3、超时时间结束返回false。
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
//释放锁
void unlock();
//获取等待通知组件
Condition newCondition();
}
ReentrantLock重入锁 作为一个同步组件,它当然也是用了AQS作为实现。
它的内部不仅定义了一个Sync静态内部类继承自AQS
还另外定义了2个继承Sync的内部类 一个作为公平锁实现一个作为非公平锁实现

在构造函数中会创建对应的实现,如果不指定公不公平,默认是不公平的
以默认的非公平实现为例,
当我们 Lock lock = new ReentrantLock()之后 会创建一个NonfairSync的内部类对象

当我们调用lock.lock()时 其实就是调用了NonfairSync里的lock方法

下面是源码
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
因为是非公平的
所以首先会利用CAS直接试图修改同步状态为1 如果成功,设置获取锁线程为当前线程
如果失败才会和其他同步组件一样调用AQS中的acquire()模板方法
acquire()模板方法的内容在AQS解析中说到了这里不说了,反正就是会调用子类重写的tryAcquire(int acquires)
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
这里不多说直接看nonfairTryAcquire()
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 这里判断同步是否为0 然后试图修改为1 获取锁
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// 如果当前当前线程就是之前获取锁的线程 那么可以直接再次获得锁然后将同步状态 + 1
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
注意了可重入的性质就是在这里实现的:
加了一层判断获取锁的线程是否为当前线程 如果是就直接获取并将同步状态 + 1
并且我们可以看到非公平锁进行了2次CAS操作设置同步状态,
我们可以对比看一下公平锁 公平锁的实现靠的是fairSync内部类
公平锁设置同步状态的操作直接放在了tryAcquire()方法里,没有和非公平锁一样另外定义一个方法
final void lock() {
// 这里没有进行cas 直接调用acquire
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 这里是重点 比非公平锁多了一个判断:是否有线程在等待
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// 如果当前当前线程就是之前获取锁的线程 那么可以直接再次获得锁然后将同步状态 + 1
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
我们可以看到公平锁比非公平锁多了一个判断方法 hasQueuedPredecessors
这个方法判断此时的同步队列里是否有节点在阻塞等待,如果有则不抢了。
这是实现公平的关键。
总结:公平锁和非公平锁只有两处不同:
- 非公平锁在调用 lock 后,首先就会调用 CAS 进行一次抢锁,如果这个时候恰巧锁没有被占用,那么直接就获取到锁返回了。
- 非公平锁在 CAS 失败后,和公平锁一样都会进入到 tryAcquire 方法,在 tryAcquire
方法中,如果发现锁这个时候被释放了(state == 0),非公平锁会直接 CAS
抢锁,但是公平锁会判断等待队列是否有线程处于等待状态,如果有则不去抢锁,乖乖排到后面。
公平锁和非公平锁就这两点区别,如果这两次 CAS 都不成功,那么后面非公平锁和公平锁是一样的,都要进入到阻塞队列等待唤醒。
相对来说,非公平锁会有更好的性能,因为它的吞吐量比较大。当然,非公平锁让获取锁的时间变得更加不确定,可能会导致在阻塞队列中的线程长期处于饥饿状态。
获取可响应中断锁实现
//可中断地获取锁,和lock()方法的不同之处在于该方法会响应中断,即在锁的获取过程中可以中断当前线程
void lockInterruptibly() throws InterruptedException;
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
获取锁可中断靠的是这2个方法,当检测到中断时直接抛出异常
对比AQS中解析那篇文章中不响应中断的处理方法就可以发现不同,不响应中断只是会判断有没有中断过然后重新设置中断状态,仍常会照常去竞争锁资源,而这里响应中断,直接抛出异常。
public final void acquireInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
// 抛出异常
throw new InterruptedException();
if (!tryAcquire(arg))
doAcquireInterruptibly(arg);
}
private void doAcquireInterruptibly(int arg)
throws InterruptedException {
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
// 直接抛出异常
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
非阻塞获取锁
public boolean tryLock() {
return sync.nonfairTryAcquire(1);
}
很简单 可以看出只会尝试一次设置同步状态,成功或失败都直接返回结果。
失败之后不会构造节点加入同步队列进入阻塞状态。
对比lock
final void lock() {
acquire(1);
}
public final void acquire(int arg) {
// 在这里tryAcquire 也是调用了nonfairTryAcquire
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
失败会构造节点加入同步队列
Condition实现看这篇文章:
831

被折叠的 条评论
为什么被折叠?



