Lock
锁是用来控制多个线程访问共享资源的方式,一般来说,一个锁能够防止多个线程同时访问共享资源(有些锁可以允许多个线程并发的访问共享资源,比如读写锁)。在Lock出现之前,Java程序是靠synchronized关键字实现锁的功能。Java SE 5之后,并发包中新增了Lock接口用来实现锁功能,它提供了与synchronized关键字类似的同步功 能,只是在使用时需要显式地获取和释放锁。虽然它缺少了(通过synchronized块或者方法所提供的)隐式获取释放锁的便捷性,但是却拥有了锁获取与释放的可操作性、可中断的获取锁以 及超时获取锁等多种synchronized关键字所不具备的同步特性。
使用synchronized关键字将会隐式地获取锁,但是它将锁的获取和释放固化了,也就是先获取再释放。当然,这种方式简化了同步的管理,可是扩展性没有显示的锁获取和释放来的好。例如,针对一个场景,手把手进行锁获取和释放,先获得锁A,然后再获取锁B,当锁B获得 后,释放锁A同时获取锁C,当锁C获得后,再释放B同时获取锁D,以此类推。这种场景下, synchronized关键字就不那么容易实现了,而使用Lock却容易许多。
Lock lock = new ReentrantLock();
lock.lock();
try{ 不要将锁释放写在try代码块中,如果获取锁发生异常,会导致锁无故释放
} finally{
lock.unlock(); 保证获取到锁之后,最终能够释放锁。
}
Lock接口提供的synchronized关键字不具备的主要特性
特性 | 描述 |
---|---|
尝试非阻塞的获取锁 | 当前线程尝试获取锁,如果这一时刻锁没有被其他线程获取到,则成功获取并持有锁 |
能被中断地获取锁 | 与synchronized不同,获取锁的线程能够响应中断,当获取到锁的线程被中断时,中断异常将会被抛出,同时锁会被释放 |
超时获取锁 | 在指定的截至时间之前获取锁,如果截止时间到了仍旧无法获取锁,则返回 |
Lock是一个接口,它定义了锁获取和释放的基本操作
方法名称 | 描述 |
---|---|
void lock() | 获取锁,调用该方法当前线程将会获取锁,当获取锁后,从该方法返回 |
void lockInterruptibly()throws InterruptedException | 可以中断地获取锁,和lock()方法的不同之处在于该方法会响应中断,即在锁的获取中可以中断当前线程 |
boolean tryLock() | 尝试非阻塞的获取锁,调用该方法后立即返回,如果能够获取则返回true,否则返回false |
boolean tryLock(long time,TimeUnit uuit)throws InterruptedException | 超时的获取锁,当线程在一下3种情况下会返回:(1)当前线程在超时时间内获得了锁(2)当前线程在超时时间内被中断(3)超时时间结束,返回false |
void unlock() | 释放锁 |
Condition newCondition() | 获取等待通知组件,该组件和当前的锁绑定,当前线程只有获得了锁,才能调用该组件的wait()方法,而后调用,当前线程释放锁 |
AQS队列同步器
用来构建锁或者其他同步组件的基础框架,它使用了一个int成员变量表示同步状态,通过FIFO队列来完成资源获取线程的排队工作。
重写同步器指定的方法时,需要使用同步器提供如下3个方法访问或修改同步状态
(1)getState():获取当前同步状态
(2)setState(int newState):设置当前同步状态
(3)compareAndSetState(int expect,int update):使用CAS设置当前状态,该方法能够保证状态的原子性。
同步器可重写的方法
方法名称 | 描述 |
---|---|
protected boolean tryAcquire(int arg) | 独占式获取同步状态,实现该方法需要查询当前状态并判断同步状态是否符合预期,然后再进行CAS设置同步状态 |
protected boolean tryRelease(int arg) | 独占式释放同步状态,等待获取同步状态的线程将有机会获取同步状态 |
protected int tryAcquireShared(int arg) | 共享式获取同步状态,返回大于等于0的值,表示获取成功,反之,获取失败 |
protected boolean tryReleaseShared(int arg) | 共享式释放同步状态 |
protected boolean isHeldExclusively() | 当前同步器是否在独占模式下被线程占用,一般该方法表示是否被当前线程所独占 |
同步器提供的模板方法
方法名称 | 描述 |
---|---|
void acquire(int arg) | 独占式获取同步状态,如果当前线程获取同步状态成功,则由该方法返回,否则,将会进入同步队列等待,该方法将会调用重写的tryAcquire(int arg)方法 |
void acquireInterruptibly(int arg) | 与acquire(int arg)相同,但是该方法响应中断,当前线程为获取到同步状态而进入同步队列中,如果当前线程被中断,则该方法会抛出中断异常并返回 |
boolean tryAcquireNanos(int arg,long nanos) | 在acquireInterruptibly基础上增加了超时限制,如果当前线程在超时时间内没有获取到同步状态,那么将返回false |
void acquireShared(int arg) | 共享式的获取同步状态,如果当前线程未获取到同步状态,将会进入同步队列等待,与独占式获取的主要区别时在同一时刻可以有多个线程获取到同步状态 |
void acquireSharedInterruptibly(int arg) | 与acquireShared(int arg),该方法响应中断 |
boolean tryAcquireSharedNanos(int atg,long nanos) | 增加了超时限制 |
boolean release(int arg) | 独占式的释放同步状态,该方法会在释放同步状态之后,将同步队列中第一个节点包含的线程唤醒 |
boolean releaseShared(int arg) | 共享式的释放同步状态 |
Colleaction getQueuedThreads() | 获取等待在同步队列上的线程集合 |
//吐槽,我直接截图不好吗?????????????
同步器提供的模板方法基本上分3类:独占式获取与释放同步状态、共享式获取与释放同步状态和查询同步队列中的等待线程情况。
队列同步器的实现分析
同步队列
同步器依赖内部的同步队列(FIFO双向队列)来完成同步状态的管理,当前线程获取同步状态失败时,同步器会将当前线程以及等待状态等信息构造成为一个节点(Node)并将其加入同步队列,同时会阻塞当前线程,当同步状态释放时,会把首节点的线程唤醒,使其再次尝试获取同步状态。
节点时构成同步队列(等待队列)的基础,同步器拥有首节点(head)和尾节点(tail),没有成功获取同步状态(锁)的线程会把这个节点添加到队尾。同步器提供了一个CAS设置尾节点的方法:compareAndSetTail(Node expect,Node update),它需要传递当前线程"认为"的尾节点和当前节点,只有设置成功后,当前节点才正式与之前的尾节点建立连接。
独占式同步状态获取与释放
通过调用同步器的acquire(int arg)方法可以获取同步状态,该方法对中断不敏感,也就是由于线程获取同步状态失败后进入同步队列中,后续对线程进行中断操作时,线程不会从同步队列中移出。
public final void acquire(int arg) {
if (!tryAcquire(arg) && 保证线程安全的获取同步状态,如果同步状态获取失败
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) 把这个节点重新添加到队列尾部
selfInterrupt();
}
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode); 将当前线程赋值给一个新的node
// Try the fast path of enq; backup to full enq on failure
Node pred = tail; 将队尾的赋值给pred
if (pred != null) { 如果队尾元素不为空,则这个没有拿到锁的node的前驱指向队尾的node
node.prev = pred;
if (compareAndSetTail(pred, node)) { 调用CAS保证节点能够被线程安全添加
pred.next = node;
return node;
}
}
enq(node); 队列为空时执行enq
return node;
}
private Node enq(final Node node) {
for (;;) { 死循环
Node t = tail;
if (t == null) { // Must initialize 队列为空就新建一个线程
if (compareAndSetHead(new Node())) 通过CAS将节点设置成尾节点
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
节点进入同步队列之后,就进入了一个自旋的过程,每个节点(或者说每个线程)都在自省地观察,当条件满足,获取到了同步状态,就可以从这个自旋过程中退出,否则依旧留在这个自旋过程中(并会阻塞节点的线程)。
队列的头节点作为参数node
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);
}
}
在acquireQueued(final Node node,int arg)方法中,当前线程在“死循环”中尝试获取同步状态,而只有前驱节点是头节点才能获取同步状态,原因有两个
(1)头节点是成功获取到同步状态的节点,而头节点的线程释放了同步状态之后,将会唤醒其后继节点,后继节点的线程被唤醒后需要检查自己的前驱节点是否为头节点。
(2)维护同步队列的FIFO原则。该方法,节点自旋获取同步状态的行为
由于非首节点但线程前驱节点出队列或者被中断而从等待状态返回,随后检查自己的前驱是否为头节点,如果是则尝试获取同步状态。节点和节点之间在循环检查的过程中基本不相互通信,而是简单地判断自己地前驱是否为头节点,这样就是使得节点地释放规则符合FIFO,并且也便于过早通知地处理(过早通知是指前驱节点不是头节点地线程由于中断而被唤醒)
独占式同步状态获取流程,acquier(int arg)方法调用流程
当前同步器队列中的头节点(线程)会通过CAS(自旋)获取锁,当前线程获取同步状态并执行了相应逻辑之后就需要释放同步状态,使得后续节点能够继续获取同步状态。通过调用同步器的release(int asg)方法可以释放同步状态,该方法释放了同步状态之后,会唤醒其后继节点(进而使后继节点重新尝试获取同步状态)
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h); 来唤醒处于等待状态的线程
return true;
}
return false;
}
独占式同步状态获取和释放过程总结
在获取同步状态时,同步器维护了一个同步队列,获取同步状态失败的线程会同步CAS(自旋)重新插入到同步器的队尾;出队列的条件是前驱节点为头节点且成功获取了锁。在释放同步状态,同步器会将头节点的后继节点唤醒。
共享式同步状态获取与释放
共享式获取与独占式获取最主要的区别在于同一时刻能否有多个线程同时获取到同步状态。以文件读写为例,如果一个线程在对文件进行读写操作,那么这一时刻对于该文件的写操作均被阻塞,而读操作可以进行。写操作要求对资源的独占式访问,而读操作可以是共享式访问。
同步器的acquireShared(int arg)方法可以共享式地获取同步状态
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0) 没有获取到同步状态,调用doAcquireShared
doAcquireShared(arg);
}
private void doAcquireShared(int arg) {
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
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)) { 通过CAS算法保证队尾元素能够正确的插入队列
pred.next = node;
return node;
}
}
enq(node); 队尾元素为空,就调用enq重新创建一个线程设置成队尾元素
return node;
}
同步器调用用tryAcquireShared(int arg)方法尝试获取同步状态,tryAcquireShared(int arg)方法返回值为int类型,当返回值大于等于0时,表示能够获取到同步状态。
因此,在共享式获取的自旋过程中,成功获取到同步状态并退出自旋的条件就是: tryAcquireShared(int arg)方法返回值大于等于0。
可以看到,在doAcquireShared(int arg)方法的自旋过程中,如果当前节点的前驱为头节点时,尝试获取同步状态,如果返回值大于等于0,表示该次获取同步状态成功并从自旋过程中退出。
共享式获取也需要释放同步状态
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) { 释放成功
doReleaseShared(); 唤醒后续处于等待状态的节点
return true;
}
return false;
}
独占式超时获取同步状态
通过调用同步器的doAcquireNanos(int arg,long nanosTimeout)方法可以超时获取同步状 态,即在指定的时间段内获取同步状态,如果获取到同步状态则返回true,否则,返回false。该 方法提供了传统Java同步操作(比如synchronized关键字)所不具备的特性。
private boolean doAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (nanosTimeout <= 0L)
return false;
final long deadline = System.nanoTime() + nanosTimeout;
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 true;
}
nanosTimeout = deadline - System.nanoTime();
if (nanosTimeout <= 0L)
return false;
if (shouldParkAfterFailedAcquire(p, node) &&
nanosTimeout > spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanosTimeout);
if (Thread.interrupted())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}