1.Reentrantlock那些常用的方法
lock()方法
unlock()方法
lockInterruptibly()方法
2.等待队列图解
说起lock方法,其实lock() 和 unlock()相当于synchronize的左花括号和右花括号,但是当我们用到lock()的时候,需要手动的进行再合适的地方unlock()这样的话才能够释放锁,而synchronize到右花括号结束的话就释放锁。
下来我们就看看线程之间是如果争夺这个锁的。
先设一个前提,有三个线程都想要获取同一把锁进行对应的操作,Reentrantlock背后是怎样处理的
public class ReentrantLock implements Lock, java.io.Serializable {
private static final long serialVersionUID = 7373984872572414699L;
private final Sync sync;
public ReentrantLock() {
//无参的初始化默认为非公平同步
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
//对于我们进行初始化的时候
//Sync类继承了AQS类
//FairSync类继承了Sync 为公平锁
//NonfairSync继承了Sync 为非公平锁
sync = fair ? new FairSync() : new NonfairSync();
}
public void lock() {
//
sync.lock();
}
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
public void unlock() {
sync.release(1);
}
}
其实在Reentrantlock中的代码很简单,但是不止我上边所展示的这些。
从上边的代码看。其实不管是这三个方法的哪一个方法,都离不开Sync,其实Sync是Reentrantlock的一个静态内部类。
Sync 继承了 AQS,这说明对于真正Reentrantlock可以做到同步的效果AQS这个类真是功不可没,并且成员sync也是有一定的分量。
剧透:
对于同一个锁来说,也相当于是同一个Reentrantlock对象来说,
这个对象中维持一个等待队列,确切的说是背后的AQS对象维持了一个等待队列,Reentrantlock是依靠AQS的。这个等待队列是一个双向的链表。AQS通过一个 volitile int state 来保证线程之间的同步。
先康康AQS中的主要成员。
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer
implements java.io.Serializable {
private transient volatile Node head;
private transient volatile Node tail;
private volatile int state;
由于AQS类继承了AOS,所以AQS也相当于拥有AOS类的成员。
public abstract class AbstractOwnableSynchronizer implements java.io.Serializable {
private transient Thread exclusiveOwnerThread;
}
插曲 : 公平锁 和 非公平锁
我觉得例子总是让我们清晰易懂。
脑补此时有三个线程,想要获取同一个锁,如果第一个线程抢占到CPU资源,那么第一个线程会通过lock方法首先拥有锁,拥有锁体现为AQS中的int state变量会变为1.
当线程一的时间片段到了,此时线程一并没有运行到unlock方法,那就说明此时的锁没有被释放,同时也体现在AQS中的int state变量没有被更改回0.
此时线程二和线程三依次运行起来,检查AQS中int state的值为1,说明锁被其他线程所持有,那么线程二和三则被加入到AQS维持的等待队列中。
1.公平锁:
如果此时线程一释放了锁,其实释放锁的同时会唤醒等待队列中最老的线程(此时为线程二),此时线程二依旧在等待队列中保存,只有线程二获取到锁,才会从等待队列中删除。
那么此时如果线程一和线程二同时竞争CPU资源,线程一竞争到了cpu资源,又开始获取锁,对于公平锁来说,那当然要维持公平了,所以线程一在获取锁的过程中,程序先检查等待队列中是否有线程正在等待,如果有的话,先看最老的等待线程是否和当前想要获取锁的线程是同一个线程,很明显此时最老线程为线程二,当前线程为线程一,所以会直接将当前的线程一阻塞起来并加入到等待队列中(加入到线程三的后面)。
线程二此时因为已经被唤醒了,只是正等待竞争到cpu然后获取锁,在获取锁后就将自己在等待队列中删除。
公平锁相当于,只要是在等待队列中的线程都有获取锁的机会。
优点:所有的线程都能够得到资源,不会产生饥饿的状态
缺点:吞吐量会下降,因为不管有几个线程进行竞争,只要不是等待队列中的等待的线程,先加入到等待队列中,只允许按照申请锁的顺序进行获取锁,cpu唤醒等待线程的开销会增大
2.非公平锁:
看完上边公平锁,非公平锁也很好理解,当线程一再次想要获取锁的时候,程序不管等待队列的是否还有其他从未获取过锁的线程在等待,只要线程一再次抢占到cpu资源可以再次拥有锁。
此时就必得是谁能先抢占CPU的资源。这样就可能导致等待中的某些线程一直处于等待的状态,甚至没有获得锁的机会。
优点:可以减少CPU唤醒线程的开销,
缺点:会导致有些等待线程饿死的现象,申请的早,但迟迟没有机会获取锁。
下来我们就具体说说这个lock的方法和unlock方法(我拿非公平锁做例子)
先看这个一层层的调用关系,你们可以对照源码比较方便,同时也是展示有用的代码,其他就不说了。
一.lock():
public class ReentrantLock implements Lock, java.io.Serializable {
public void lock() {
sync.lock();
}
这边的sync.lock()调用的是NonfairSync中的lock()方法;NonfairSync是Reentrantlock的静态内部类。
static final class NonfairSync extends Sync {
final void lock() {
//采用CAS操作将AQS中的state变量进行修改为1,表明当前线程已经拥有锁
if (compareAndSetState(0, 1))
//将AQS中的exclusiveOwnerThread变量设置为当前的线程
setExclusiveOwnerThread(Thread.currentThread());
else
//对于如果其他线程持有锁,而其当前的线程也想要获取锁的话
//当前线程会运行到这里,因为AQS中的state状态已经被其他持有锁
//的线程从0修改为1,当前线程只能去尝试获取锁。
acquire(1);
}
}
acquire();
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer
implements java.io.Serializable {
public final void acquire(int arg) {
//tryAcquire方法是由NonfairSync类进行实现的。
//tryAcquire方法中进行AQS中状态的获取,
// 如果是0,表明现在没有任何线程持有锁,那么会将AQS中的状态改为1.并将AQS中的exclusiveOwnerThread设为当前执行的线程。并且返回true。
//如果是1,表明已经有线程持有锁了,那判断AQS中的exclusiveOwnerThread是否是当前线程,如果是当前线程,那此时表明当前线程重入,那么只是将AQS的状态加1,返回true
//如果不是当前运行的线程,那说明当前线程想要获取锁,而此时锁已经被占有那只能返回false。表明当前线程获取锁失败。
if (!tryAcquire(arg) &&//当前线程试图想要通过tryAcquire(arg)获取锁
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
//当tryAcquire(arg)返回值为false时,有必要进行acquireQueued()的判断
//返回false说明获取锁失败,那么就要对此种情况进行处理。
//addWaiter(Node.EXCLUSIVE)方法就是将当前线程形成一个Node节点放入AQS维持等待队列中,返回值为Node。
//在加入到队列的过程中,如果队列为空,表示AQS中的head 和tail的值为null,AQS也是通过head和tail来维持这个等待队列的。
//同时也说明当前线程是第一个加入到等待队列中的,等待队列是一个带头节点的双向链表,
//那么此时会先生成一个空的node作为头节点,然后再将当前线程所形成的节点加入头节点之后,形成这个等待队列.
// acquireQueued()方法将当前的Node节点中的线程,也就是当前线程阻塞起来。
//具体的过程看下面两个方法具体过程
}
}
Node节点主要成员
static final class Node {
static final int CANCELLED = 1;
static final int SIGNAL = -1;
static final int CONDITION = -2;
static final int PROPAGATE = -3;
volatile int waitStatus;
volatile Node prev;
volatile Node next;
volatile Thread thread;
Node nextWaiter;
}
acquireQueued()
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer
implements java.io.Serializable {
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
//p为线程所包装的节点的前驱节点,因为在addwaiter的方法中已经将当前线程
//包装为一个Node几点并且加入到等待队列中了
//p可能为空的头节点,也可能为已经加入到等待队列中的其他线程所构成的节点
final Node p = node.predecessor();
//如果为头节点,表明当前线程是所构成的节点是第一个加入到等待队列中的,此时由机会去获取锁,
//为什么这么说呢,跟排队一样当然是第一个排队的人最可能先得到服务
//这里也一样,如果此时是等待队列中的第一个,那么有机会进行tryAcquire(arg),也就是锁的获取操作,如果成功获取锁,那就说明当前线程所形成的节点不用在等待队里中了,那么会进行一次队列删除操作。
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
//当当前线程获取锁没有成功时,就会进入到下面的判断,
// shouldParkAfterFailedAcquire(p, node)这个方法主要是来判断当前线程是否具备阻塞的条件,
//新节点刚加入的时候是不满足阻塞条件的,那么此时会再次循环
//此时就会跑到上面的if判断,相当于又给了线程一个次获得锁的机会,当for循环第二次到 shouldParkAfterFailedAcquire(p, node)时候,此时当前线程就满足了阻塞条件,
//因为第一次的时候shouldParkAfterFailedAcquire(p, node),将新加入的节点的前驱节点的状态改为SIGNAL,返回false
//第二次的时候检查当前节点节点的前驱节点状态已经为SIGNAL,所以返回true。
//只要当前节点的前驱节点的状态为SIGNAL,就表明当前线程可以进行阻塞。
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
// parkAndCheckInterrupt()方法就是当shouldParkAfterFailedAcquire(p, node)方法返回true的时候就执行,执行的内容就是将当前线程进行阻塞,如果当前线程设置类中断标志,那么当该线程被唤醒的时候返回true
//如果没有设置中断标志,那么该线程被唤醒的时候返回false
//并且当前线程已经形成节点在等待队列中了,就等着其他线程释放锁,在等待队里中国唤醒自己。
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
}
shouldParkAfterFailedAcquire(p, node)
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer
implements java.io.Serializable {
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;//得到当前线程所在节点的前驱节点的状态,
if (ws == Node.SIGNAL)//如果前驱节点的状态为SIGNAL(-1)状态,说明此事该线程的阻塞条件达到,
//因为在之后在释放锁的时候会在等待队列中去唤醒头节点的的后驱节点
//每一个等待节点都有可能变为头节点,在下图会看的更详细
//所以当前节点的前驱节点的状态应该是SIGNAL状态,因为这样在之后能够根据它的前驱节点为SIGNAL而唤醒当前节点。
return true;
if (ws > 0) {//ws大于0表明前驱节点的状态为CANCELLED状态,表明为取消状态
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
//表示前驱节点为状态为0;
//因为等待队列中的最后一个节点状态始终为0;
//所以对新加入到等待队列中的节点来说它的前驱节点为当前队列中的最后一个节点
//对于新加入的节点首次进入shouldParkAfterFailedAcquire方法
//首先通过下面的这个方法将前驱节点的状态设置为SIGNAL状态。
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
二.Unlock方法()
public void unlock() {
sync.release(1);
}
release(int)
public final boolean release(int arg) {
if (tryRelease(arg)) {//进行锁的释放,更改AQS中的state,和拥有锁的线程
Node h = head;
//此时的head就是等待队列的头节点
//每次当一个线程释放自己有的锁的时候,
//总是唤醒该锁等待队列中那个最早等待的,也就是头节点的后驱节点
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
/对于释放锁来说,如果一个线程产生了重入锁的情况那么
下面方法的getstate就不太是1. 可能会是2,3,4…
tryRelease(int)
protected final boolean tryRelease(int releases) {
//getstate得到AQS中的state的值,如果某一线程只获得过一次锁,那么此时该线程想要释放锁的话c = 1 -1 = 0;
//如果之前某一线程产生了重入锁的情况,那么此时getstate不再是1,
//所以对于重入锁的情况来说,现线程重入锁几次,就要释放锁几次,不然在那些等待队列中的线程是无法被唤醒的
//当某一线程只获得过一次锁,那么该线程此时释放锁的的话,c = 0
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
//将AQS中将保存之前持有锁的线程的变量赋值为null。
}
//并且将AQS的state改为c的值
setState(c);
return free;
}
unparkSuccessor(Node)
private void unparkSuccessor(Node node) {
//参数为头节点
int ws = node.waitStatus;
if (ws < 0)//表示头节点的状态为SIGNAL,将头节点的状态改为0
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;//得到头节点的后驱节点
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)//当头节点有后驱节点的时候,那就唤醒后驱节点中的线程
LockSupport.unpark(s.thread);
}
三. lockInterruptibly()
对于lockInterruptibly和lock来说不一样的一点在于lockInterruptibly将lock调用过程的的acquireQueued()方法换成了doAcquireInterruptibly方法。
采用lockInterruptibly方法进行获取锁,表明的是可以处理中断,意思很简单,就是两个线程都想要获取同一个锁,另外一个线程获取了锁,而当前的线程由于没有获取锁而加入到了等待队列,但是如果当前的线程被别的线程设置了Thread.interrupt。
那么此时当前被唤醒的性质就不是说由于某一个线程释放锁而被唤醒,而是因为中断被唤醒,所以在 doAcquireInterruptibly(int arg)方法中if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())成立之后,
那么此时这个线程被唤醒后就直接
throw new InterruptedException();由外部处理,说明当前线程放弃了获取锁。
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 class Interrupt implements Runnable{
private static final Lock lock = new ReentrantLock();
private volatile int selected;
public Interrupt(int select) {
this.selected = select;
}
public void run() {
if(selected == 0) {
try {
lock.lockInterruptibly();
for(int i = 0; i < 1000; i++) {
}
lock.unlock();
}catch (InterruptedException e) {
System.out.println("线程一说:我不想等了");
}
}else if(selected == 1){
try {
lock.lockInterruptibly();
Thread.sleep(1000);
lock.unlock();
ystem.out.println("线程二说: 锁我用完了");
} catch (InterruptedException e) {
}
}
}
public class Test {
public static void main(String[] args) {
Thread one = new Thread(new Interrupt(0));
Thread two = new Thread(new Interrupt(1));
two.start();
one.start();
one.interrupt();
2.等待队列的图解