关于java中的锁,大家想必十分熟悉。提到锁,大家都会想到,哦,synchronized,wait,sleep,lock,notify 等等等等。当然对一些老鸟来说,这这些关键字或者方法,类都有一定的了解。对于一些新手来说可能只是处于那种不上不下,提到了,知道这么个东西,知道可以防止并发问题。说一个不太好笑的笑话,之前关于锁,我的理解就是synchronized,lock可以加锁,解锁,lock需要自己控制,而synchronized不需要,如果有人问我wait()可以用在lock中么?恐怕我的第一反应就是,为什么不可以?对啊,这不是等待么,加锁之后等待完全没问题啊。是啊没问题,可是就是不能用,【笑哭】,他们都不是一个体制内的,怎么混合使用?你让A公司的主管去命令B公司的员工试试?根本不可能么!
那么为什么不能用呢?我们来深入探讨下。
上一节,我们已经探讨了一下synchronized的使用以及简单的底层实现。知道synchronized是通过monitor来实现对方法,代码块,类等进行加锁,防止资源的抢占。那么wait()呢?其实,wait()也是根据monitor来的。当调用wait()方法时,首先会获取监视器,让线程进入等待队列并释放锁。然后其他线程调用notify或者notifyAll以后,会选择从等待队列中唤醒任意一个线程。而执行完notify方法之后,并不会马上唤醒线程,因为当前线程仍然持有这把锁,处于等待状态的线程无法获得锁,必须要等到当前的线程执行完monitorexit指令之后,也就是被释放之后,处于等待队列的线程就可以开始竞争锁了。
所以,wait()的作用有两个:释放锁和线程进入等待队列,二者都是和监事器相关所以要配合synchronized使用,不能和ReentrantLock混用。
我们来看下ReentrantLock,从定义看,public class ReentrantLock implements Lock, java.io.Serializable只是实现了一个Lock接口,并不是很特别复杂,一般一些类的实现继承了抽象类,然后又实现了几个接口,不同类之间都紧密的连在一起,看着都有些头晕。ReentrantLock 相比而言还是比较友好的哟。
既然是一个对象,我们使用的时候肯定要调用构造方法,从ReentrantLock中来看,构造方法有两个,一个是无参构造方法,一个是有参。
public ReentrantLock() {
// 无参构造方法,默认使用非公平锁
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
// 有参构造方法,定义公平锁与分公平锁
sync = fair ? new FairSync() : new NonfairSync();
}
下面是非公平锁NofairSync中lock的实现,因为是非公平锁,所以上来就进行抢占锁的操作,使用乐 观锁CAS,抢占成功,当前线程就占有资源,否则就加入缓存队列。
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
//进行加锁
final void lock() {
// 非公平,上来直接抢占锁,成功的话获取到所,直接可以执行
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
//尝试获取锁
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
--
我们一步一步来看:点进去compareAndSetState方法,可以看到是进入了AQS(AbstractQueuedSynchronizer)类中的方法unsafe.compareAndSwapInt(this, stateOffset, expect, update); 最终进入了java unsafe包中的cas操作,就是如果stateOffset对应的字段是0的话,改成1,否则失败。那么stateOffset对应的是哪个字段呢?我们点进去看下就知道了。然后大家就看到了这段代码
static { try { stateOffset = unsafe.objectFieldOffset (AbstractQueuedSynchronizer.class.getDeclaredField("state")); headOffset = unsafe.objectFieldOffset (AbstractQueuedSynchronizer.class.getDeclaredField("head")); tailOffset = unsafe.objectFieldOffset (AbstractQueuedSynchronizer.class.getDeclaredField("tail")); waitStatusOffset = unsafe.objectFieldOffset (Node.class.getDeclaredField("waitStatus")); nextOffset = unsafe.objectFieldOffset (Node.class.getDeclaredField("next")); } catch (Exception ex) { throw new Error(ex); } }
也就是一个静态代码块,也就是AQS创建的时候就已经加载的,我们可以看到stateOffset对应的正是state字段!利用的是反射技术。也就是说,CAS操作是把state从0修改为1。
说到这里,我们来看下state,不然后面的看着可能有点晕。我们知道ReentrantLock或者说AQS是用来加锁,防止线程的竞争的,那么这个“锁”来自哪里呢?
其实也就是通过一个状态来控制,这个状态(state)为0,表示没有线程竞争当前资源,大于0表示有线程占用当前资源,每次线程抢占到锁,都会对状态(state)进行加一,(独占锁)其他线程无法抢占该资源。如果释放资源,则状态(state)减一,表示释放资源。当然state可以一直加一,表示可重入,然后释放的时候一直减一,直到资源释放,状态(state)归0。
所以ReentrantLocknt中的加锁,就是讲状态(state)改成1,抢占锁资源。
好了,我们继续。
如果抢占锁成功,当然是将当前线程设置为资源拥有者,一个set操作,不多说了
setExclusiveOwnerThread(Thread.currentThread())
如果抢占锁失败,执行acquire(1);进入这个方法,我们可以看到
if (!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt();
我们一个一个来看;我们点击tryAcquire,进入了AQS中的tryAcquire方法,发现实现是抛出一个不支持的异常,当然这个不是最终的实现,据我从别的资料上看到的说,这个定义为方法而不是接口,主要是方便某些子类集成的时候,如果不需要这个方法,不用再单独实现了,当然如果需要肯定要自己实现。所以,我们点看这个方法的实现,可以看到非公平锁的一个实现。(idea的话,按住ctrl+alt,然后单击就能看到方法的实现);
我们可以看到进了java.util.concurrent.locks.ReentrantLock.NonfairSync#tryAcquire这个方法,具体的实现在nonfairTryAcquire中,下面我们来看下这个方法。
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState(); //获取state,就是控制锁的那个状态,这个是父类的方法
if (c == 0) { //state为0,表示资源空闲,可以进行资源抢占,使用cas乐观锁抢占
//acquires传来的参数是1,也就是把0改1,这个不多说了,看上文
if (compareAndSetState(0, acquires)) {
//抢占锁成功,将当前线程设置为资源拥有者,并返回true,表示加锁成功
setExclusiveOwnerThread(current);
return true;
}
}
// 可重入判断,表示当前线程是资源拥有者,再次加锁处理(锁中锁,连续多个lock)
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires; // state 就加一
if (nextc < 0) // 超出最多限制了,state是int类型,如果超出最大数,会获得负数,一般不会的
throw new Error("Maximum lock count exceeded");
setState(nextc); //把状态设置进去
return true; //再次获取锁(重入锁)成功
}
// 否则获取锁失败
return false;
}
这个方法总的来说就是当前线程先尝试获取锁,不成功的话就判断是否已经拥有锁,然后进行再次加锁(重入),否则就是加锁失败。我们接着看下一个方法acquireQueued(addWaiter(Node.EXCLUSIVE) , acquireQueued(addWaiter(Node.EXCLUSIVE),arg)这个方法ReentrantLock并没有自己实现,而是使用AQS的实现,具体来看下。
先看下 addWaiter(Node.EXCLUSIVE),Node.EXCLUSIVE定义就是null
private Node addWaiter(Node mode) { // mode传来的参数是null
Node node = new Node(Thread.currentThread(), mode); //以当前线程为参数,建立一个节点
Node pred = tail;
//队列尾部不等于空,将当前节点插入队列尾部
if (pred != null) {
node.prev = pred;
// cac操作,插入并返回
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//插入失败,使用一个无限循环进行插入,直到插入成功
enq(node);
return node;
}
// enq 直接复制过来了在这里看下,就是一个自旋处理
private Node enq(final Node node) {
for (;;) { // 一直循环,直到break或者return,下面就是插入队列尾部的一个操作,不多说了
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;
}
}
}
}
结合起来,其实addWaiter(添加等待者)就是当前线程没有抢占到资源,需要去排队等待,于是将当前线程加入到等待队列(双向链表)中。AQS处理锁的思想类似于我们早上买早餐,轮到你了,你就开始做事情(告诉营业员要什么),没轮到就在后面排队,等到买东西的人让出了位置,队伍就短了一点,直到你可以买早餐。
既然加到队列里面去了,那不是就结束了么?怎么还有个方法啊?是啊,正常情况下是结束了,但是突然一个人看了下时间,唉?时间不够了,今天不吃早餐了,然后他就离开了,这时候队伍不是少了一个人么?是不是要更新队列?同理,这个线程的阻塞队列也需要进行更新处理的。
final boolean acquireQueued(final Node node, int arg) { //node是尾节点, arg传来的参数是1,这个我们要心里有数哟 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; // 返回false,线程不中断 } //获取资源失败之后等待,并且中断对线程设置中断标志,等待唤醒 if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt()) interrupted = true; } } finally { if (failed) cancelAcquire(node); } }
又来了几个方法tryAquire(1),shouldParkAfterFailedAcquire(p, node) ,parkAndCheckInterrupt() 一个一个来吧,我们先来看下,tryAquire这个方法有种实现,我们来看下非公平锁的实现(我们说的就是非公平锁哦),就是下面这个方法,呃,上文已经讲过了。
final boolean nonfairTryAcquire(int acquires) { //入参是1要明确 // 代码就不复制过来了,上文有讲解的 }
开始下个方法shouldParkAfterFailedAcquire,一起来看下,汗,看了下源码,又牵涉到新东西了,节点的状态waitStatus,就是说后面既然在等待,那线程就不用进行操作了啊,等占有资源的线程干完活了,再唤醒你不就好了,省的浪费CPU性能,于是,就有了一个状态waitStatus的处理了,这个是AQS里面的,大家有空可以先看下,下次再续,本想简单的分享下,没想到这么长。
感觉还可以的话,给个赞呗~