生活中常见的锁--取款机(独占锁)
假设有一个取款机,所有的取钱的人都要进去取款,因此取款机相当于是被锁的代码块,一次只能有一个人进去,进去之后会将门上一个锁。门拥有的锁的一个数量 记为state。门打开是state为0,关闭的state是1
当A想要进去之后 ,他会对比门锁的状态是否和他期望的状态一致(期望state为0) 如果与他预期的一致,他将会进去并且把门上锁(state由0->1)。在上锁的时候我们必须保证是原子性的。因此用到CAS。
这个时候又有一个人B想要进去,但是他发现门上锁的数量与他期望的不一致(期望是0但是实际是1),这个时候他假设A可能会很快出来,因此他便在门口徘徊 (自旋),但是徘徊了很多次之后A仍然没有出来。
这个时候又来了很多很多人,他们都想去取钱,但是都获取不到锁,因此都在门口徘徊,人多时就会造成拥堵了交通之类的(线程过多会导致cpu的压力增大,甚至崩溃), 这个时候我们可以用到银行排号的方式,让所有人停止徘徊(线程被阻塞),都来拿一个号坐在那里等(等待队列)。
锁的基石
通过上面的讲解,我们大概了解实现一个锁需要什么了
1,记录锁的变量:state
2,获取锁的原子操作:CAS
3,自旋
4,阻塞和唤醒线程:SupportLock
5,存储线程的队列:双向链表
通过上面的一个流程我们大致可以写出以下的简易代码
public class AQS {
private volatile int state = 0;
private volatile BlockingQueue<Thread> threads = new ArrayBlockingQueue<Thread>(10);
public void lock() {
Thread current = Thread.currentThread();
for(;;) {
//CAS保证整个操作的原子性
if(compareAndSetState(0, 1)) {
//假设进入的线程获取了锁 , 跳出自旋的循环
break;
}
//假设线程没有获取锁,释放线程资源
LockSupport.park();
//将释放了的线程放入队列里面 , 对所有的线程做一个记录,以防将来唤醒线程时找不到
threads.add(current);
}
}
public void release() {
if(compareAndSetState(1, 0)) {
//释放锁成功之后, 唤醒队列的第一个线程
LockSupport.unpark(threads.poll());
}
}
protected final boolean compareAndSetState(int expect, int update) {
// See below for intrinsics setup to support this
// return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
return true;
}
}
通过以上代码,我们对代码流程有了一个基础的认识,但是就上面的代码是远远不够的 ,因此接下来我们来结合ReentrantLock看一看源码
ReentrantLock源码
为了让我们对执行流程有一个更清晰的认识,因此先上流程图
在ReentrantLock 里面有三个内部类
1,Sync 继承了 AbstractQueuedSynchronizer (大部分锁的基石),
2,NonfairSync 以及 fairSync继承了Sync。
本篇文章主要讲非公平锁(NonfairSync ),
在ReentrantLock 的注释上面,我们可以看到 使用范式如下所示。但是为什么lock.lock() 不能放在try里面呢?其实主要是为了防止释放的别的线程获取的锁
* class X {
* private final ReentrantLock lock = new ReentrantLock();
* // ...
*
* public void m() {
* lock.lock(); // block until condition holds
* try {
* // ... method body
* } finally {
* lock.unlock()
* }
* }
获取锁
public ReentrantLock() {
// 默认实现的是非公平锁
sync = new NonfairSync();
}
public void lock() {
sync.lock();
}
final void lock() {
if (compareAndSetState(0, 1))
// 获取到锁,并记录当前拥有的锁的线程
setExclusiveOwnerThread(Thread.currentThread());
else
// 未获取到锁,接下来会通过自旋的方式尝试获取,
acquire(1);
}
通过上面 部分的代码,得到的结果有两种 , 一种是获取到锁, 然后结束了流程,另外一种是并没有获取到锁,接下来会执行 acquire(1)继续尝试获取锁。
// 未获取到锁之后执行的流程
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
// 第二次尝试获取锁
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
// 如果此时恰好拥有锁的线程释放了,可以直接获取锁。
// 公平锁与非公平锁的区别就在这儿,公平锁在检测到锁被释放了之后,
//仍然需要到队列排队,而非公平锁无需排队,直接抢锁的使用权
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//ReentrantLock是一个可重入的锁,如果当前线程与获取锁的线程是同一个,那么可以可重入的获取锁
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;
}
//如果再次获取锁失败,则将线程加入队列里面
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//如果 线程插入队列失败,则通过自旋的方式插入
enq(node);
return node;
}
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
通过上面的代码 ,初始未获取锁的线程也可以得到两种结果,
1,重试获取锁成功
2,已经插入队列。
对插入队列里面的线程,我们可以通过自旋的方式再次让它尝试去获取锁,如果获取不到就阻塞当前线程。
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
//通过自旋的方式 尝试获取锁失败之后,检查是否需要阻塞
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
//一直没有获取到锁,取消线程获取锁的尝试,
cancelAcquire(node);
}
}
//阻塞线程
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
到这里,获取锁的代码就讲完了,接下来看释放锁.
释放锁
释放锁的过程比较简单,通过改变 state状态,以及拥有锁的线程实现
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}