前言
lock是Java接口,采用乐观锁的设计思想,采用CAS操作来实现的一个锁,这几点跟synchronized不同,synchronized原始采用的是CPU悲观锁机制。
为什么需要LOCK 锁
1.5之前大家都用synchronized锁,怎么突然要搞一个lock锁?大家都不喜欢重复造轮子,一件事有人干了,你就别重复了,没必要,但Java源码开发者难道不懂这个道理,他们的考虑是什么?我们来看看:
-
1、获取到锁后,如果获取不到资源,是不是要等待?
synchronized锁确实是,没有获取到足够的资源,就要一直等待,它是不能释放掉的,你也改不了它,似乎有点别扭 -
2、超时设置
synchronized锁 是没有超时设置的,如果一个线程等待超时了,自动释放掉,让其他线程来处理,似乎是一个不错的设计。
于是有人就说了,再来一个锁,于是lock锁出现了。
lock锁怎么获取锁
通过volatile 修饰的state变量来控制,底层通过cas来设置变量,设置成功就获取锁成功。
- state == 0
这个状态表示锁被释放了,但不会说这个时刻就是你获取了锁,如果有其他线程在队列里,可能需要互相竞争,获取了锁,就会唤醒线程,没有获取锁的线程被加入到AQS的队列中,自旋方式来获取同步状态state,等待唤醒。 - state > 0
表示锁被占用,需要通过类似自旋的方式来获取同步状态,直到这个同步状态等于0。
如果当前线程已经拥有该锁,再调用lock()方法会立即返回。那么就可以执行lock() 与 unlock() 之间的代码逻辑。
公平锁、非公平锁
公平锁主要是根据排队的线程先后顺序来抢占资源,非公平锁就是看每个线程本事,自由竞争锁资源。通过字段来控制公平和非公平,这个有默认值的。
如何保证公平
非公平其实很好实现,大家都自由竞争,公平则是靠AQS 来实现,AQS全程是 Abstract Queued Synchronizer,中文叫做队列同步器,是一个抽象队列安全器,这个AQS主要是面向锁的实现的,用作构建lock的主要组建,所以有些博客里认为:
- lock锁是面向使用者
- AQS 是面向锁的,AQS屏蔽里同步状态管理、线程排队、等待\唤醒 等一些底层操作。这句话是我抄的,文字表达很得体。
其实非公平就是每次来一个线程抢锁,抢到就是你的,抢不到,你就老老实实的去排队,不过在tryAcquire方法中也有不用,公平锁会判断当前节点是不是第一个节点,如果不是就继续自旋,非公平就不会有这一步判断
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
/**
* Performs lock. Try immediate barge, backing up to normal
* acquire on failure.
*/
final void lock() {
if (compareAndSetState(0, 1)) // 这里 会去cas同步状态,失败了就执行acquire(1); 其实就变成了公平锁。
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires); // 注意这个差异
}
}
/**
* Sync object for fair locks
*/
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
acquire(1);
}
/**
* Fair version of tryAcquire. Don't grant access unless
* recursive call or no waiters or is first.
*/
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;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
独占和共享
在实际的实现方式上,有2种方式,独占和共享:
- 独占式 Exclusive
只要AQS的state变量不为0,并且持有锁的线程不是当前线程,那么代表资源不可访问,其他线程进来就会获取失败,并且加入到同步队列中。意思就是同步状态state是只能一个线程占有。如ReentrantLock利用了其独占功能。 - 共享式 Share
只要AQS的state变量不为0,那么代表资源不可以访问。但共享式在同一时刻可以有多个线程获取同步状态。例如读操作可以多个线程同时读,写操作同一时刻只能有一个线程写。 采取自旋的方式来获取同步状态会竞争资源。CountDownLatch,Semaphore利用了其共享功能。
共享式与独占式的最主要区别在于同一时刻独占式只能有一个线程获取同步状态,而共享式在同一时刻可以有多个线程获取同步状态。
- 如果某个同步器支持独占的获取操作,那么需要实现一些保护方法,包括tryAcquire、tryRelease、isHeldExclusivery等。
- 对于支持共享获取的同步器,则应该实现tryAcquireShared、tryReleaseShared等方法。
AQS的acquire、acquireShared、release、releaseShared等方法都将调用这些方法在子类中带有前缀try的版本来判断某个操作。一层一层调用:lock --> acquire --> tryAcquire ,这样三层一层一层调用。
AQS 队列同步器
AQS解决了在实现同步器时涉及的大量细节问题,例如获取同步状态、FIFO同步队列。基于AQS来构建同步器可以带来很多好处:
- 极大的减少实现工作,而且也不必处理在多个位置上发生的竞争问题。
- 在基于AQS构建的同步器中,只能在一个时刻发生阻塞,从而降低上下文切换的开销,提高了吞吐量。
- 同时在设计AQS时充分考虑了可伸缩性。
因此JUC中,所有基于AQS构建的同步器均可以获得这个优势。 AQS的主要使用方式是继承,子类通过继承同步器,并实现它的抽象方法来管理同步状态。AQS使用一个int类型的成员变量state来表示同步状态:
- 1、当state>0时,表示已经获取了锁。
- 2、当state=0时,表示释放了锁。
它提供了三个方法,来对同步状态state进行操作,并且AQS可以确保对state的操作时安全的: getState(); setState(int newState); compareAndSetState(int expect, int update); 另外,AQS通过内置的双向FIFO同步队列来完成资源获取线程的排队工作:
- (1)如果当前线程获取同步状态(锁)失败时,AQS则会将当前线程以及等待状态等信息构造一个节点(Node)并将其加入同步队列,同时会阻塞当前线程。
- (2)当同步状态(锁)释放时,则会把节点中的线程唤醒,使其再次尝试获取同步状态。
一个节点(Node)表示一个线程,它保存着线程的引用(thread)、状态(waitStatus)、前驱节点(prev)、后继节点(next)。入队逻辑实现的addWaiter(Node)方法,需要考虑并发情况。为了解决并发问题,它通过CAS方式,来保证正确的添加Node。
实现阻塞
我们想直接放弃一个线程的时候,可以阻塞、中断这个线程或者stop掉,AQS内部其实是用LockSupport。JDK描述LockSupport为构建锁和其他同步类的基本线程阻塞原语,构建更高级别的同步工具集。LockSupport提供的park/unpark从线程的粒度上进行阻塞和唤醒,park/unpark模型真正解耦了线程之间的同步,线程之间不再需要一个Object或者其它变量来存储状态。
ReentrantLock和Synchronized有什么异同
首先他们肯定具有相同的功能和内存语义。不同之处在于以下几点:
- 1、与synchronized相比,ReentrantLock提供了更多更加全面的功能,具备更强的扩展性。例如时间锁等候,可中断锁等候和锁投票。
- 2、ReentrantLock还提供了条件Condition,对线程的等待唤醒操作更加详细灵活,所以在多个条件变量和高度竞争锁的地方,ReentrantLock更加适合。
- 3、ReentrantLock提供了可轮询的锁请求,它会尝试去获取锁,如果成功则继续,否则等到下次运行时处理,而synchronized则一旦进入锁请求要么成功要么阻塞, 所以相对于synchronized来说,ReentrantLock会不容易死锁些。
- 4、ReentrantLock支持更加灵活的同步代码块,但是使用synchronized时,只能在一个synchronized块结构中获取和释放。
- 5、ReentrantLock支持中断处理,且性能相对好一些
参考博客
深入Lock锁底层原理实现,手写一个可重入锁
万字超强图文讲解AQS以及ReentrantLock应用
面试-并发编程之AQS及可重入锁