Nicksxs's Blog

What hurts more, the pain of hard work or the pain of regret?

很多东西都是时看时新,而且时间长了也会忘,所以再来复习下,也会有一些新的角度看法这次来聊下AQS的内容,主要是这几个点,

第一个线程

第一个线程抢到锁了,此时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
/**
* Fair version of tryAcquire. Don't grant access unless
* recursive call or no waiters or is first.
*/
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
// 这里如果state还是0说明锁还空着
if (c == 0) {
// 因为是公平锁版本的,先去看下是否阻塞队列里有排着队的
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
// 没有排队的,并且state使用cas设置成功的就标记当前占有锁的线程是我
setExclusiveOwnerThread(current);
// 然后其实就返回了,包括阻塞队列的head和tail节点和waitStatus都没有设置
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
// 这里就是第二个线程会返回false
return false;
}
}

第二个线程

当第二个线程进来的时候应该是怎么样,结合代码来看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* Acquires in exclusive mode, ignoring interrupts. Implemented
* by invoking at least once {@link #tryAcquire},
* returning on success. Otherwise the thread is queued, possibly
* repeatedly blocking and unblocking, invoking {@link
* #tryAcquire} until success. This method can be used
* to implement method {@link Lock#lock}.
*
* @param arg the acquire argument. This value is conveyed to
* {@link #tryAcquire} but is otherwise uninterpreted and
* can represent anything you like.
*/
public final void acquire(int arg) {
// 前面第一种情况是tryAcquire直接成功了,这个if判断第一个条件就是false,就不往下执行了
// 如果是第二个线程,第一个条件获取锁不成功,条件判断!tryAcquire(arg) == true,就会走
// acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}

然后来看下addWaiter的逻辑

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
/**
* Creates and enqueues node for current thread and given mode.
*
* @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared
* @return the new node
*/
private Node addWaiter(Node mode) {
// 这里是包装成一个node
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
// 最快的方式就是把当前线程的节点放在阻塞队列的最后
Node pred = tail;
// 只有当tail,也就是pred不为空的时候可以直接接上
if (pred != null) {
node.prev = pred;
// 如果这里cas成功了,就直接接上返回了
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 不然就会继续走到这里
enq(node);
return node;
}

然后就是enq的逻辑了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* Inserts node into queue, initializing if necessary. See picture above.
* @param node the node to insert
* @return node's predecessor
*/
private Node enq(final Node node) {
for (;;) {
// 如果状态没变化的话,tail这时还是null的
Node t = tail;
if (t == null) { // Must initialize
// 这里就会初始化头结点,就是个空节点
if (compareAndSetHead(new Node()))
// tail也赋值成head
tail = head;
} else {
// 这里就设置tail了
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}

所以从这里可以看出来,其实head头结点不是个真实的带有线程的节点,并且不是在第一个线程进来的时候设置的

解锁

通过代码来看下

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
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
/**
* Attempts to release this lock.
*
* <p>If the current thread is the holder of this lock then the hold
* count is decremented. If the hold count is now zero then the lock
* is released. If the current thread is not the holder of this
* lock then {@link IllegalMonitorStateException} is thrown.
*
* @throws IllegalMonitorStateException if the current thread does not
* hold this lock
*/
public void unlock() {
// 释放锁
sync.release(1);
}
/**
* Releases in exclusive mode. Implemented by unblocking one or
* more threads if {@link #tryRelease} returns true.
* This method can be used to implement method {@link Lock#unlock}.
*
* @param arg the release argument. This value is conveyed to
* {@link #tryRelease} but is otherwise uninterpreted and
* can represent anything you like.
* @return the value returned from {@link #tryRelease}
*/
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 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 final boolean release(int arg) {
// 尝试去释放,如果是完全释放,返回的就是true,否则是false
if (tryRelease(arg)) {
Node h = head;
// 这里判断头结点是否为空以及waitStatus的状态,前面说了head节点其实是
// 在第二个线程进来的时候初始化的,如果是空的话说明没后续节点,并且waitStatus
// 也表示了后续的等待状态
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}

/**
* Wakes up node's successor, if one exists.
*
* @param node the node
*/
// 唤醒后继节点
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)
compareAndSetWaitStatus(node, 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;
// 从后往前找,找到非取消的节点,注意这里不是找到就退出,而是一直找到头
// 所以不必担心中间有取消的
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
// 将其唤醒
LockSupport.unpark(s.thread);
}

最近群里大佬发起了一个读书打卡活动,需要每天读一会书,在群里打卡分享感悟,争取一个月能读完一本书,说实话一天十分钟的读书时间倒是问题不大,不过每天都要打卡,而且一个月要读完一本书,其实难度还是有点大的,不过也想试试看。
之前某某老大给自己立了个 flag,说要读一百本书,这对我来说挺难实现的,一则我也不喜欢书只读一小半,二则感觉对于喜欢看的内容范围还是比较有限制,可能也算是比较矫情,不爱追热门的各类东西,因为往往会有一些跟大众不一致的观点看法,显得格格不入。所以还是这个打卡活动可能会比较适合我,书是人类进步的阶梯。
到现在是打卡了三天了,读的主要是白岩松的《幸福了吗》,对于白岩松,我们这一代人是比较熟悉,并且整体印象比较不错的一个央视主持人,从《焦点访谈》开始,到疫情期间的各类一线节目,可能对我来说是个三观比较正,敢于说一些真话的主持人,这中间其实是有个空档期,没怎么看电视,也不太关注了,只是在疫情期间的节目,还是一如既往地给人一种可靠的感觉,正好有一次偶然微信读书推荐了白岩松的这本书,就看了一部分,正好这次继续往下看,因为相对来讲不会很晦涩,并且从这位知名央视主持人的角度分享他的过往和想法看法,还是比较有意思的。
从对汶川地震,08 年奥运等往事的回忆和一些细节的呈现,也让我了解比较多当时所不知道的,特别是汶川地震,那时的我还在读高中,真的是看着电视,作为“猛男”都忍不住泪目了,共和国之殇,多难兴邦,但是这对于当事人来说,都是一场醒不过来的噩梦。
然后是对于足球的热爱,其实光这个就能掰扯很多,因为我不爱足球,只爱篮球,其中原因有的没的也挺多可以说的,但是看了他的书,才能比较深入的了解一个足球迷,对足球,对中国足球,对世界杯,对阿根廷的感情。
接下去还是想能继续坚持下去,加油!

前面写过一系列的 redis 源码分析的,但是实际上很多的问题还是需要结合实际的使用,然后其实就避不开缓存使用的三个著名问题,穿透,击穿和雪崩,这三个概念也是有着千丝万缕的关系,

缓存穿透

缓存穿透是指当数据库中本身就不存在这个数据的时候,使用一般的缓存策略时访问不到缓存后就访问数据库,但是因为数据库也没数据,所以如果不做任何策略优化的话,这类数据就每次都会访问一次数据库,对数据库压力也会比较大。

缓存击穿

缓存击穿跟穿透比较类似的,都是访问缓存不在,然后去访问数据库,与穿透不一样的是击穿是在数据库中存在数据,但是可能由于第一次访问,或者缓存过期了,需要访问到数据库,这对于访问量小的情况其实算是个正常情况,但是随着请求量变高就会引发一些性能隐患。

缓存雪崩

缓存雪崩就是击穿的大规模集群效应,当大量的缓存过期失效的时候,这些请求都是直接访问到数据库了,会对数据库造成很大的压力。

对于以上三种场景也有一些比较常见的解决方案,但也不能说是万无一失的,需要随着业务去寻找合适的方案

解决缓存穿透

对于数据库中就没这个数据的时候,一种是可以对这个 key 设置下空值,即以一个特定的表示是数据库不存在的,这种情况需要合理地调整过期时间,当这个 key 在数据库中有数据了的话,也需要有策略去更新这个值,并且如果这类 key 非常多,这个方法就会不太合适,就可以使用第二种方法,就是布隆过滤器,bloom filter,前置一个布隆过滤器,当这个 key 在数据库不存在的话,先用布隆过滤器挡一道,如果不在的话就直接返回了,当然布隆过滤器不是绝对的准确的

解决缓存击穿

当一个 key 的缓存过期了,如果大量请求过来访问这个 key,请求都会落在数据库里,这个时候就可以使用一些类似于互斥锁的方式去让一个线程去访问数据库,更新缓存,但是这里其实也有个问题,就是如果是热点 key 其实这种方式也比较危险,万一更新失败,或者更新操作的时候耗时比较久,就会有一大堆请求卡在那,这种情况可能需要有一些异步提前刷新缓存,可以结合具体场景选择方式

解决缓存雪崩

雪崩的情况是指大批量的 key 都一起过期了,击穿的放大版,大批量的请求都打到数据库上了,一方面有可能直接缓存不可用了,就需要用集群化高可用的缓存服务,然后对于实际使用中也可以使用本地缓存结合 redis 缓存,去提高可用性,再配合一些限流措施,然后就是缓存使用过程总的过期时间最好能加一些随机值,防止在同一时间过期而导致雪崩,结合互斥锁防止大量请求打到数据库。

题目介绍

A path in a binary tree is a sequence of nodes where each pair of adjacent nodes in the sequence has an edge connecting them. A node can only appear in the sequence at most once. Note that the path does not need to pass through the root.

The path sum of a path is the sum of the node’s values in the path.

Given the root of a binary tree, return the maximum path sum of any path.

路径 被定义为一条从树中任意节点出发,沿父节点-子节点连接,达到任意节点的序列。该路径 至少包含一个 节点,且不一定经过根节点。

路径和 是路径中各节点值的总和。

给你一个二叉树的根节点 root ,返回其 最大路径和

简要分析

其实这个题目会被误解成比较简单,左子树最大的,或者右子树最大的,或者两边加一下,仔细想想都不对,其实有可能是产生于左子树中,或者右子树中,这两个都是指跟左子树根还有右子树根没关系的,这么说感觉不太容易理解,画个图

可以看到图里,其实最长路径和是左边这个子树组成的,跟根节点还有右子树完全没关系,然后再想一种情况,如果是整棵树就是图中的左子树,那么这个最长路径和就是左子树加右子树加根节点了,所以不是我一开始想得那么简单,在代码实现中也需要一些技巧

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int ansNew = Integer.MIN_VALUE;
public int maxPathSum(TreeNode root) {
maxSumNew(root);
return ansNew;
}

public int maxSumNew(TreeNode root) {
if (root == null) {
return 0;
}
// 这里是个简单的递归,就是去递归左右子树,但是这里其实有个概念,当这样处理时,其实相当于把子树的内部的最大路径和已经算出来了
int left = maxSumNew(root.left);
int right = maxSumNew(root.right);
// 这里前面我有点没想明白,但是看到 ansNew 的比较,其实相当于,返回的是三种情况里的最大值,一个是左子树+根,一个是右子树+根,一个是单独根节点,
// 这样这个递归的返回才会有意义,不然像原来的方法,它可能是跳着的,但是这种情况其实是借助于 ansNew 这个全局的最大值,因为原来我觉得要比较的是
// left, right, left + root , right + root, root, left + right + root 这些的最大值,这里是分成了两个阶段,left 跟 right 的最大值已经在上面的
// 调用过程中赋值给 ansNew 了
int currentSum = Math.max(Math.max(root.val + left , root.val + right), root.val);
// 这边返回的是 currentSum,然后再用它跟 left + right + root 进行对比,然后再去更新 ans
// PS: 有个小点也是这边的破局点,就是这个 ansNew
int res = Math.max(left + right + root.val, currentSum);
ans = Math.max(res, ans);
return currentSum;
}

这里非常重要的就是 ansNew 是最后的一个结果,而对于 maxSumNew 这个函数的返回值其实是需要包含了一个连续结果,因为要返回继续去算路径和,所以返回的是 currentSum,最终结果是 ansNew

结果图

难得有个 100%,贴个图哈哈

今天真的是被气得不轻,情况是碰到一个有 70 多秒的直行红灯,然后直行就排了很长的队,但是左转车道没车,就有好几辆车占着左转车道,准备往直行车道插队加塞,一般这种加塞的,会挑个不太计较的,如果前面一辆不让的话就再等等,我因为赶着回家,就不想让,结果那辆车几次车头直接往里冲,当时怒气值基本已经蓄满了,我真的是分毫都不想让,如果路上都是让着这种人的,那么这种情况只会越来越严重,我理解的这种心态,就赌你怕麻烦,多一事不如少一事,结果就是每次都能顺利插队加塞,其实延伸到我们社会中的种种实质性的排队或者等同于排队的情况,都已经有这种惯有思维,一方面这种不符合规则,可能在严重程度上容易被很多人所忽视,基本上已经被很多人当成是“合理”行为,另一方面,对于这些“微小”的违规行为,本身管理层面也基本没有想要管的意思,就更多的成为了纵容这些行为的导火索,并且大多数人都是想着如果不让,发生点小剐小蹭的要浪费很多时间精力来处理,甚至会觉得会被别人觉得自己太小气等等,诸多内外成本结合起来,会真的去硬刚的可能少之又少了,这样也就让更多的人觉得这种行为是被默许的,再举个非常小的例子,以我们公司疫情期间的盒饭发放为例,有两个比较“有意思”的事情,第一个就是因为疫情,本来是让排队要间隔一米,但是可能除了我比较怕死会跟前面的人保持点距离基本没别人会不挨着前面的人,甚至我跟我前面的人保持点距离,后面的同学会推着我让我上去;第二个是关于拿饭,这么多人排着队拿饭,然后有部分同学,一个人拿好几份,帮组里其他人的都拿了,有些甚至一个人拿十份,假如这个盒饭发放是说明了可以按部门直接全领了那就没啥问题,但是当时的状况是个人排队领自己的那一份,如果一个同学直接帮着组里十几个人都拿了,后面排队的人是什么感受呢,甚至有些是看到队伍排长了,就找队伍里自己认识的比较靠前的人说你帮我也拿一份,其实作为我这个比较按规矩办事的“愣头青”来说,我是比较不能接受这两件小事里的行为的,再往下说可能就有点偏激了,先说到这~

0%