锁
为什么加锁
- 并发会造成数据混乱
事务
- 原子性a
- 一致性c
- 隔离性i
- 持久性d
加锁的影响和优化点
-
线程切换的开销(缓存命中率)
加锁失败会进行线程切换
-
用户态和内核态的切换开销(栈的切换/寄存器切换)
synchronized重量锁的实现是 系统调用 会涉及到内核和用户态的切换(加锁成功和失败都会切换)
-
cas保证原子性,避免系统调用
cas 无系统调用
-
加锁粒度
表->段->行
-
互斥到共享
锁存在的问题
- 死锁
指标和分类
- 同步和异步(阻塞和非阻塞和带超时的阻塞)
- 互斥和读写
- 独占和共享
- 公平和非公平
- 可中断
- 可重入
- 分段
- 自旋
锁的组成
- 共享资源 比如state 必须是所有线程共享的
- 临界区 产生竞争的一段代码 加锁和释放锁中间的操作(左闭右闭区间)
- 原子性 只有一个线程可以更改共享资源 比如state+1 state-1
- 可见性 volatile
- 互斥性
java
synchronized
对象头
https://www.cnblogs.com/hongdada/p/14087177.html
状态机
无锁->偏向锁->轻量锁(自旋锁)->重量锁
状态转换关系
线程1进入临界区,检测到同步对象处于无锁状态,将线程id拷贝到对象头,同步对象升级为偏向锁,线程1退出时,检查同步对象头是偏向锁,不对锁进行释放
线程1再次进入临界区,检测到偏向锁的线程id是自己的id, 获取锁成功,线程1退出时,检查同步对象头是偏向锁,不对锁进行释放
线程2进入临界区,检测到偏向锁的线程id不是自己的id, 获取锁失败,将锁升级为轻量级锁(更新同步对象头的字段 设置是否为偏向锁为否),线程2进入循环获取锁,n次(可配置 默认15)
线程1退出时,判断同步对象头不是偏向锁,要把锁释放为无锁,此时循环中的线程2,可以获取到锁,并且设置为偏向锁(此时偏向的是线程2)
如果线程2在循环15层次之后,仍然没能获取到锁,那么进入阻塞,同步对象升级为重量锁(更新同步对象头,设置锁指针为一个监视器对象),
线程3进入临界区,检测对象头,已经是轻量锁,升级锁为重量锁,
线程释放锁时,检查同步对象头,
如果是偏向锁,不做任何处理,直接退出
如果是轻量锁,要把同步对象头中的偏向线程id给去掉(检查是不是自己的),轻量锁线程id给去掉,把同步对象置为无锁状态
如果是重量锁,要把同步对象中的偏向线程id给去掉,轻量锁线程id给去掉,检查重量锁的监视器对象是否还有等待状态的线程,进行唤醒,如果没有,把重量锁的监视器指针给去掉,把锁置为无锁状态
AQS
- 可中断
- state和等待队列
- 基于LockSupport实现
park和unpark实现线程的阻塞和唤醒,底层又是通过UNSAFE实现
- . 加锁失败,即cas更新共享资源state失败,加入等待队列,调用park,阻塞当前线程。
锁释放的时候,从当前阻塞队列中获取一个或全部进行唤醒
concurrenthashmap
java7
段锁
java8
行锁
CAS原子操作 链表为空插入第一个节点时是原子操作
synchronized 插入链表非第一个节点时 使用synchronized 锁住链表头节点
mysql
表锁
myisam
update where sex = 1(sex列不是索引)
行锁
innodb
依赖索引
update where index=1 (index是索引列)
间隙锁
(负无穷,a)(a,b)(b,无穷)
update where index > 2(index是索引列)
update where index<1
update where index<1 and index ❤️
乐观锁
修改时再加锁
select 不加锁
update 加锁
悲观锁
提前加锁
select for update
分布式锁
同单体应用的锁,分布式锁也需要共享资源来实现锁,共享资源需要由第三方中间件来实现,比如redis,zookeeper,mysql等
分布式锁一般是非阻塞的
要实现阻塞,需要自旋
redis
通过 nx 参数来实现CAS互斥
通过 ex 参数实现自动过期
加锁流程
- client1首先加锁,发现key的值为null,设置key为1 cas(key,null,1)
- client2其次加锁,发现key的值为1,加锁失败 cas(key,null,1)
解锁流程
- client1解锁,把key设置为null,或者删除key
- client2重试,发现key的值为null,加锁成功
- key超时,redis自动清除key,client2重试,发现key的值为null,加锁成功
特性:
-
可以通过ex 参数来设置有效期
优点: 保证不会产生死锁,客户端宕机之后,锁不会永久存在
缺点: 可能会在解锁之前就失效,过期时间不好确定
-
内存操作,性能高,持久性由redis保证
-
redis保证集群的高可用
-
没办法阻塞,客户端只能自旋循环检测加锁
缺点:
- 不能实现公平锁,先阻塞的不一定先唤醒
zookeeper
临时节点实现自动过期
临时顺序节点实现互斥
watch机制实现阻塞队列
加锁流程:
- client1 在创建lock1临时节点
- client2 发现已经存在lock1临时节点 加锁失败,创建lock2临时节点,并watch lock1
- client3 发现已经存在lock2临时节点,加锁失败,创建lock3临时节点,并watch lock2
解锁流程
- client1删除lock1临时节点,client2收到lock1节点删除事件,client2唤醒.
- client2宕机,事务失败,lock1临时节点删除,client2收到lock1节点删除事件,client2唤醒
mysql
- 悲观锁
通过 select for update 实现悲观锁
- 乐观锁
v
e
r
=
s
e
l
e
c
t
v
e
r
f
r
o
m
x
x
x
;
r
o
w
s
=
u
p
d
a
t
e
x
x
x
w
h
e
r
e
v
e
r
=
ver=select ver from xxx; rows=update xxx where ver=
ver=selectverfromxxx; rows=updatexxxwherever=ver;
rows==0 表示更新失败,表示加锁失败
rows>0 表示加锁成功
参考来源
- https://blog.csdn.net/qq_19007169/article/details/124591890
- JAVA 对象头分析及Synchronized锁 - hongdada - 博客园 (cnblogs.com)