。。锁

This commit is contained in:
chenyan 2022-12-30 04:22:58 +08:00
parent 994fa7d270
commit ecc755a43b
7 changed files with 309 additions and 11 deletions

View File

@ -9,7 +9,7 @@ tags: [git]
## .gitconfig
```sh
```
[user]
name = chenyan
email = chenyan@feling.net

View File

@ -1,7 +1,7 @@
---
layout: pages
title: 读 LinkedHashMap
categories: [源码]
categories: [Java]
---
## 别人那边听到的 Map

View File

@ -71,6 +71,17 @@ username: chenyan
password: 密码
```
## mypy 源码错误检查
```sh
pip install mypy
```
vscode 配置
```json
"python.linting.mypyEnabled": true
```
* 文章目录

View File

@ -2,7 +2,7 @@
layout: pages
title: redis 的调用可以再精致点
categories: 杂谈
tags: [middleware, redis]
tags: [middleware, redis, lock]
---
@ -25,7 +25,7 @@ tags: [middleware, redis]
### 击穿
击穿的意思是在高并发的前提下,某个缓存因为过期或淘汰策略等原因,暂时在缓存里取不到,大量的请求几乎同时打到数据库。
击穿的意思是在高并发的前提下,某个缓存因为过期或淘汰策略等原因,暂时在缓存里取不到,大量的请求几乎同时打到数据库。
如何避免大量的请求同时打到数据库,是不是跟前面 消息折叠 中的毛刺问题很像。但是侧重点不同,消息折叠只需要 setnx() 就够了, 也只偏向 tryLock() 的语义。讨论击穿的问题,因为没有具体的业务和功能场景,作为一个通用的解决方案,更强调纯粹的锁的技术。在 setnx() 的基础上,再考虑下面几个问题,把锁的实现再完善一点。
@ -40,7 +40,7 @@ tags: [middleware, redis]
对于问题3可以从 redis 的 value 值下手,加锁时把 value 设置成自己的唯一值。解锁前检查一下。
进一步深入的问题还有很多,但是简单处理下上面的问题,就可以说这是一把可用的分布式不可重入自旋锁了。
进一步深入的问题还有很多,但是简单处理下上面的问题,就可以说这是一把可用的 分布式不可重入自旋+阻塞的 锁了。
是否一定要用分布式锁呢JVM 提供的锁同样可以把大量的并发限制到服务实例的个数这就可以用了没必要精确限制到1。@Cacheable 注解的 sync 属性就提供这个功能。
@ -55,9 +55,9 @@ tags: [middleware, redis]
![](/images/redis-xuebeng-solution.jpg)
### redission
### redisson
我一直是反感 redission 的(同样反感 spring-data,恰恰是因为它的 开源、活力、长期的积累。导致它的模型复杂而庞大。它实现了完整的 jdk 定义的锁的接口
我一直是反感 redisson 的,恰恰是因为它的 开源、活力、长期的积累。导致它的模型复杂而庞大。
我一直期待,遇到一个场景,是下面这三个接口方法不够用的。
@ -67,7 +67,7 @@ tryLock()
unlock()
```
何必引入完整的jdk接口实现团队里的人都那么牛逼hold得住嘛
何必引入庞大的接口定义团队里的人都那么牛逼hold得住嘛JUC里的锁实现都熟悉了嘛
这天,时间大概在一年一度的组织架构调整期间,收到一个原本是其他小组维护的服务,组织架构调整调过来的,代码来了但人没来。里面有段不停打异常日志的代码是这样的。
@ -114,7 +114,7 @@ try {
## 以一己之力发明了MemoryCache
跟前面讲的消息折叠里遇到的bug一样 也是五六年前的老代码了。顺带跑个题讲讲一个OOM的事情吧。那个服务大概启动一两周左右内存就满了fgc也回收不掉,只能重启。
跟前面讲的消息折叠里遇到的bug一样 也是五六年前的老代码了。顺带跑个题讲讲一个OOM的事情吧。那个服务大概启动一两周左右内存就会满不断地fgc回收不掉,只能重启。
把内存dump下来看到占内存最多的除了日志打印就都是同一个sql语句的缓存。特点是 where 条件里包含 `in (?, ? ...)` , 这里面的 `?` 可能有上万个, 而且不是固定数量。
@ -122,7 +122,7 @@ ORM 框架会把 sql 缓存下来,但是因为 `?` 的个数有几万种可能
ORM 框架是动不了了,在用的低版本的没法处理这个问题, 又不敢升级版本。保底咱还能把 `?` 分批多执行几次sql至少让内存不爆是吧。但我又挣扎了一下想看看完整的业务逻辑感觉不会有什么功能是必须要用这么奇葩的sql实现的。结果就发现相关的数据库操作明显有优化过一波的痕迹mysql前面加过一层redis确认这个sql查询是没删干净的直接删了就完事。
回到正题,"mysql前面加过一层redis" 它是怎么加的呢,业务逻辑已经记不清了, 但我永远忘不了,它把 list 类型的数据整个 get() 出来, 添加一个元素,再 set() 回去。
回到正题,"mysql前面加过一层redis" 它是怎么加的呢,业务逻辑已经记不清了, 但我永远忘不了,它把 list 类型的数据整个列表从 redis 里 get() 出来, 添加一个元素,再 set() 回去。
## 缓存与DB的数据一致性

View File

@ -0,0 +1,276 @@
---
layout: pages
title: JVM 和 JDK 中的锁
categories: Java
tags: [jvm, lock]
---
锁......就是关住厕所门,让别人进不来,能独占茅坑的那玩意儿。占坑的花样多了,就出现了各种不同的花样手法的名字。每个名字还都用锁结尾,吓得不查攻略都不敢靠近城市里的公共卫生间。
把最后一个锁字拿掉。把它们读作 乐观的、悲观的、可重入的、不可重入的、公平的,非公平的、自旋的、偏向、读写分离的 就会舒服很多。 这些只是形容词,这些不是锁,我们不要从形容词开始学。爆多的、没见过的、甚至是不贴切的形容词是老手之间交流的内行话、装逼话。
新手应该先了解锁的各个型号。型号才能精确对应实物。把实物观察明白的话,甚至可以自己再编几个新的形容词出来。
希望这篇笔记,能帮助我们,消除对锁的恐惧,无视形容词。
## 自己实现一把锁
如果要造轮子,自己实现一把锁,应该怎么做呢?
--
首先需要一个共享的标志位,用来给各个线程抢占,抢占的意思,就是谁先给标志位设置了值,差不多就算谁获得了锁。
如果标志位是空的,说明没人拿到锁,这时候是可以去抢的。
标志位有值,标记的是其他线程,那就该做 阻塞,重试,等待重试,放弃执行 之类的操作了。
标志位有值,而且是自己线程标记的,那就可以做 继续运行 之类的操作了。
释放锁的操作,就是把这个标志位设置回空。如果其他线程没抢到锁后的操作是阻塞,还得记得把他们叫起来,通知他们可以再来试着抢锁了。
--
上面的逻辑,严重依赖标志位,抢着给标志位设置值的时候,至少要 `get()` 一次,判断下当前值是可抢的,才能去 `set()` 值来抢锁。三步操作才能完成。这三步操作首先要是原子的,不能被打断。其次是要可见的,原子的修改操作结束后,其他线程看到的一定是修改完后的新数据。
这就出现了 要有鸡,就得先有蛋 的情况。。不过至少,我们把锁的执行流程给剥离出来了,问题只剩下要一步操作实现 `compareAndSet()` 了。
--
总结一下,做个名词解析。我们把实现一把锁,拆成了两个部分。“锁的执行流程” 和 “蛋的问题”。
“锁的执行流程” 的不同,对应的就是文章第一段占茅坑中提到的 “占坑花样” 的不同, 也对应第三段中用 C++/Java 实现的 “主要逻辑” 的不同。
“蛋的问题” 如何解决,可以寻求他人的帮助。比如 操作系统内核就提供了一些系统调用。比如 cpu 支持 `cmpxchg` 指令来比较并交换数据,多核的时候这个指令前面还可以加个 `lock`。再比如 redis 的 setNx() 命令。
思维再放开点,比如 mysql 的:
```sql
update table_name set version = version + 1 where version = 1
```
--
记住 `lock cmpxchg`,记住 `compareAndSet()`, 记住 `CAS`: **C**ompare**A**nd**S**et/**C**ompare**A**nd**S**wap。
## 旧型号的锁
旧型号的锁这里专指旧版JDK中的 synchronized 关键字,调用操作系统内核里互斥量的实现来解决蛋的问题,锁的执行流程则在 JVM 里,用 C++ 写的。
以 synchronized 对一个普通Java对象加锁为例。
--
先要了解对象在堆中的结构它由几个部分组成开头是8个字节的 markWord。之后是一个指针指向方法区里的Class对象然后是一个数组对象才有的4字节记录数组长度。再往后就是成员变量的值或是指针。最后是一些占位符用来占满8字节的整数倍以方便整存整取。
展开点说,
指向类信息的指针 和 指向成员变量的指针 两类指针上面都没有提到占几个字节要看指针压缩有没有生效生效就是4字节不生效就是8字节。由两个jvm参数分别配置这两类指针的压缩与否默认开启但系统可用内存大小比jvm参数的优先级更高大内存32G以上就不会启用指针压缩。由程序员在编码阶段就确定对象的大小能做到一件很好玩的事情对象从主存加载到cpu缓存中也是整存整取的单位叫缓存行64字节。假设有一个多线程共享的数组实现的列表每个线程使用列表里的不同元素列表里每个元素又很小在元素对象的有效的成员变量前后加上适量的成员变量来占位就能保证一个cpu缓存行只能存在一个元素线程对各个元素的并发操作就没有缓存一致性协议的开销。
方法区里的Class对象是一个c++对象而且不在堆内所以c++对象里还有一个指针指向堆中的Class对象堆中的Class对象是java对象可以用来反射用。
markWord 是多种数据混用的一个区域有2个比特的标志位标识当前的数据含义。比如某个含义下GC年龄就存在这个区域。
--
synchronized 对 java Object 的加锁,就操作了 markWord 区域,把数据设置成一个指针,指向一个 ObjectMonitor c++对象。这个c++对象里就保存了 当前锁的拥有者线程、抢锁的线程集合、锁的重入次数 等等。
这已经到 jvm 的 c++ 代码了,没法看得太深。不过已经可以联想到一些推论了。比如 wait()、 notify() 为什么都是 java Object 提供的方法? 因为它们操作的就是 java Object 关联的 c++ ObjectMonitor 里保存的线程集合。比如 wait()、 notify() 都只在 synchronized 代码块里面调用。
--
做个特点总结,给 synchronized 加几个形容词:
重量级的:因为系统调用开销大
互斥的:只有一个线程能加锁成功
悲观的:先获取锁再执行,心里就是默认了肯定有人在抢了嘛
可重入的:同一个线程可以加锁多次,有个计数器记着次数
非公平的:这个特点是使用的那些内核调用带的
对象的:
## 新型号的锁
新型号的锁,这里指的是 java.util.concurrent 包下的那些类。放眼望去,这个包下的锁几乎都使用了 AQS。
### AQS
`AQS`**A**bstract**Q**ueued**S**ynchronizer是 java.util.concurrent 包下的一个较为底层的类, 它规定并提供了一套数据结构 和 一套模板方法。
--
数据结构,包含一个 `volatile int state` 和 一个 自定义的 `Node` 类型组成的链表。
state 的含义是不固定的上层的锁实现自己去规定。ReentrantLock 里含义是上锁与否 和 锁的重入次数。CountDownLatch 里是 Count 的值。ReentrantReadWriteLock 里把 int 按二进制拆成了两瓣 分别标识读锁和写锁。
链表 是线程的等待队列。可以用不同的模式来使用队列。比如 Condition 模式下,队列不一定只有一条,可以通过 ConditionObject 新起一个队列,这样在唤醒线程的时候就可以只唤醒特定队列里的线程。标准的生产者消费者模型,就可以用 Condition 分开保存生产者和消费者线程。
--
模板方法,已经把 申请锁失败的线程加进等待队列、用 LockSupport 完成阻塞和唤醒 等等逻辑实现好了。主要留下未完成的 tryAcquire()、tryRelease()、tryAcquireShared()、tryReleaseShared()等方法。
看两个模板例子:
```java
public final void acquire(int arg) {// 申请锁, 子类可以把这个方法包装成互斥的 lock() 实现。
if (!tryAcquire(arg) && // tryAcquire() 需要子类实现一般用CAS操作去设置 state 的值。
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // addWaiter 用CAS操作设置队列的tail节点把当前线程加入等待队列。
// acquireQueued 里把线程 park() 阻塞。
selfInterrupt();
}
```
```java
public final boolean release(int arg) {// 释放锁, 子类可以把这个方法包装成互斥的 unLock() 实现。
if (tryRelease(arg)) {// tryRelease() 需要子类实现一般用CAS操作去设置 state 的值。
Node h = head;// 从等待队列里取一个线程。
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);// unpark() 唤醒线程
return true;
}
return false;
}
```
### ReentrantLock
ReentrantLock 里面,实现了两个版本的 AQS 实现类FairSync 和 NonfairSync。 构造方法不指定参数的时候,默认是 NonfairSync。
NonfairSync 加锁的源码截取如下:
```java
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;
}
```
看得出来它是可重入的,因为判断当前拥有锁的线程是自己的时候,`nextc = c + acquires` 然后返回了 true。
FairSync 加锁的源码截取如下,只有第五行跟 NonfairSync 有区别,
即使 c == 0是可以抢锁的状态也要看下队列里是不是已经有等待线程。有其他人已经在排队等待了就只返回 false让父类的模板代码把自己加到队列末尾排队。
```java
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
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;
}
```
ReentrantLock 还把 AQS 的 ConditionObject 暴露了出来,可以创建多个等待队列。
```java
public Condition newCondition() {
return sync.newCondition();
}
```
AQS.ConditionObject 类实现了 Condition 接口。每个 ConditionObject 里存着一条 Node 组成的等待队列。 ConditionObject 队列的 await()、signal() 方法,也已经由 AQS 模板实现好了。
## 展开讨论下CAS
### AtomicInteger
AtomicInteger 就强依赖 cpu 指令提供的 CAS 操作。核心代码长这样:
```java
// AtomicInteger 类的 incrementAndGet()
public final int incrementAndGet() {
// this, valueOffset 这俩参数:标明了 atomicInteger 对象里面的 volatile int value 成员变量在内存里的地址。
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
// unsafe.getAndAddInt()
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
v = getIntVolatile(o, offset);
} while (!compareAndSwapInt(o, offset, v, v + delta));// CAS 操作
return v;
}
```
--
自旋的这个形容词用在这里就很典型。自己不停的在循环而不是去阻塞线程释放cpu。
乐观的,
轻量级的,
看到有人把它叫做 “无锁的”。。。明明 CAS 就是锁啊。
还想再造一个形容词, 如果在循环里每次都自己阻塞一小段时间,到时间自己醒来 ,是不是可以叫 “自旋+阻塞”的。。。。简单的分布式锁里可以用。
### synchronized 的优化
偏向 这个词,只有 synchronized 的优化 可以涉及到了。通过这个例子,还可以借鉴一下 乐观还是悲观、轻量还是重量、自旋还是阻塞 应该怎么选型。
偏向的功能,也是有 jvm 参数配置的目前是默认开启jvm 启动4秒后生效。记得回来感受一下前几秒为啥不生效。
第一个线程来申请锁的时候,查看 markWord 状态是空白的。使用一次 CAS 操作,把自己的线程标识填入 markWord。之后自己再来申请锁看到 markWord 状态没变,就无须任何同步操作了。
第二个线程来的时候,查看 markWord 状态显示已经有人占用,就开始把锁升级成轻量级锁了,就是乐观锁,就是自旋锁。
统计自旋的消耗。最后升级到旧版的重量级锁。
--
jvm 里 c++ 的东西,只能看看书上说的,浅浅了解下了。
--
偏向 这个词就是换了个视角啊,其他形容词都从线程自身的角度出发的。偏向站在了上帝视角从并发量的角度来形容。就 ReentrantLock 在单线程的情况下,能说不是偏向的么。多线程时,不也自然而然升级了么。
--
并发量小,锁住的时间短,适合用乐观锁。
## 锁的本质
大胆的猜测一下锁的本质是串行化这个世界上最终能实现串行化的也许只有cpu从电路层面停掉多核、停掉线程调度才能实现。点到为止吧不可能再去了解cpu的电路原理啊。
再猜一下, 这个停掉多核、停掉线程调度,也可以是假的吧。只要从单个内存地址的角度看起来像是停的就行了。
* 文章目录
{:toc}

View File

@ -1,7 +1,7 @@
---
layout: pages
title:
categories: 架构师之路
categories: 杂谈
tags: [os, epoll]
published: false
---

View File

@ -0,0 +1,11 @@
---
layout: pages
title:
categories: 杂谈
tags: [os, epoll]
published: false
---
* 文章目录
{:toc}