一、AQS的基本思想CLH队列锁
维护了一个链表,每个节点包含以下几个属性: 当前线程本身、我的前驱结点 以及 locked 标记(默认为true,当为false的时候,意味着释放锁了),示意图如下:
当有线程需要获取锁的时候,便构造一个QNode类型结点 ,通过 CAS 操作将这个结点接入到链表尾部,同时将结点的前驱指向之前的尾部结点,自己则成为新的尾部结点(tailNode)。
获取锁的阶段:每个结点不断轮询前驱节点的 locked 是否为 false ,否的话,则 yield 等待下一次判断;如果是的话,则意味着前驱结点已经释放了锁,接下来该自己获取锁了。当然,实际操作可能还需要额外操作(比如公平锁时还需要判断自己当前是否是头结点)
CLH队列锁提供了一个很好的思路,但是一直自旋会消耗资源,因此,AQS 在这个思路上做了一些改进:
链表改为了双向链表,而不是之前的单向
控制自旋次数(一般2到3次),当自旋到一定次数后,就会阻塞挂起
二、公平锁和非公平锁
我们以 ReentrantLock 为例,看下它在公平锁和非公平锁时的代码实现,首先是非公平锁:
1 | static final class NonfairSync extends Sync { |
公平锁的实现如下:
1 | static final class FairSync extends Sync { |
可以看出二者并没有什么区别,只有在 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 ,造成内存抖动。会引起动画的卡顿