上一篇AQS源码解析(2),学习了ReentrantLock的加锁过程,对照地来看一下如何释放锁。与加锁有一些不同的地方是,释放时直接就release(1),这是在AQS中实现的,不像加锁时在Sync的子类中才实现。

1
2
3
4
5
6
7
8
9
10
11
public class ReentrantLock implements Lock, java.io.Serializable {
private final Sync sync;

public void unlock() {
sync.release(1);
}

abstract static class Sync extends AbstractQueuedSynchronizer {
protected final boolean tryRelease(int releases) {} // 见下文
}
}

核心方法有两个tryRelease()unparkSuccessor(),看过加锁实现,应该也能猜到,tryRelease应该是在子类中才实现的,果不其然,AQS里只是定义,但不像加锁那样分为公平/非公平各自实现。而后者从名字能猜到,是要唤醒链表中的后续节点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/** AbstractQueuedSynchronizer.java **/
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();
}

/** ReentrantLock#Sync.java **/
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) { // 之前acquire(1),现在releases(1)
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
}

ReentrantLock为例,lock()本质上就是acquire(1)的过程,在unlock()时只需释放掉这拿到的1个状态资源,接着分析上一篇中遗留的unparkSuccessor()方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/** AbstractQueuedSynchronizer.java **/
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0); // 节点置为无状态

// 找到后续节点,如果是null或者是CANCELLED状态的话,从尾节点向前找
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);
}

至此,可以看到Reentrant的整个lock()unlock()过程,当节点acquire成功时,置为头节点,同时其他节点也在acquire,得不到竞态资源(state)就入队自旋等待,直到持有资源的头节点释放,并唤醒其后续节点。

上一篇 AQS源码解析(1)已经介绍了AQS的数据结构。在这篇,将从J.U.C里的许多基于AQS实现的同步工具出发,看一看AQS到底提供了哪些功能。
比如Lock lock = new ReentrantLock(),默认是非公平锁的实现,使用lock()unlock()进行同步,先来看一下如何加锁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class ReentrantLock implements Lock, java.io.Serializable {
private final Sync sync;

public ReentrantLock() {
sync = new NonfairSync();
}

public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}

public void lock() {
sync.lock();
}

abstract static class Sync extends AbstractQueuedSynchronizer {
abstract void lock();
}

static final class FairSync extends Sync {
final void lock() {
acquire(1);
}

protected final boolean tryAcquire(int acquires) {} // 见下文
}

static final class NonfairSync extends Sync {
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}

protected final boolean tryAcquire(int acquires) {} // 见下文
}
}

看代码还是很清晰的,核心就在acquire()中。这个方法是在父类AQS内定义实现的,而tryAcquire()在AQS内只给了定义,具体实现还是在子类中。

1
2
3
4
5
6
7
8
9
10
/** AbstractQueuedSynchronizer.java **/
public final void acquire(int arg) {
// tryAcquire失败,将独占模式节点入队,再从队列中获取
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}

protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}

ReentrantLock为例,内部类Sync的子类FairSyncNonFairSync分别实现了公平锁与非公平锁。这时能清楚看到,在AQS中用来表示同步状态的state,会随着每次acquire()而从0开始累加。同理每次release()会减少直到为0,当state == 0可以认为资源都被释放,公平锁需要把资源让给等待更久的线程。如果当前线程已经持有资源,可以继续增加state,无需重新等待竞争,这就是可重入的意义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
/** ReentrantLock#FairSync.java 公平锁tryAcquire实现 **/
static final class FairSync extends Sync {
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState(); // volatile int state
if (c == 0) {
// 公平锁:需要先看看是不是有线程比当前线程等的更久,没有的话再acquire资源
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;
}
}

/** ReentrantLock#NonfairSync.java 非公平锁tryAcquire实现 **/
static final class NonfairSync extends Sync {
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires); // 父类Sync中的nonfairTryAcquire方法
}
}

abstract static class Sync extends AbstractQueuedSynchronizer {
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 非公平锁:不用hasQueuedPredecessors,直接acquire资源
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;
}
}

如果tryAcquire(arg)失败,将创建独占模式节点入队,addWaiter(Node.EXCLUSIVE)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/** AbstractQueuedSynchronizer.java **/    
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode); // 节点与当前线程关联,并设置模式
// 尝试快速方式入队
Node pred = tail;
if (pred != null) {
node.prev = pred;
// 如果多个线程尝试入队,操作必须是原子的,将尾节点设置为node
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; // 返回先前节点
}
}
}
}

进行到此,再回顾一下:!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
acquireQueued()将开始从队列中自旋获取刚加入的节点,主要做的就是三件事,一是将满足条件的节点设置为新的头节点,二是给不满足条件的节点调整到最近的一个非CANCELLED节点后,并且将其先前节点的状态修改为SIGNAL,三是使用LockSupport.park将合适节点的线程休眠,最终返回结果表示当线程等待过程中是否被中断。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
// 当前节点满足条件:是第二个节点,并且acquire成功
if (p == head && tryAcquire(arg)) {
// 设置为头节点,这里不用CAS,因为已经acquire到资源
setHead(node);
p.next = null; // help GC,回收旧的头节点
failed = false;
return interrupted;
}
// 当前节点不满足条件,调整节点,如果可以的话,休眠线程
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
interrupted = true; // 只要有一次中断,就将中断标志设置为true
}
} finally { // return前,先检查failed,如果出现异常则取消该节点的acquire
if (failed)
cancelAcquire(node);
}
}

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL) // 先前节点状态已经是SIGNAL,当前节点位置安全,可以休眠
return true;
if (ws > 0) {
// 先前节点状态是CANCELLED,将当前节点调整到最近的一个非CANCELLED节点后
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 先前节点状态不是CANCELLED,CAS为SIGNAL
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
// 此时只是将先前节点状态修改,但考虑到并发情况,也有其他线程在做同样的事情,需要再次自旋检查
return false;
}

private final boolean parkAndCheckInterrupt() {
LockSupport.park(this); // 阻塞线程,被unpark或被interrupt可以唤醒
return Thread.interrupted(); // 返回线程是否被中断过
}

private void cancelAcquire(Node node) {
if (node == null)
return;
node.thread = null;
// 跳过CANCELLED状态的先前节点,调整该节点pred域
Node pred = node.prev;
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
Node predNext = pred.next; // 先前节点的后续节点

node.waitStatus = Node.CANCELLED;

// 如果该节点是队尾,将先前节点设置为队尾,然后将其next域置空
if (node == tail && compareAndSetTail(node, pred)) {
compareAndSetNext(pred, predNext, null);
} else {
// 不是队尾
// 先前节点同时如下满足条件:不是头节点、状态是SIGNAL或者可以变为SIGNAL、绑定了线程
// 将其pred节点的next域置为该节点的后续节点
int ws;
if (pred != head &&
((ws = pred.waitStatus) == Node.SIGNAL ||
(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
pred.thread != null) {
Node next = node.next;
if (next != null && next.waitStatus <= 0)
compareAndSetNext(pred, predNext, next);
} else {
unparkSuccessor(node); // 唤醒后续节点
}

node.next = node; // help GC,使CANCELLED状态的节点不可达,被GC回收
}
}

在看源码时有一个问题困扰着我,为什么在shouldParkAfterFailedAcquire时一定是从尾向头去找,而不是反过来呢?
至此,对于ReentrantLocklock()的实现,还剩一步unparkSuccessor()来唤醒后续节点,这个留在接下来要学习的unlock()实现中来分析。

CLH(Craig, Landin, and Hagersten)锁常用于实现自旋锁,AQS使用一种变形后的CLH锁队列,来代替阻塞同步。

1
2
3
     +------+  prev +-----+       +-----+
head | | <---- | | <---- | | tail
+------+ +-----+ +-----+

AQS使用双向队列,每一个Node都代表一个线程,有status属性,标记着线程是否应该被阻塞,prev指针指向前个节点,next指针(图中没有画出)指向后续节点,head和tail不言而喻

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
/** AbstractQueuedSynchronizer#Node.java **/
static final class Node {
// 处于共享模式
static final Node SHARED = new Node();

// 处于独占模式
static final Node EXCLUSIVE = null;

// 表示线程取消。(因为中断或者超时,不会再参与到竞争中,状态也不再改变)
static final int CANCELLED = 1;

// 表示后续线程等待释放。(因为后续节点被阻塞,处于等待状态,需要当前节点释放或者取消,之后会通知后续节点)
static final int SIGNAL = -1;

// 表示线程正在等待条件。(线程正处于Condition队列中等待条件,直到状态转变为0,才会被用于同步队列中)
static final int CONDITION = -2;

// 表示下一次共享式acquire将无条件传播。(共享式release会传播到其他节点,在doReleaseShared方法中)
static final int PROPAGATE = -3;

// 等待状态,初始化为0,非负数表示节点无需被通知
volatile int waitStatus;

// 注意:该节点的先前节点如果是CANCELLED的,就往前找第一个非CANCELLED的,肯定能找到一个,因为至少头节点就不是CANCELLED。
volatile Node prev;

// 注意:只有该节点成为尾节点时才会给先前节点的next域赋值。也就是说,next为null不代表这是尾节点。
volatile Node next;

// 节点绑定线程
volatile Thread thread;

// 等待conditon的下一个节点(独占模式),或者SHARED(共享模式)
Node nextWaiter;

final boolean isShared() {
return nextWaiter == SHARED;
}

final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}

Node() {}

Node(Thread thread, Node mode) {
// addWaiter方法使用
this.nextWaiter = mode;
this.thread = thread;
}

Node(Thread thread, int waitStatus) {
this.waitStatus = waitStatus;
this.thread = thread;
}
}

接下来再看一下AQS定义的域,特别要注意这里的state,表示同步状态,而不是上文中的线程等待状态waitStatus

AQS内部通过一个int类型的state字段表示同步状态,状态的具体含义可以子类来定义,例如ReentrantLock中用state表示线程重入的次数,Semaphore表示可用的许可的数量等。使用int是由于int能够应对大部分的场景,而long在很多平台需要使用额外锁来保证一致性的读取。(引用自他山之石)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 头节点,通过setHead方法修改,等待状态不能是CANCELLED
private transient volatile Node head;

// 尾节点,入队时增加
private transient volatile Node tail;

// 同步状态
private volatile int state;

static final long spinForTimeoutThreshold = 1000L;

protected final int getState() {
return state;
}

protected final void setState(int newState) {
state = newState;
}

AQS的一些包装UnSafe的CAS操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long stateOffset;
private static final long waitStatusOffset;
// ...省略 headOffset、tailOffset、nextOffset

static {
try {
stateOffset = unsafe.objectFieldOffset
(AbstractQueuedSynchronizer.class.getDeclaredField("state"));
waitStatusOffset = unsafe.objectFieldOffset
(Node.class.getDeclaredField("waitStatus"));
// ...省略 headOffset、tailOffset、nextOffset

} catch (Exception ex) { throw new Error(ex); }
}

// CAS同步状态
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

// 入队时如果队空CAS头节点
private final boolean compareAndSetHead(Node update) {
return unsafe.compareAndSwapObject(this, headOffset, null, update);
}

// 入队时CAS尾节点
private final boolean compareAndSetTail(Node expect, Node update) {
return unsafe.compareAndSwapObject(this, tailOffset, expect, update);
}

// CAS节点等待状态
private static final boolean compareAndSetWaitStatus(Node node,
int expect,
int update) {
return unsafe.compareAndSwapInt(node, waitStatusOffset,
expect, update);
}

// CAS节点next域
private static final boolean compareAndSetNext(Node node,
Node expect,
Node update) {
return unsafe.compareAndSwapObject(node, nextOffset, expect, update);
}

【题外话】补充一下,因为UnSafe做了安全验证,只允许信任的JDK调用,如果使用如上所示的Unsafe.getUnsafe()或者直接实例化,那么会抛Caused by: java.lang.SecurityException: Unsafe的异常,可以通过反射其内部的theUnsafe的域来进行实例化。

1
2
3
Field f = Unsafe.class.getDeclaredField("theUnsafe"); //Internal reference
f.setAccessible(true);
Unsafe unsafe = (Unsafe) f.get(null);

一、java.util
java.util 包提供了 Date 类来封装当前的日期和时间。 Date 类提供两个构造函数来实例化 Date 对象。

1
2
3
4
5
6
7
8
9
// 当前日期,输出:Fri May 10 14:15:23 CST 2019
System.out.println(new Date());

// 格式化日期,输出:2019-05-10 14:18:03
System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));

// 字符串日期解析,输出:Sun May 05 12:30:05 CST 2019
String dateStr = "2019-05-05 12:30:05";
System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(dateStr));

二、java.time
这个包是在Java8中引入的。在Java 8之前,所有关于时间和日期的API都存在各种使用方面的缺陷,主要有:

  1. Java的java.util.Datejava.util.Calendar类易用性差,不支持时区,而且他们都不是线程安全的;
  2. 用于格式化日期的类DateFormat被放在java.text包中,它是一个抽象类,所以我们需要实例化一个SimpleDateFormat对象来处理日期格式化,并且DateFormat也是非线程安全,这意味着如果你在多线程程序中调用同一个DateFormat对象,会得到意想不到的结果。
  3. 对日期的计算方式繁琐,而且容易出错,因为月份是从0开始的,从Calendar中获取的月份需要加一才能表示当前月份。

增加的API:

  1. LocalDate 表示一个具体的日期,不包含具体的时间和时区信息。

    1
    2
    3
    4
    5
    6
    7
    8
    LocalDate now = LocalDate.now();                    // 当前日期
    LocalDate localDate = LocalDate.of(2017, 1, 4); // 初始化一个日期:2017-01-04
    int year = localDate.getYear(); // 年份:2017
    Month month = localDate.getMonth(); // 月份:JANUARY
    int dayOfMonth = localDate.getDayOfMonth(); // 月份中的第几天:4
    DayOfWeek dayOfWeek = localDate.getDayOfWeek(); // 一周的第几天:WEDNESDAY
    int length = localDate.lengthOfMonth(); // 月份的天数:31
    boolean leapYear = localDate.isLeapYear(); // 是否为闰年:false
  2. LocalTime 表示一句包含具体时间的日期

    1
    2
    3
    4
    LocalTime localTime = LocalTime.of(17, 23, 52);     // 初始化一个时间:17:23:52
    int hour = localTime.getHour(); // 时:17
    int minute = localTime.getMinute(); // 分:23
    int second = localTime.getSecond(); // 秒:52
  3. LocalDateTime 是LocalDate和LocalTime的结合

    1
    2
    3
    4
    5
    6
    7
    8
    LocalDateTime ldt1 = LocalDateTime.of(2017, Month.JANUARY, 4, 17, 23, 52);
    LocalDate localDate = LocalDate.of(2017, Month.JANUARY, 4);
    LocalTime localTime = LocalTime.of(17, 23, 52);
    LocalDateTime ldt2 = localDate.atTime(localTime);

    // LocalDate与LocalTime的转化
    LocalDate date = ldt1.toLocalDate();
    LocalTime time = ldt1.toLocalTime();
  4. Instant 表示一个精确到纳秒的时间戳,System.currentTimeMillis()只精确到毫秒

    1
    2
    3
    4
    5
    6
    7
    // 当前时间戳
    Instant now = Instant.now()

    // ofEpochSecond()方法的第一个参数为秒,第二个参数为纳秒
    // 表示从1970-01-01 00:00:00开始后两分钟的10万纳秒的时刻
    // 输出1970-01-01T00:02:00.000100Z
    Instant instant = Instant.ofEpochSecond(120, 100000);
  5. Duration 表示一个精确到纳秒的时间段

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    // 2017-01-05 10:07:00
    LocalDateTime from = LocalDateTime.of(2017, Month.JANUARY, 5, 10, 7, 0);

    // 2017-02-05 10:07:00
    LocalDateTime to = LocalDateTime.of(2017, Month.FEBRUARY, 5, 10, 7, 0);

    // 表示从 2017-01-05 10:07:00 到 2017-02-05 10:07:00 这段时间
    Duration duration = Duration.between(from, to);

    long days = duration.toDays(); // 这段时间的总天数
    long hours = duration.toHours(); // 这段时间的小时数
    long minutes = duration.toMinutes(); // 这段时间的分钟数
    long seconds = duration.getSeconds(); // 这段时间的秒数
    long milliSeconds = duration.toMillis(); // 这段时间的毫秒数
    long nanoSeconds = duration.toNanos(); // 这段时间的纳秒数

    // 通过of()方法创建,接受一个时间段长度,和一个时间单位作为参数
    Duration duration1 = Duration.of(5, ChronoUnit.DAYS); // 5天
    Duration duration2 = Duration.of(1000, ChronoUnit.MILLIS); // 1000毫秒
  6. Period 表示以年月日来衡量时间段

    1
    2
    3
    4
    5
    6
    7
    8
    // 长度为2年3个月6天的时间段
    Period period = Period.of(2, 3, 6);

    / /通过between()方法创建,只接收LocalDate类型的参数
    // 2017-01-05 到 2017-02-05 这段时间
    Period period = Period.between(
    LocalDate.of(2017, 1, 5),
    LocalDate.of(2017, 2, 5));

相关操作:

  1. 与原有API的转化

    1
    2
    3
    // Date转LocalDate
    Date date = new Date();
    LocalDateTime localDateTime = date.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
  2. 增加和减少日期

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    LocalDate date = LocalDate.of(2017, 1, 5);          // 2017-01-05

    LocalDate date1 = date.withYear(2016); // 修改为 2016-01-05
    LocalDate date2 = date.withMonth(2); // 修改为 2017-02-05
    LocalDate date3 = date.withDayOfMonth(1); // 修改为 2017-01-01

    LocalDate date4 = date.plusYears(1); // 增加一年 2018-01-05
    LocalDate date5 = date.minusMonths(2); // 减少两个月 2016-11-05
    LocalDate date6 = date.plus(5, ChronoUnit.DAYS); // 增加5天 2017-01-10

    LocalDate date7 = date.with(nextOrSame(DayOfWeek.SUNDAY)); // 返回下一个距离当前时间最近的星期日
    LocalDate date9 = date.with(lastInMonth(DayOfWeek.SATURDAY)); // 返回本月最后一个星期六
  3. 格式化日期

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    LocalDateTime dateTime = LocalDateTime.now();
    String strDate1 = dateTime.format(DateTimeFormatter.BASIC_ISO_DATE); // 20170105
    String strDate2 = dateTime.format(DateTimeFormatter.ISO_LOCAL_DATE); // 2017-01-05
    String strDate3 = dateTime.format(DateTimeFormatter.ISO_LOCAL_TIME); // 14:20:16.998
    String strDate4 = dateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")); // 2017-01-05

    // 今天是:2017年 一月 05日 星期四
    String strDate5 = dateTime.format(DateTimeFormatter.ofPattern("今天是:YYYY年 MMMM DD日 E", Locale.CHINESE));

    String strDate6 = "2017-01-05";
    String strDate7 = "2017-01-05 12:30:05";

    LocalDate date = LocalDate.parse(strDate6, DateTimeFormatter.ofPattern("yyyy-MM-dd"));
    LocalDateTime dateTime1 = LocalDateTime.parse(strDate7, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
  4. 时区与历法,见参考,这里不贴了

参考
[1] Java 8新特性(四):新的时间和日期API
[2] Java8 日期/时间(Date Time)API指南
[3] Java 日期时间

现在大部分时间使用公司电脑,于是搞一下。
主要参考这个解决办法,master分支上是生成的页面文件,hexo分支上是源文件,设置hexo为默认分支。
遇到的第一个问题就是多个git账号,参考这个解决办法,主要思路是在ssh中增加一个config文件。

1
2
3
4
5
6
7
8
9
10
# 别名,可以随便取
Host github
User git
Hostname github.com
IdentityFile ~/.ssh/github_id_rsa

Host gitlab
User git
Hostname gitlab.xxx.com
IdentityFile ~/.ssh/id_rsa

这里有个小小的坑,就是之前设置了git config —global,在hexo deploy时会使用全局配置的用户名进行,需要在hexo的项目中配置局部设置。

1
2
$ git config user.name "A"
$ git config user.email "A@gmail.com"

这样就会使用配置后的局部用户名进行push。

其次就是不要用hexo init命令。原因是当前目录已经建立了git仓库环境, hexo init会覆盖到当前的git环境,重建一个新的,这样和我们的私有Hexo源码仓库脱离了联系。
最后,由于在yml文件已经配置了deploy的branch是master,所以执行hexo d时还是会将页面文件保存至master,而源文件需要手动push到hexo分支上。

一些hexo常用命令:
hexo new newpage # 新建
hexo g # 生成
hexo s # 本地预览
hexo d # 部署

另外,在mac上发现了一款markdown神器——typora
注意一下,直接使用回车会有空行,使用shift+回车就可以没有空行了。

一些typora常用快捷键:
超链接 ⌘ + k
代码块 ⌃ + `

ps1: mac技术符号输入
ps2: 如果两个重音符`中间的内容也包含重音符,则在最外层需要用两个连续重音符夹起来


题目要求:给定一个数组nums,找出和为0的所有三元组集合。

示例:nums = [-1, 0, 1, 2, -1, -4],和为0的三元组集合 = [[-1, 0, 1], [-1, -1, 2]]。

思路:将数组排序后,指针指定一个元素后,对该元素之后的元素进行2sum运算,考虑到不能有重复结果,所以需要跳过重复元素。

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import java.util.Arrays;

public class Solution {
public List<List<Integer>> threeSum(int[] nums) {
int N = nums.length;
List<List<Integer>> result = new ArraysList<>();
if (nums == null || N == 0 || N < 3)
return result;
Arrays.sort(nums);

for (int i = 0; i < N-2; i++) {
// 跳过重复元素
if (i > 0 && num[i-1] == nums[i])
continue;
int left = i + 1;
int right = N - 1;
int target = -nums[i];
// 对之后的元素进行2sum
twoSum(nums, left, right, target, result);
}
return result;
}

private void twoSum(int[] nums, int left, int right, int target, List<List<Integer>> result) {
while (left < right) {
if (nums[left] + nums[right] == target) {
List<Integer> list = new ArrayList<>();
list.add(-target);
list.add(nums[left]);
list.add(nums[right]);
result.add(list);

left++;
right--;

// 从左边跳过重复元素
while (left < right && nums[left-1] == nums[left])
left++;
// 从右边跳过重复元素
while (left < right && nums[right] == nums[right+1])
right--;
}
// 和小于target,增大left的值
else if (nums[left] + nums[right] < target)
left++;
// 和大于target,减小right的值
else
right--;
}
}
}


题目要求:给定一个非负的整型数组,元素a[i]代表坐标(i, a[i]),得到一组每个点到x轴的垂线,选出两条垂线和x轴构成水箱,求围成的最大的面积。

思路:
左右两个指针扫描数组,计算面积。假设找到最佳的两条线为left,right,易知left左边没有比left更高的垂线,right右边没有比right更高的垂线,即最佳的组合在当前候选的left和right的范围内。收缩底边长度,增大高,当left的高度大于right的,right向左扫描,反之left向右扫描,求出最大面积。

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Solution {
public int maxArea(int[] height) {
int N = height.length;
if (N < 2)
return 0;
int maxArea = 0;
int left = 0;
int right = N - 1;
while (left < right) {
int currArea = (right - left) * Math.min(height[left], height[right]);
maxArea = Math.max(maxArea, currArea);
if (height[left] > height[right])
right--;
else
left++;
}
return maxArea;
}
}


题目要求:给定两个有序数组nums1、nums2,找到合并后数组的中位数。

示例:nums1 = [1, 3],nums2 = [2],中位数2.0

思路:

  1. 传统思路,将两个数组合并起来再排序,找到中位数。
  2. 将问题扩展为在两个有序数组中找到合并后第K个数的问题。

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// 采用第二种思路
public class Solution {
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
int m = nums1.length;
int n = nums2.length;
int total = m + n;
// 奇数
if (total % 2 != 0) {
return findKth(nums1, 0, m-1, nums2, 0, n-1, total/2 + 1); // 第total/2+1个元素
} else {
double x = findKth(nums1, 0, m-1, nums2, 0, n-1, total/2);
double y = findKth(nums1, 0, m-1, nums2, 0, n-1, total/2 + 1);
return (x + y) / 2;
}
}

// 指针从1开始计数
private double findKth(int[] a, int aLeft, int aRight, int[] b, int bLeft, int bRight, int k) {
int m = aRight - aLeft + 1; // 当前a的扫描长度
int n = bRight - bLeft + 1; // 当前b的扫描长度
if (m > n)
return findKth(b, bLeft, bRight, a, aLeft, aRight, k);
if (m == 0)
return b[k-1];
if (k == 1)
return Math.min(a[aLeft], b[bLeft]);

int partA = Math.min(m, k/2); // a的指针偏移量
int partB = k - partA; // a指针偏移了partA,在合并后的数组中,b指针偏移k-partA

// a和b哪个偏移后的数值小,说明Kth大于哪个,证明见下文。则可忽略小于偏移量的那些元素。
if (a[aLeft + partA - 1] > b[bLeft + partB - 1])
return findKth(a, aLeft, aRight, b, bLeft+partB, bRight, k-partB);
else if (a[aLeft + partA - 1] < b[bLeft + partB -1])
return findKth(a, aLeft+partA, aRight, b, bLeft, bRight, k-partA);
else
// a[aLeft+partA-1] == b[bLeft+partB-1],找到Kth
return a[aLeft + partA - 1];
}
}

分析:考虑一种情况,假设a和b的个数都大于k/2,设p=k/2-1,如果a[p] < b[p],则合并后Kth > a[p]
证明:用反证法,假设当a[p] < b[p]时,kth < a[p],不妨令a[p]是合并后第k+1个数。于是,a中有p个数小于a[p],由假设知b中最多只有p个数小于a[p],所以总共有2p=k-2个数小于a[p]。a[p]是第k+1个数并且a[p]之前有k-2个数,这是矛盾的,故得证Kth > a[p]。
同理,当a[p] > b[p]时,Kth > b[p],当a[p] == b[p]时,Kth==a[p]==b[p]

在求Kth时,从相对更短的数组入手来计算偏移量p,主要是比较偏移后元素的大小。如果是上文中假设的情况,则直接取k/2,否则取数组长度,即Math.min(k/2, m)。


题目要求:给定一个数组nums以及一个数target,求出和为target的两个元素的索引。

示例:nums = [2, 7, 11, 15], target = 9,因为nums[0] + nums[1] = 2 + 7 = 9,所以返回[0, 1]

思路:

  1. 传统的暴力方法,两次遍历数组,求出所有二元组的和,复杂度O(n^2)
  2. 使用Map,数值(key)-索引(value),遍历一次数组,根据当前nums[i]在Map中查找target-nums[i]

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import java.util.Arrays;
public class Solution {
public int[] twoSum(int[] nums, int target) {
int N = nums.length;
if (N < 2)
return null;
int[] result = new int[2];
Map<Integer, Integer> map = new HashMap<Integer, Integer>();
for (int i = 0; i < N; i++) {
if (map.containsKey(target-nums[i]) {
result[0] = map.get(target-num[i]);
result[1] = i;
}
map.put(nums[i], i);
}
return result;
}
}


硬件内存架构

现代计算机硬件架构的简单图示:
"Model"
一个现代计算机通常由两个或者多个CPU。其中一些CPU还有多核。从这一点可以看出,在一个有两个或者多个CPU的现代计算机上同时运行多个线程是可能的。每个CPU在某一时刻运行一个线程是没有问题的。这意味着,如果你的Java程序是多线程的,在你的Java程序中每个CPU上一个线程可能同时(并发)执行。
每个CPU都包含一系列的寄存器,它们是CPU内内存的基础。CPU在寄存器上执行操作的速度远大于在主存上执行的速度。这是因为CPU访问寄存器的速度远大于主存。
每个CPU可能还有一个CPU缓存层。实际上,绝大多数的现代CPU都有一定大小的缓存层。CPU访问缓存层的速度快于访问主存的速度,但通常比访问内部寄存器的速度还要慢一点。一些CPU还有多层缓存,但这些对理解Java内存模型如何和内存交互不是那么重要。只要知道CPU中可以有一个缓存层就可以了。
一个计算机还包含一个主存。所有的CPU都可以访问主存。主存通常比CPU中的缓存大得多。
通常情况下,当一个CPU需要读取主存时,它会将主存的部分读到CPU缓存中。它甚至可能将缓存中的部分内容读到它的内部寄存器中,然后在寄存器中执行操作。当CPU需要将结果写回到主存中去时,它会将内部寄存器的值刷新到缓存中,然后在某个时间点将值刷新回主存。
当CPU需要在缓存层存放一些东西的时候,存放在缓存中的内容通常会被刷新回主存。CPU缓存可以在某一时刻将数据局部写到它的内存中,和在某一时刻局部刷新它的内存。它不会再某一时刻读/写整个缓存。通常,在一个被称作“cache lines”的更小的内存块中缓存被更新。一个或者多个缓存行可能被读到缓存,一个或者多个缓存行可能再被刷新回主存。

内存数据存储

提出问题:指令和数据都存放在内存中,那么CPU怎么区分是指令还是数据呢?
这是一个很大的问题,就简单从这两个方面来分析

  • 从时间上
    取指令事件发生在指令周期的第一个CPU周期中,即发生在“取指令”阶段,指令周期的长短与指令的复杂程度有关,而取数据事件发生在指令周期的后面几个CPU周期中,即发生在“执行指令”阶段。
  • 从空间上
    如果取出的代码是指令,那么一定送往指令寄存器,如果取出的代码是数据,那么一定送往运算器。例如:8086采用分段存储管理方式,CPU将CS:IP指向的内存单元的内容看作指令。因为任何时候,CPU用CS:IP合成指令的物理地址,在执行指令的周期,CPU读DS段去找数据。
    于是有这样的内存模型:
    "MemoryModel"
    每当一个程序被执行,系统就要为它开启一个进程,并且为它分配内存。从低址区到高址区,分成几个不同的区域。
  • 低址:存放程序代码本身。(例如8086的CS寄存器中保存的段地址指向的位置)
  • 次低址:存放全局变量,无论是初始化的还是未初始化的。
  • 中址:就是堆(Heap)和堆栈(Stack)的区域。用来储存进程运行过程中产生的变量。
  • 最高址:为系统额外预留的空间。我们无法操作。

stack(堆栈)
从高址区往下延展。用来存储Scoped Variable(限域变量)。简单说就是已知存储空间以及生命周期的变量。为什么stack效率高呢?因为变量大小确定,都是紧挨着储存的,在堆栈中创建和释放储存空间只要一条汇编语言,分别是将栈顶指针向下和向上移动。而stack本身又是LIFO(Last in First out)的,所以效率极高。
heap(堆)
从较低地址区往上移动。heap是个动态内存池。从下面的图我们看的很清楚,heap不像stack那样是数据是连续的,而且使用LIFO机制。heap的数据是不连续的,动态随便乱贴的。创建和释放效率都不高。
"Heap-Stack"

至于是大端存储还是小端存储的方式在这里就不做过多的讨论了:
"Big-Endian-Little-Endian"

Java内存模型内部原理

Java内存模型把Java虚拟机内部划分为

  • 线程栈(堆栈,Stack == Thread Stack)
  • 堆(Heap)
    “引用”存放在Stack中,Object存放在Heap中
    这张图演示了Java内存模型的逻辑视图。
    "JavaModel"
    每一个运行在Java虚拟机里的线程都拥有自己的线程栈。这个线程栈包含了这个线程调用的方法当前执行点相关的信息。一个线程仅能访问自己的线程栈。一个线程创建的本地变量对其它线程不可见,仅自己可见。即使两个线程执行同样的代码,这两个线程任然在在自己的线程栈中的代码来创建本地变量。因此,每个线程拥有每个本地变量的独有版本。
    所有原始类型的本地变量都存放在线程栈上,因此对其它线程不可见。一个线程可能向另一个线程传递一个原始类型变量的拷贝,但是它不能共享这个原始类型变量自身。
    堆上包含在Java程序中创建的所有对象,无论是哪一个对象创建的。这包括原始类型的对象版本。如果一个对象被创建然后赋值给一个局部变量,或者用来作为另一个对象的成员变量,这个对象任然是存放在堆上。
    示例代码:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    // 假设有两个线程Thread1、Thread2
    public class MyRunnable implements Runnable() {

    public void run() {
    methodOne();
    }

    public void methodOne() {

    // 本地变量,存放在Thread Stack中
    int localVariable1 = 45;

    // 对象Object3存放在Heap中,引用存放在Thread Stack中
    MySharedObject localVariable2 = MySharedObject.sharedInstance;

    methodTwo();
    }

    public void methodTwo() {
    // 对象Object1、Object5存放在Heap中,引用存放在Thread Stack中
    Integer localVariable1 = new Integer(99);
    }
    }

    public class MySharedObject {

    // 指向MyShareObject实例的静态变量(Object3),随着类定义存放在Heap中
    // 这个对象可以被持有该对象引用(localVariable2)的线程T1、T2访问
    // 如果调用Object3的同一个方法,T1、T2会创建一个该对象本地成员变量的私有拷贝
    public static final MySharedObject sharedInstance = new MySharedObject();

    // 存放在Heap中的成员变量引用(Object1、Object5)
    public Integer object2 = new Integer(22);
    public Integer object4 = new Integer(44);

    // 成员变量存放在Heap中
    public long member1 = 12345;
    public long member1 = 67890;
    }

下面这张图演示了调用栈和本地变量存放在线程栈上,对象存放在堆上。
"LocalVariable"

Java内存模型和硬件内存架构之间的桥接

"bridge"
Java内存模型模拟了硬件内存架构,如果把Heap比作内存,把Thread Stack比作多个处理器与缓存层的话,当有多个线程时,每个Stack从内存中将数据拷贝,变更以后再写回内存时就会出现数据不一致的情况。
"disaccord"
Java中的volatile关键字。volatile关键字可以保证直接从主存中读取一个变量,如果这个变量被修改后,总是会被写回到主存中去。但这只能保证用volatile读到的数据是内存中最新的,但在copy到Thread Stack以后其他线程再改变数据就会出现问题,这就是存在竞争。
为了解决这个问题,可以使用Java同步块。一个同步块可以保证在同一时刻仅有一个线程可以进入代码的临界区(读取临界资源)。同步块还可以保证代码块中所有被访问的变量将会从主存中读入,当线程退出同步代码块时,所有被更新的变量都会被刷新回主存中去,不管这个变量是否被声明为volatile。

参考: