0%

Java筑基-:(13)2021.8.24 深入理解并发编程和归纳总结01

一、AQS的基本思想CLH队列锁

维护了一个链表,每个节点包含以下几个属性: 当前线程本身、我的前驱结点 以及 locked 标记(默认为true,当为false的时候,意味着释放锁了),示意图如下:

CLH队列锁结点示意图

当有线程需要获取锁的时候,便构造一个QNode类型结点 ,通过 CAS 操作将这个结点接入到链表尾部,同时将结点的前驱指向之前的尾部结点,自己则成为新的尾部结点(tailNode)。

获取锁的阶段:每个结点不断轮询前驱节点的 locked 是否为 false ,否的话,则 yield 等待下一次判断;如果是的话,则意味着前驱结点已经释放了锁,接下来该自己获取锁了。当然,实际操作可能还需要额外操作(比如公平锁时还需要判断自己当前是否是头结点)

CLH队列锁提供了一个很好的思路,但是一直自旋会消耗资源,因此,AQS 在这个思路上做了一些改进:

  • 链表改为了双向链表,而不是之前的单向

  • 控制自旋次数(一般2到3次),当自旋到一定次数后,就会阻塞挂起

二、公平锁和非公平锁

我们以 ReentrantLock 为例,看下它在公平锁和非公平锁时的代码实现,首先是非公平锁:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static final class NonfairSync extends Sync {
final boolean initialTryLock() {
Thread current = Thread.currentThread();
if (compareAndSetState(0, 1)) { // first attempt is unguarded
setExclusiveOwnerThread(current);
return true;
} else if (getExclusiveOwnerThread() == current) {
int c = getState() + 1;
if (c < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(c);
return true;
} else
return false;
}
}

公平锁的实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static final class FairSync extends Sync {
final boolean initialTryLock() {
Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedThreads() && compareAndSetState(0, 1)) {
setExclusiveOwnerThread(current);
return true;
}
} else if (getExclusiveOwnerThread() == current) {
if (++c < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(c);
return true;
}
return false;
}
}

可以看出二者并没有什么区别,只有在 if 条件里面,公平锁多了 一个 !hasQueuedPredecessors() 判断,这个方法的含义是这样的:

Queries whether any threads have been waiting to acquire longer than the current thread.

这也就是公平的含义了,要判断是否有其他线程比当前这个线程等待时间更久。

三、可重入

假如有 2 个成员方法,a() 和 b() ,都使用 synchronized 关键字修饰,其中 a 方法里面 调用了 b 方法,因为 synchronized 关键字的锁是可重入的,所以能够正常执行,但是,如果锁是不可重入的话,会发生什么?

答案是会发生死锁,自己把自己给锁死了。因为 b 方法获取锁执行 compareAndSetState(0, acquires) 的时候肯定是不成功的,因为 a 方法的时候已经将这个值设置为 1 了。

1、实现

可重入锁是怎么实现的呢?还是在上述代码中,我们能看到这样的判断:

1
else if (getExclusiveOwnerThread() == current)

也就是当前获得锁的线程是否就是自己,如果是自己的话,就直接进入了!这就是可重入锁的奥秘。

这里聊下为什么用 ++state 操作来标记重入锁的这个 state 状态,这主要是因为方便释放锁,退出一个同步块 的时候只需要将 state 减去 1 即可,一直减到 0 就释放了最外层的锁了

四、JMM(Java 内存模型:Java Memory Model)

看下 Google 大牛做的报告:

速度概念

如果要做 a + b 的操作,CPU 读取 a 和 b 这 2 个值花费了 200ns ,而真正的计算只花了 0.6ns ,所以需要cpu里面需要高速缓存。

工作内存和主内存是抽象概念,工作内存可能是寄存器、cpu高速缓存等,主内存可能主要指内存条。每个线程里面的工作内存是独享的,不能访问其他线程的工作内存。

五、volatile 详解

只保证可见性和有序性(禁止指令重排),对于复合指令(如 i ++)是没有原子性的。怎么做到的呢?

volatile 变量修饰的共享变量在进行写操作的时候会适用 CPU 提供的 lock 前缀指令,它的作用是:

  • 将当前处理器缓存行的数据写回系统内存

  • 写回内存的操作同时让其他 cpu 缓存的该变量数据失效

适用的场景:

  • 一个线程写,多个线程读

  • 多个线程写没有关联

比如 count = count + 1 那么,这个 count 是跟以前的值是有关联的。而 count = 5 之类的操作是没有关联的

一般可以用 volatile + CAS 操作来替换 synchronized 来提升效率, 如 ConcurrentHashMap

刚野课看到的

RecycleView用了自定义View ,View 里面有动画,动画的 update 里面执行了 invalidate ,这就会触发 draw 方法,而 draw 方法里面new 了 多个 Path ,造成内存抖动。会引起动画的卡顿

谢谢你的鼓励