在了解如何加锁时候,我们再来了解如何解锁。可重入互斥锁ReentrantLock的解锁方法unlock()并不区分是公平锁还是非公平锁,Sync类并没有实现release(int arg)方法,这里会实现调用其父类AbstractQueuedSynchronizer的release(int arg)方法。在release(int arg)方法中,会先调用其子类实现的tryRelease(int arg)方法,这里我们看看ReentrantLock.Sync.tryRelease(int releases)的实现。
正常情况下,这段代码只能由占有锁的线程调用,所以这里会先获取锁的引用计数,再减去释放次数,即:c = getState() - releases,然后判断尝试释放锁的线程,是否是独占锁的线程,如果不是则抛出IllegalMonitorStateException异常。如果独占线程在释放锁后锁的引用计数为0,则设置独占线程为null,再设置state为0,代表锁成为无主状态。
如果确定锁成为无主状态后,release(int arg)会检查队列中是否有需要唤醒的线程,如果头节点header为null,则代表除了释放锁的线程,没有任何线程抢锁,如果头节点的等待状态为0,代表头节点目前没有需要唤醒的后继节点。如果头节点不为null且等待状态不为0,则代表队列中可能存在需要唤醒的线程,会进而执行unparkSuccessor(Node node),将头节点作为参数传入。
当我们将头节点传入unparkSuccessor(Node node),如果判断头节点的等待状态<0,则会将头节点的等待状态设置为0,如果头节点的后继节点不为null,或者后继节点尚未被取消,会直接唤醒后继节点,后继节点会退出parkAndCheckInterrupt()方法,acquireQueued(final Node node, int arg)的循环中重新竞争锁,如果竞争成功,则后继节点成为头节点,退出抢锁逻辑开始访问资源,如果竞争失败,这里最多循环两次执行shouldParkAfterFailedAcquire(Node pred, Node node),第一次先将后继节点的前驱节点的等待状态由0改为SIGNAL(1),表示前驱节点的后继节点处于等待唤醒状态,第二次循环如果还是抢锁失败,shouldParkAfterFailedAcquire(Node pred, Node node)判断前驱节点的等待状态为SIGNAL,返回true,后继节点的线程陷入阻塞。需要注意的是,即便是公平锁,也可能存在不公平的情况,公平锁头节点的后继节点,也有可能存在抢锁失败的情况,比如之前说过,公平锁在调用tryLock()时是不保证公平的。
这里我们需要注意一下,后继节点是可能存在被移除的情况,这种情况会在后续讲解tryLock(long timeout, TimeUnit unit)的时候说明,如果一旦出现后继节点被移除的情况(waitStatus > 0),在唤醒后继节点时,会从尾节点向前驱节点遍历,找到最靠近头节点的有效节点。

public class ReentrantLock implements Lock, java.io.Serializable {
//...
private final Sync sync;
//...
abstract static class Sync extends AbstractQueuedSynchronizer {
//...
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;
}
//...
}
//...
public void unlock() {
sync.release(1);
}
//...
} public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
//...
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 boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}
//...
private void unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
*/
int ws = node.waitStatus;
if (ws < 0)
node.compareAndSetWaitStatus(ws, 0); /*
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
*/
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
//如果后继节点不为null但被移除,会从尾节点向前驱节点遍历,找到最靠前的有效节点
for (Node p = tail; p != node && p != null; p = p.prev)
if (p.waitStatus <= 0)
s = p;
}
if (s != null)
LockSupport.unpark(s.thread);
}
//...
}

  

可重入互斥锁的tryLock()方法不分公平锁或非公平锁,统一调用ReentrantLock.Sync.nonfairTryAcquire(int acquires),在JUC长征篇之ReentrantLock源码解析(一)已经介绍过nonfairTryAcquire(int acquires)方法了,这里就不再赘述了。

public class ReentrantLock implements Lock, java.io.Serializable {
//...
private final Sync sync;
//...
abstract static class Sync extends AbstractQueuedSynchronizer {
//...
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
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;
}
//...
}
//...
public boolean tryLock() {
return sync.nonfairTryAcquire(1);
}
//...
}

  

调用ReentrantLock.tryLock(long timeout, TimeUnit unit)方法时,会调用Sync父类AQS的tryAcquireNanos(int arg, long nanosTimeout)方法,如果线程已被标记为中断,则会进入<1>处的分支并抛出InterruptedException异常,否则先调用tryAcquire(int acquires)尝试抢夺,如果抢锁失败,则会调用doAcquireNanos(int arg, long nanosTimeout)陷入计时阻塞,如果在阻塞期间内线程被中断,则会抛出InterruptedException异常,如果到达有效期后线程还未获得锁,tryAcquireNanos(int arg, long nanosTimeout)将返回false。
当进入doAcquireNanos(int arg, long nanosTimeout)后,会先在<2>处计算获取锁的截止时间,之后和原先的acquireQueued()很像,先把当前线程封装成一个Node对象并加入到等待队列,再判断当前节点的前驱节点是否是头节点,是的话再尝试抢锁,如果抢锁成功则退出。否则用截止时间减去系统当前时间,算出线程剩余的抢锁时间(nanosTimeout)。如果剩余时间<=0的话,代表到达截止时间后线程依旧未占用锁,于是调用<3>处的代表将线程对应的Node节点从等待队列中移除,返回false表示抢锁失败。如果在后面的循环中如果发现当前时间大于截止时间,而线程还未获得锁,代表抢锁失败。
如果剩余时间大于0,则会调用shouldParkAfterFailedAcquire(),如果前驱节点的状态状态为0,则将前驱节点的等待状态设置为-1表示其后继节点等待唤醒,然后在下一次循环的时候,shouldParkAfterFailedAcquire()判断前置节点的等待状态为-1,就有机会阻塞当前线程。但相比acquireQueued()不同的是,这里会判断剩余时间是否大于1000纳秒,如果剩余时间小于等于1000纳秒的话,这里就不会阻塞线程,而是用自旋的方式,直到抢锁成功,或者锁超时抢锁失败。如果自旋期间或者阻塞期间线程被中断,则会在<6>处抛出InterruptedException异常。

public class ReentrantLock implements Lock, java.io.Serializable {
//...
private final Sync sync;
//...
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
//...
} public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
static final long SPIN_FOR_TIMEOUT_THRESHOLD = 1000L;
//...
public final boolean tryAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (Thread.interrupted())//<1>
throw new InterruptedException();
return tryAcquire(arg) ||
doAcquireNanos(arg, nanosTimeout);
}
//...
private boolean doAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (nanosTimeout <= 0L)
return false;
final long deadline = System.nanoTime() + nanosTimeout;//<2>
final Node node = addWaiter(Node.EXCLUSIVE);
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
return true;
}
nanosTimeout = deadline - System.nanoTime();
if (nanosTimeout <= 0L) {
cancelAcquire(node);//<3>
return false;
}
if (shouldParkAfterFailedAcquire(p, node) &&//<4>
nanosTimeout > SPIN_FOR_TIMEOUT_THRESHOLD)//<5>
LockSupport.parkNanos(this, nanosTimeout);
if (Thread.interrupted())
throw new InterruptedException();//<6>
}
} catch (Throwable t) {
cancelAcquire(node);
throw t;
}
}
//...
}

  

下面,我们来看看AQS是如何移除一个节点的,当要移除一个节点时,会先置空它的线程引用,再在<1>处遍历当前节点的前驱节点,直到找到有效节点(等待状态<=0),找到最靠近当前节点的有效节点pred后,再找到有效节点的后继节点predNext(pred.next),然后我们将当前节点的等待状态置为CANCELLED(1),如果当前节点是尾节点,则用CAS的方式设置尾节点为有效节点pred,如果pred成功设置为为节点,这里还会用CAS的方式设置pred的后继为null,因为尾节点不应该有后继节点,这里用CAS设置尾节点的后继节点是防止pred成为尾节点后,还未置空尾节点的后继节点前,又有新的节点入队成为新的尾节点,并且设置pred的后继节点为最新的尾节点。如果<2>出的代码执行成功,代表当前没有新的节点入队,如果执行失败,代表有新的节点入队,pred已经不是尾节点,且pred的后继已经被修改。
如果node不是尾节点,或者在执行compareAndSetTail(node, pred)的时候,有新节点入队,tail引用指向的对象已经不是node本身,或者在执行compareAndSetTail()执行失败,则会进入<3>处的分支。进入<3>处的分支后,会先判断当前节点的前驱节点是不是头节点,如果是头节点的话,会进入<5>处的分支,唤醒当前节点的后继节点来竞争锁,如果当前节点的后继节点为null或者被取消的话,则会从尾节点开始遍历前驱节点,找到队列最前的有效节点,唤醒有效节点竞争锁。
如果前驱节点不是头节点,且前驱节点的等待状态为SIGNAL或者前驱节点的等待状态为有效状态(waitStatus<=0)且成功设置前驱节点的等待状态为SIGNAL,再判断前驱节点前驱节点的thread引用是否为null,如果不为null则代表前驱节点还不是头节点或者尚未被取消,此时就可以进入<4>处的分支,这里如果判断当前节点的后继节点不为null或者尚未被取消,则用CAS的方式设置前驱节点的后继节点,为当前节点的后继节点。

public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
//...
private void cancelAcquire(Node node) {
// Ignore if node doesn't exist
if (node == null)
return; node.thread = null; // Skip cancelled predecessors
Node pred = node.prev;
while (pred.waitStatus > 0)//<1>
node.prev = pred = pred.prev; // predNext is the apparent node to unsplice. CASes below will
// fail if not, in which case, we lost race vs another cancel
// or signal, so no further action is necessary, although with
// a possibility that a cancelled node may transiently remain
// reachable.
Node predNext = pred.next; // Can use unconditional write instead of CAS here.
// After this atomic step, other Nodes can skip past us.
// Before, we are free of interference from other threads.
node.waitStatus = Node.CANCELLED; // If we are the tail, remove ourselves.
if (node == tail && compareAndSetTail(node, pred)) {
pred.compareAndSetNext(predNext, null);//<2>
} else {//<3>
// If successor needs signal, try to set pred's next-link
// so it will get one. Otherwise wake it up to propagate.
int ws;
if (pred != head &&
((ws = pred.waitStatus) == Node.SIGNAL ||
(ws <= 0 && pred.compareAndSetWaitStatus(ws, Node.SIGNAL))) &&
pred.thread != null) {//<4>
Node next = node.next;
if (next != null && next.waitStatus <= 0)
pred.compareAndSetNext(predNext, next);
} else {//<5>
unparkSuccessor(node);
} node.next = node; // help GC
}
}
//...
}

  

先前我们说过,会存在后继节点指向被取消节点的情况,就是发生在cancelAcquire(Node node)方法里,下图是一个锁的等待队列,N1是队头,N1所对应的线程T1正占有锁进行资源访问,N2和N5调用lock()方法采用非计时阻塞请求锁,除非N2和N5对应的线程获取到锁,否则将永远阻塞;N3和N4调用tryLock(long timeout, TimeUnit unit)方法采用计时阻塞请求锁,如果超时对应的线程还未获取到锁,N3和N4将会从队列中移除,返回抢锁失败。

我们假定N3和N4已经超时,要从队列中移除,看看并发场景下是如何出现有效节点的后继引用指向无效节点,这里笔者稍微简化cancelAcquire(Node node),我们只要专注可能出现有效节点指向无效节点的代码。

假定N3和N4两个节点对应的线程是T3和T4,T3要从等待队列中移除N3,先获取N3的前驱节点pred(N3)为N2,N2是一个有效节点(waitStatus<=0),所以不需要在<1>处遍历,接着在<2>处获取N2的后继节点predNext(N2)为N3,再将N3的等待状态改为CANCELLED,此时T3挂起,T4开始执行。T4线程同样获取N4前驱节点pred(n4)为N3,然后发现N3的等待状态>0,会一直往前遍历到N2,所以N4的前驱节点pred(N4)会为N2,接着T4执行<2>处的代码,predNext(N2)也是N3,此时T4挂起,T3恢复执行。T3判断N3不是尾节点,于是进入分值<4>,T3判断N3的前驱节点N2不是头节点,N2的状态为为SIGNAL,且N2的thread字段不为空,表明N2既没有被取消,也不是头节点,于是进入<5>处的分值,这里获取N3的后继节点N4,由于N4对应的线程T4尚未执行到<3>处的代码,N4的等待状态依旧为SIGNAL,所以T3会进入<6>处的分支,将N2的后继节点指向N4。此时T4开始执行,将N4的等待状态改为CANCELLED,T4进行<4>处的分支,N4的前驱N2不是头节点,等待状态为SIGNAL,且N2的线程引用不为怒null,继而进入<5>处的分值,获取到N4的后继N5,N5不Wie空,且N5的等待状态<=0,进而进入到<6>分支,最后要用CAS的方式尝试将N2的后继节点设置为N5,但这里的设置一定会失败,因为此时N2的后继节点为N4,而T4原先获取到N2的后继节点为N3,出现了有效节点指向无效节点的情况。

private void cancelAcquire(Node node) {
//...
Node pred = node.prev;
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;//<1>
Node predNext = pred.next;//<2>
node.waitStatus = Node.CANCELLED;//<3>
if (node == tail && compareAndSetTail(node, pred)) {
//...
} else {//<4>
// If successor needs signal, try to set pred's next-link
// so it will get one. Otherwise wake it up to propagate.
int ws;
if (pred != head &&
((ws = pred.waitStatus) == Node.SIGNAL ||
(ws <= 0 && pred.compareAndSetWaitStatus(ws, Node.SIGNAL))) &&
pred.thread != null) {//<5>
Node next = node.next;
if (next != null && next.waitStatus <= 0)//<6>
pred.compareAndSetNext(predNext, next);
} else {
//...
}
//...
}
}

  

最后等待队列的布局如下所示,N2指向无效节点N4,而非N4的后继节点。同时这里也引出另一个问题,哪怕N4不是无效节点,在上面移除节点的代码中,只设置了N2的后继引用指向N4,却没设置N4的前驱引用指向N2,所以这里N4的前驱依旧指向N3。

那么如果真的出现上述这样的情况,ReentrantLock是如何来修复这个队列呢?答案在释放锁的时候调用unparkSuccessor(Node node)。笔者先前在介绍这个方法时,就有意提到后继节点可能指向无效节点,当N1对应的线程使用完锁释放之后,N2对应的线程T2接着使用锁并释放锁,在N2释放锁的时候,发现N2的后继节点已经成为失效节点(waitStatus > 0),这里会从尾节点开始找到队列中最前面的有效节点,然后将其唤醒,这里也就是N5。

private void unparkSuccessor(Node node) {
//...
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node p = tail; p != node && p != null; p = p.prev)
if (p.waitStatus <= 0)
s = p;
}
if (s != null)
LockSupport.unpark(s.thread);
}

  

N5在被唤醒后,会调用shouldParkAfterFailedAcquire(Node pred, Node node)发现原先的前驱节点N4的等待状态处于被移除,会进入<1>处的分支,查找到最靠近自己的有效前驱节点,并将前驱节点的后继节点指向自己。这里也能回答我们先前的问题,为何在要移除一个节点时,只修改前驱节点的后继引用为被移除节点的后继,却不将被移除节点的后继节点的前驱引用,指向其前驱,因为在释放锁的时候,会唤醒头节点的后继,如果被唤醒的后继发现自己的前驱已经被移除,会往前查找最靠近自己的有效前驱,这里一般是头节点,接着将头节点的后继引用指向自己,再往后就是我们熟悉的流程了,如果前驱节点是头节点且抢锁成功,则退出acquireQueued()方法进行资源的访问,如果抢锁失败,则最多执行两次shouldParkAfterFailedAcquire()然后陷入阻塞,等待下一次的唤醒。

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
//...
if (ws > 0) {//<1>
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
//...
}
return false;
}

  

我们已经了解了lcok()、tryLock()以及tryLock(long timeout, TimeUnit unit),下面的lockInterruptibly()就变得尤为简单,lockInterruptibly()顾名思义也是无限期陷入阻塞,直到获得锁或者被中断,如果线程本身被中断,在抢锁时会直接抛出InterruptedException异常,否则就开始抢锁,如果抢锁成功则皆大欢喜,抢锁失败则执行doAcquireInterruptibly(int arg),这个方法相信不需要笔者做过多的介绍,基本上很多步骤和方法在上面已经介绍过了。将线程封装成Node节点并入队,判断Node节点的前驱是否是头节点,是的话则试图抢锁,如果不是的话则最多循环两次执行shouldParkAfterFailedAcquire(),然后陷入阻塞,直到前驱节点成为头节点并释放锁将其唤醒,或者线程被中断,抛出InterruptedException异常。

public class ReentrantLock implements Lock, java.io.Serializable {
//...
private final Sync sync;
//...
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
//..
} public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
//...
public final void acquireInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (!tryAcquire(arg))
doAcquireInterruptibly(arg);
}
//...
private void doAcquireInterruptibly(int arg)
throws InterruptedException {
final Node node = addWaiter(Node.EXCLUSIVE);
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
return;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
} catch (Throwable t) {
cancelAcquire(node);
throw t;
}
}
//...
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
//...
}

  

最后,笔者就简单介绍下公平锁(FairSync)的tryAcquire(int acquires)方法,如果我们查看ReentrantLock的源码会发现,在执行lock()、tryLock(long timeout, TimeUnit unit)和lockInterruptibly(),这几个方法最终都会调用到AQS对应的三个方法:acquire(int arg)、tryAcquireNanos(int arg, long nanosTimeout)、acquireInterruptibly(int arg),这三个方法在抢锁的时候会优先执行子类实现的tryAcquire(int arg)方法,也就是公平锁(FairSync)或者非公平锁(NonfairSync)实现的tryAcquire(int arg)方法,抢锁失败再执行AQS自身实现的入队、阻塞方法。

下面,我们来看下公平锁实现的tryAcquire(int acquires)方法,其实这个方法的实现也非常简单,先判断锁目前是否处于无主状态,是的话再判断队列中是否有等待线程,确认锁是无主状态且队列中没有等待线程,便开始尝试抢锁,抢锁成功则直接返回。公平锁相较于非公平锁的抢锁逻辑,也仅仅是多了一步而已,判断锁是否无主,是的话再判断队列中是否有等待线程,不像非公平锁,只要判断是无主线程便不再查看等待队列,直接尝试抢锁。

    static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
/**
* Fair version of tryAcquire. Don't grant access unless
* recursive call or no waiters or is first.
*/
@ReservedStackAccess
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
//如果state为0,且队列中没有等待线程,则尝试抢锁
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;
}
}

  

Java并发之ReentrantLock源码解析(二)的更多相关文章

  1. Java并发之ReentrantLock源码解析(四)

    Condition 在上一章中,我们大概了解了Condition的使用,下面我们来看看Condition再juc的实现.juc下Condition本质上是一个接口,它只定义了这个接口的使用方式,具体的 ...

  2. Java并发之ReentrantLock源码解析(三)

    ReentrantLock和BlockingQueue 首先,看到这个标题,不要怀疑自己进错文章,也不要怀疑笔者写错,哈哈.本章笔者会从BlockingQueue(阻塞队列)的角度,看看juc包下的阻 ...

  3. Java并发之ReentrantLock源码解析(一)

    ReentrantLock ReentrantLock是一种可重入的互斥锁,它的行为和作用与关键字synchronized有些类似,在并发场景下可以让多个线程按照一定的顺序访问同一资源.相比synch ...

  4. Java并发之Semaphore源码解析(二)

    在上一章,我们学习了信号量(Semaphore)是如何请求许可证的,下面我们来看看要如何归还许可证. 可以看到当我们要归还许可证时,不论是调用release()或是release(int permit ...

  5. Java并发之Semaphore源码解析(一)

    Semaphore 前情提要:在学习本章前,需要先了解笔者先前讲解过的ReentrantLock源码解析,ReentrantLock源码解析里介绍的方法有很多是本章的铺垫.下面,我们进入本章正题Sem ...

  6. Java并发之ReentrantReadWriteLock源码解析(一)

    ReentrantReadWriteLock 前情提要:在学习本章前,需要先了解笔者先前讲解过的ReentrantLock源码解析和Semaphore源码解析,这两章介绍了很多方法都是本章的铺垫.下面 ...

  7. Java并发之ThreadPoolExecutor源码解析(二)

    ThreadPoolExecutor ThreadPoolExecutor是ExecutorService的一种实现,可以用若干已经池化的线程执行被提交的任务.使用线程池可以帮助我们限定和整合程序资源 ...

  8. Java并发之ReentrantReadWriteLock源码解析(二)

    先前,笔者和大家一起了解了ReentrantReadWriteLock的写锁实现,其实写锁本身实现的逻辑很少,基本上还是复用AQS内部的等待队列思想.下面,我们来看看ReentrantReadWrit ...

  9. Java并发之ThreadPoolExecutor源码解析(三)

    Worker 先前,笔者讲解到ThreadPoolExecutor.addWorker(Runnable firstTask, boolean core),在这个方法中工作线程可能创建成功,也可能创建 ...

随机推荐

  1. 调用免费API查询全年工作日、周末、法定节假日、节假日调休补班数据

    前言 日常开发中,难免会用到判断今天是工作日.周末.法定节假日.节假日调休补班做一些业务处理,例如:仅在上班时间给用户推送消息.本文记录调用免费API查询全年工作日.周末.法定节假日.节假日调休补班数 ...

  2. DOM 绑定事件

    // 1.获取事件源 var oDiv = document.getElementById('box'); console.log(oDiv); //2.事件 (1)直接绑定匿名函数 oDiv.onc ...

  3. Let's go!

    第一次开通博客 心情还是很激动的,而且做出了这么好看的页面虽然都是用的别人的组件,自己不是很知道原理但是也很开心,以后会将自己学习的东西写成笔记发在上面

  4. NumPy之:ndarray中的函数

    NumPy之:ndarray中的函数 目录 简介 简单函数 矢量化数组运算 条件逻辑表达式 统计方法 布尔数组 排序 文件 线性代数 随机数 简介 在NumPy中,多维数组除了基本的算数运算之外,还内 ...

  5. Linux(深度)系统安装富士施乐(网络)打印机

    一般来讲,linux系统识别打印机没有问题,重点难点在于后面设置.此文特别感谢:河北石龙的陈一繁销售代表.P288dw施乐官网并未提供Linux的驱动并在安装过程中遇到很多问题,其不厌其烦的为我联系厂 ...

  6. 5Spring动态代理开发小结

    5Spring动态代理开发小结 1.为什么要有动态代理? 好处 1.利于程序维护 2.利于原始类功能的增强 3.得益于JDK或者CGlib等动态代理技术使得程序扩展性很强 为什么说使得程序扩展性很强? ...

  7. 持续集成和持续交付工具-jenkins

    jenkins说明 jenkins是一款由Java编写的开源的持续集成工具,它运行在Servlet容器中(例如Apache Tomcat).它支持软件配置管理(SCM)工具(包括AccuRev SCM ...

  8. CentOS、RHEL、Asianux、Neokylin、湖南麒麟、BC Linux、普华、EulerOS请参考“1.1 CentOS本地源配置”;

      本文档适用于CentOS.RHEL.Asianux.Neokylin.湖南麒麟.BC Linux.普华.EulerOS.SLES.Ubuntu.Deepin.银河麒麟. CentOS.RHEL.A ...

  9. 详解Linux中的cat文本输出命令用法

    作系统 > LINUX >   详解Linux中的cat文本输出命令用法 Linux命令手册   发布时间:2016-01-14 14:14:35   作者:张映    我要评论   这篇 ...

  10. :整数 跳转到该行 Vim中常用的命令

    :set nu 显示行号 :set nonu 不显示行号 :命令 执行该命令 :整数 跳转到该行 :s/one/two 将当前光标所在行的第一个one替换成two :s/one/two/g 将当前光标 ...