Java锁学习
[参考资料-B站课程]
一 基本锁类型
1.1 synchronized关键字
- 使用位置
- 可在方法上、代码块上
- 所属类型
- 互斥锁
- 悲观锁
- 重量级锁(在JDK1.6版本之前,因为涉及到其他线程被阻塞、线程上线文切换、线程的唤醒。所以OS的线程阻塞恢复的调度成本较高)
- 其他名称
- 同步锁
- 使用原理
- JDK1.6 之前版本,存在线程A B C ,若A已经获得锁,则B C 就会被加入到一个同步队列中阻塞等待(JDK 1.6版本之前是这个原理)
1.2 CAS 的概念
- CAS 比较并交换
- java的实现:compareAndSet ,compareAndSwap
- 又称为:无锁、乐观锁、自旋锁、轻量级锁
// CAS 实现方式之一, 自旋重试
public class AtomicInteger02 {
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger();
while (true) {
int oldValue = atomicInteger.get();
int newValue = oldValue + 1;
if (atomicInteger.compareAndSet(oldValue, newValue)) {
break;
}
}
}
}
-
特点
- 可见 没有线程发生阻塞,只是一直在循环重试,
-
CAS的问题
- 原子性问题
compareAndSet:
底层至少应该:
a.compare
b. set值
假如在步骤a 和b之间,有另一个线程执行了更新至的操作,则就会出问题了
该截图为X86平台下实现原子性比较并更新至的源码;
可见该底层是通过两条汇编指令:lock 和cmpxchgl实现的,“lock 和cmpxchgl”二者联合起来就是一条原子命令,不会被其他线程打断。 所以无锁的原子操作在底层的硬件级别(汇编指令)还是有lock的,只是成本低了。
- ABA问题(ABA 现象)
- 线程1:将数值从A 改成B ,由改成了A;
- 线程2:CAS时认为值没有改变,但是值本身已经改变了
- 总结:这是一种现象,不一定会引发问题,看具体情况而定;
- 解决方式:添加版本号,任何操作均增加版本号,设置标志位;
- 注意:轻量级锁不一定比重量级锁性能高。因为若存在大量线程循环CAS处理,高度并发,CAS一直无法处理成功,相当于弯路走的过多反而不如阻塞等待执行的效率高了。
1.3 锁优化
- synchronized 在1.6版本之后做了很多的优化
- 锁具有了状态的逐渐变化过程
(1). 无状态
(2). 偏向锁:偏向锁启用,一个线程加锁,将线程ID写入对象头的markword;
(3). 状态流转示意图
- 锁具有了状态的逐渐变化过程
- 场景1,虽然加了synchronized,但是大部分场景下只有一个线程来请求,存在资源争抢的情况占比较少。所以JDK1.6之后引入了偏向锁,即不使用复杂的底层机制。只将线程ID存到加锁对象内部暂存,即每次有方法调用时,只需检查存入的线程ID和当前请求调用的线程ID是否一致即可,若是同一个线程ID,则直接可调用方法; 若线程ID不一致,则锁升级;
- 偏向锁存在时应对大多数只有一个线程的情况,若出现了多个线程来请求执行同步方法,则升级到轻量级锁,轻量级锁的底层是通过自旋来实现的。但是自旋是很消耗CPU的,若自旋次数过多还不能获取到锁,则升级到重量级锁,会导致线程苏泽。
- 场景2:轻量级—> 升级到重量级锁。 弯路过度曲折则导致自旋处理耗时过长,或者永远无法成功了。即大量线程来加锁时,导致的大量的CPU进行while 空转自旋。还不如将多余的线程进入阻塞队列,直接切换到重量级锁。
- 总结:synchronized锁的优化就是锁根据情况自适应升级膨胀的过程。
1.4 锁优化升级二
- JAVA 对象的结构
- HostSpot虚拟机中,对象在内存中存储布局分为3块:- 对象头 Header
- 实例数据 Instance Data
- 对齐填充 Padding
- 在字节码层面,有monitorenter,monitorexit两个 方法;
- LongAdder 实现了分段CAS优化
- 实现一个线程一个初始值,最终再合并最终结果数值
- 最终返回结果时将所有值的结果进行汇总。
1.3 公平锁与非公平锁
- 代码示例
new Thread(() -> {
reentrantLock.lock();
try {
drayMoney();
} finally {
reentrantLock.unlock();
}
}, "T1").start();
## 解释
reentrantLock.lock(); // 尝试获取锁,会使得线程阻塞尝试获取锁;
a. 让线程阻塞的方式:wait(), sleep(),park(),while(true)
b.
- LockSupport.park()
- ReentrantLock源码
ReentrantLock reentrantLock = new ReentrantLock();
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
// 若线程加锁未成功,则进行排队并且将该排队线程进行park()处理,
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
// tryAcquire(arg) 表示尝试加锁成功, 即只有尝试加锁失败才执行后续逻辑
// 先看 FairSync 的实现
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
acquire(1);
}
/**
公平锁的源码
*/
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;
}
}
// 若state不为0,则判断已获得锁的线程和当前线程是否相同;即该处就是重入的关键逻辑
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
// 若同一个线程 两次获取了锁,则将state值+1;
setState(nextc);
return true;
}
// 返回false表示没有加上锁
return false;
}
}
}
4. 执行优先级 vs 提交优先级
- 提交优先级: 核心线程,存入队列,非核心线程
- 执行优先级:核心线程执行,非核心线程执行任务,去队列中执行任务;
- execute方法执行内部的逻辑为: