- 拿到出现1.锁的一些理解:锁宏观上就是一定并发控制的手段,是一种解决问题的思想;其次此时一些经典的锁的实现,我们学习这些经典的锁,帮组我们理解并解决复杂问题
- 参考文章:
Markable:记号 reference:引用,参考
displaced: 被取代、位移
1. 定义锁
定义锁是指一种锁的思想,并不是指某一种具体的锁,这里不纠结锁的实现,只说思想带来的好处
1.1 乐观锁
理解:乐观锁、悲观锁并不是一个实际的锁,它是一个在并发竞态环境下的一种思想,
java
中并没有确定的方法或者关键字,它只是一个处理的流程或者策略
1.1.1 流程机制
- 在读取数据的时候,总是假设最好的情况,每次读取数据时认为数据不会被修改,这种情况就不会加锁,
当进行更新操作时,会判断这条数据是否被修改
每个数据都会维护一个版本值
- 获取数据的时候也会获取到版本值
更新数据的时候会先校验当前数据的版本值与取出来的时候是否一致
版本能控制能保证数据的一致性
-
1.1.3 乐观锁衍生出的CAS
它
Compare And Swap
是一种乐观的心态概念,也是一种比较交换的机制,简称CAS
机制- 它是假设一个线程在取数据的时候不会被其他线程更改数据,但是在更新数据的时候,会校验数据有没有被修改过
一旦检测到冲突,也就是版本号或者更新时间不一致,(重试机制很重要)他就会进行重试,直到没有冲突为止,具体的逻辑查看下面的
CAS
章节1.1.4 常见的实现
JDK1.5
之后基于CAS
的大量原子类AtomicXXX
都是乐观锁的实现1.2. 悲观锁
1.2.1 流程机制
悲观锁总是假设最坏的情况,每次读取数据时认为数据会被修改(即加锁)
当进行更新操作时,直接更新数据,结束操作后释放锁(此处才可以被其他线程读取)
1.2.2 使用场景
乐观锁适用于“读多,写少”,尽量减少加锁的开销。
悲观锁适用于“读少,写多”,尽量减少类似乐观锁重试更新引起的性能开销
1.2.3 常见的实现
经典的
Synchronized
关键字-
1.3 公平锁
理解:首先公平锁和非公平锁是一种锁的思想
- 公平锁在多线程下,对待每一个线程都是公平的,本质上线程会按照它们发送请求的顺序获取到锁
- ……
- 秒杀场景下,遵循先到先得的逻辑,可以使用公平锁让请求排队,依次获取资源
主要是理解
ReentrantLock
中实现的公平锁和非公平锁1.4 非公平锁
1.4.1 概念理解
非公平锁在多线程下,对待每一个线程是区分优先级的,这里可以联想到线程的优先级,非常类似,如果有相同情况要实现,可以包装线程优先级来实现非公平锁
1.4.2 性能比较
一般来说非公平是的性能要比公平锁的性能好
- 公平锁:如果另一个线程持有锁,或者有其它线程在等待队列中等待这个锁,那么新发出的请求的线程将被放入到队列中;也就是说公平锁的每一个请求都需要进入队列一次利用队列的特性以确保公平性
非公平锁:当锁被某个线程持有的时候,新发出请求的线程才会被放入队列;如果在发出请求的时候,锁编程可用状态,这个线程会跳过队列中的等待线程直接获取到锁
1.5 共享锁
1.6 排它锁
1.7 可重入锁
Synchronized、ReentrantLock它是可重入锁
```java increase: 增长、增大
// 下面的代码:如果有对象获取了increase()的锁,在里面有调用了someMethod()方法 // 那么也会自动获取someMethod()方法的锁 private synchronized void increase(){ i = i + 1; this.someMethod(); } private synchronized void someMethod(){ System.out.println(1); }
<a name="UUiy0"></a>
## <br />
<a name="R1U1f"></a>
# 2. 原子操作
<a name="Ot1qb"></a>
## 2.1 什么是原子操作
1. `cpu`的指令是原子性的,它不可分割
1. `i++`是不是原子操作呢?不是的
1. 读取i的值
1. 进行`i+1`
1. 写入新的值
<a name="xZUG2"></a>
## 2.2 竞争条件/线程安全问题
1. 竞争条件`(race condition)`,也叫竞争灾难`(race hezard)`
1. 什么是竞争条件,就是线程安全问题,多线程并发访问资源
1. 多线程并发执行可以提高程序效率,多线程访问同一资源,会引发线程安全问题
1. 存在线程安全的前提
1. 存在共享数据
1. 多个线程并发操作共享数据
5. 临界区:两个线程发生竞争的区域,也就是并发访问的资源区域
<a name="WLm48"></a>
## 2.3 死锁的进阶知识
<a name="Zly3n"></a>
# 3. 解决线程安全的方式
<a name="rX3yg"></a>
## 3.1 减少竞争
<a name="XA1PQ"></a>
### 3.1.1 ThreadLocal
1. 这里主要描述`ThreadLocal`如何减少竞争,具体在`java`中如何实现看单独的笔记
<a name="gIdjC"></a>
### 3.1.2 多线程分区执行
1. ........
<a name="EBkfO"></a>
## 3.2 原子操作的CAS机制
<a name="KDPRB"></a>
### 3.2.1 原理解析
1. 全称:`Compare and Swap`或者 `Compare And Set`
1. 字面意思:先比较,在替换
1. 它是一个`cpu`的指令,所以它是原子操作,所以它是线程安全的,它是通过操作系统底层实现的,`CAS`操作是无锁,非阻塞的
1. 作用:设置一个地址的值
1. 约束:要求指令使用方必须知道操作对象原来的值
1. 为什么说它想乐观锁机制
1. 因为每一次调用`CAS`的时候,都是认为传入的期望值和对象在内存的值是一样的
```c
// cpu提供的原子操作,执行的过程不会被打断
cas(&oldValue, expectedValue, targestValue)
// 例如:100和&i比较,如果相同就更新到目标值,不相同就拒绝
cas(&i, 100, 101)
// 单纯的操作CAS并不能解决,线程安全问题,需要稍微改造一下,也体现的乐观锁的思想
Boolean flag = cas(&i, 100, 101)
while(!flag){
// 什么都不做
}
6.2.2 Java中的CAS
// CAS的方法都是native的,是操作系统实现的,底层估是C语言写的,在sun.misc.Unsafe.class
// 最底层的三个源码
public final class Unsafe {
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);
/**
* @param var1:你想改变的对象
* @param var2:偏移量
* @param var4:你认为这个对象是多少,也就是期待值
* @param var5:你希望把这个对象的值改成多少
*/
public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);
// 流程分析:
// 1.var1、var2:前两个参数确定对象在内存中的值,var4比较,看是否相同
// 2.如果相同就把确定的对象的值改成var5
}
// Atomic中的CAS方法
public final boolean compareAndSet(boolean expect, boolean update) {
int e = expect ? 1 : 0;
int u = update ? 1 : 0;
return unsafe.compareAndSwapInt(this, valueOffset, e, u);
// 流程分析
// 1.操作系统的CAS会将内存值与expect值进行比较
// 2.如果相等就将update参数更新到内存,并且返回成功,不等则返回失败
}
6.2.3 ABA问题
这两个原子类(AtomicMarkableReference AtomicStampedReference)
实现了,带版本的CAS操
6.2.4 经典使用场景
AQS
中的addWaiter(Node mode)
方法使用到CAS
的方法eureka
中的服务下线场景 ,调用了AtomicBoolean
方法的CAS
操作Ribbon
的轮询策略-
3.3 Tas指令
可以看成一个
CAS
的特例,只能修改0/1
的数据tas(&lock){
return cas(&lock, 0, 1)
}
3.4 锁机制
让最多一个线程进入临界区,访问共享资源,俗称锁
(lock)
```java // 核心逻辑:在竞争区的入口处加锁,在出竞争去的出口解锁
int lock = 0;
// 第一个线程走到这里获取到锁 enter(){ while(&lock, 0, 1){ // cas失败的线程一直循环尝试改变lock的值看能否成功,相当于阻塞在这里 } }
// 竞争资源 i++;
// 解锁的逻辑 leave(){ lock = 0; }
<a name="ZPCsi"></a>
# 4. Java的锁机制
<a name="tZg8O"></a>
## 4.1 Synchronized
<a name="ySMNG"></a>
### 4.2.1 使用方式
<a name="aUZwu"></a>
#### 4.2.1.1 修饰实例方法
1. 修饰实例方法的格式`public synchronized void bill(...){...}`
1. 实例方法的锁
1. 它的锁就是当前调用该方法的对象(也就是new对象,或者是实例对象),也就是`this`指向的对象
1. 好处:同步方法被所有资源共享,方法所在的对象相对于所有的线程来说是唯一的,保证了锁的唯一
```java
// 这个实现类Runnable接口,并且里有一个非静态同步方法,并且被run()方法调用
Ticket ticket = new Ticket;
new Thread(ticket,"线程一").start
new Thread(ticket,"线程二").start
new Thread(ticket,"线程三").start
// 上面的情况,同步方法的锁就是"ticket"
4.2.1.2 修饰静态方法
- 静态方法不需要
new
对象就可以"类名.class"
调用,那么没有对象如何来确定锁呢? - 这时候就是给当前的类加锁,锁就是该静态方法所在类的
class
对象,可以使用"类名.class"
获取锁 假设上面的代码示例换成静态同步方法,那么锁就是
"Ticket.class"
4.2.1.3 修饰代码块
同步代码块的格式
synchronized(lock){...}
lock
是一个锁对象,可以是任意类型的对象,但是在多线程共享资源的时候,对象必须是唯一的所以锁对象的创建方法不能放到
run()
方法中,否则每一个线程在运行run()
方法的时候都会创建一个新的锁对象4.2.2 特性
互斥性:一个线程占有锁之后,后续的线程必须等待锁的释放,所有的锁都应该都这个特性
- 可重入性:什么是可重入锁查看本文章
1-7
记录的信息 -
4.2.3 互斥锁的简单原理
同步代码块是如何保证资源仅被一个线程访问(
同步方法的逻辑也是一样
)- 当第一个线程执行同步代码块的时候,首先会检查锁对象的标志位,默认情况下为
"1"
- 此时线程会执行同步代码块,并且修改标志位为
"0"
- 后续线程执行到该同步代码块的时候,由于锁对象标志位为
"0"
,所以后续线程阻塞 - 直到占用资源的线程执行完毕,释放锁,后续线程依然使用此逻辑不断执行
4.2.4 早期的实现原理
在JDK6
之前的Synchronized
是基于对象内部的监视器来实现的,监视器依赖于操作系统的互斥锁Mutex Lock
,这个版本的实现,在阻塞或者唤醒一个线程的时候(这里可以联想到线程的声明周期),都需要操作系统来帮忙,这就需要从用户态
切换到内核态
,而切换状态的开销是比较大的,甚至有可能比用户执行代码的时间还要长,所以这个版本的实现在锁竞争比较激烈的情况下性能是很差的4.2.5 对象在堆里面的存储
- 当第一个线程执行同步代码块的时候,首先会检查锁对象的标志位,默认情况下为
对象头:是实现
Synchronized
基础,存储对象自身的运行时数据,它的位数和虚拟机的位数对应Mark Word
:中文“标志词”,用来存储对象的运行时数据,例如:对象的HashCode
、GC
的分代年龄、锁状态的标志、线程锁持有的锁……- 类型指针:虚拟机通过这个指针来确定,这个对象是哪一个类的实例
- 数组长度:当对象是数组的时候才会有这一部分
- 实例数据,它是对象的具体数据
对其填充:这部分起着占位符的作用,原因是目前主流的虚拟机
hotspot
,要求对象的大小必须是八字节的整数倍4.2.6 锁升级策略
图中的数据简单解释
- 对象包含五中锁状态,每种状态下
Mark Word
的信息是不同的 - 锁可以升级但不能降级
- 重要级锁:就是
JDK6
之前借助操作系统实现的锁机制 - 锁升级之后
GC Age
、HashCode
等对象去哪里了?- 在栈帧中的所记录空间的
_displaced_header
属性中 - 详细请看:https://gorden5566.com/post/1019.html
- 在栈帧中的所记录空间的
- 对象包含五中锁状态,每种状态下
一个小知识
是指锁总是会偏向已经拿到锁的线程,它主要用来优化同一个线程多次申请同一个锁的情况
- 如果在某些情况下,只有一个线程持续使用锁资源,那么使用偏向锁,性能将大幅度提升
- 如何开启/关闭偏向锁,在启动的
VM options
中:- 关闭:
-XX:-UseBiasedLocking
- 开启:
-XX:+UseBiasedLocking
,默认就开启,但是它是项目启动5
秒钟之后才开启,设置偏向锁开启即启动参数:-XX:BiasedLockingStartupDelay=0
- 关闭:
为什么偏向锁可以提升性能
轻量级锁是一种乐观锁,适用于多线程竞争比较弱的情况
- 轻量级锁的自旋操作
- 早期自旋操作是比较机械的,需要设置自旋的次数,从
JDK7
开始,自旋次数就变成自适应,JDK
会根据实际情况动态的改变自旋次数 - 线程如果自旋成功的话,那么下一次的自选次数就会增多,虚拟机就会人为,那么上一次成功了,下一次就会再成功
- 反之如果某个锁很少能够自旋成功,那么虚拟机就会减少甚至取消自旋次数
4.2.6.3 重量级锁
查看“早期的实现原理”章节4.2.6.4 升级流程
新版本的Synchronized的底层实现原理,就是尽量的避免掉操作系统的切换开销,而是操作Mark Word里面的数据,根据里面数据的状态去实现偏向锁和轻量级锁
4.2.7 优化
4.2.7.1 锁分级
这个就是上面将的一大堆,升级策略的内容4.2.7.2 锁消除
```java public class Demo{ public static void main(String[] args){ this.someMethod(); } private static void someMethod(){ Object o = new Object(); synchronize(o){
} } }System.out.println("11");
- 早期自旋操作是比较机械的,需要设置自旋的次数,从
// 上面的条件不满足,线程安全的条件,没有共享数据,Object是局部变量 // JDK会通过逃逸分析,检测出这里没有线程安全问题,会自动进行锁消除
<a name="IYp0B"></a>
#### 4.2.7.3 锁粗化
我是锁初花我是锁初花我是锁初花我是锁初花我是锁初花我是锁初花我是锁初花我是锁初花我是锁初花我是锁初花我是锁初花我是锁初花我是锁初花我是锁初花我是锁初花我是锁初花我是锁初花
<a name="e21TC"></a>
## 4.2 AQS
1. 全称是`AbstractQueueSynchronizer`
1. 用于构建锁和同步容器的框架,简化一些同步的细节,`concurrent`包下的很多都是基于它实现的,例如`ReentrantLock、Semaphore、FutureTask`
1. 知识点
1. `Semaphore`:信号量,大致的意思是一个厕所有五个坑位,一群人来抢占这些坑位,坑位就代表信号
![image.png](https://cdn.nlark.com/yuque/0/2022/png/26687455/1655216982887-6be54012-6006-44c0-a293-43b831284f7d.png#clientId=u8f02f0a9-b7de-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=284&id=udc96a6fa&margin=%5Bobject%20Object%5D&name=image.png&originHeight=568&originWidth=1584&originalType=binary&ratio=1&rotation=0&showTitle=false&size=285401&status=done&style=none&taskId=uc7d1ef6e-0073-4274-a194-b7dd970b028&title=&width=792)
addWaiter():把线程包装成node,扔到尾节点,mode有独占模式,例如ReentrantLock的lock方法<br />分享模式:ReentrantReadWriteLock
<a name="iciYI"></a>
## 4.3 ReentrantLock
<a name="C2cnf"></a>
### 4.3.1 Api的入门使用
1. `lock`:获取锁,没有任何返回值
1. `tryLock`:尝试获取锁,如果失败返回`fasle`
1. `tryLock(long timeout, TimeUnit unit)`:带超时时间的尝试获取锁,失败返回`false`
1. `lockInterruptibly()`:如果当前线程`Interrupt`了就抛异常,如果没有就去获得锁
1. `unLock`:解锁
<a name="UzQs0"></a>
### 4.3.2 特性
1. 互斥性:一个线程占有锁之后,后续的线程必须等待锁的释放,所有的锁都应该都这个特性
1. 可重入性:什么是可重入锁查看本文章`1-7`记录的信息
1. 公平性:支持公平锁和非公平锁,默认是非公平锁,查看本文章`1-3、1-4`记录信息
<a name="p3ojE"></a>
### 4.3.3 Condition组件
1. 替代传统线程通信中,`object`的`wait()`和`notify()`方法,使用起来比它们更加简单高效
1. 核心`Api`
1. `await`:对应`wait()`
1. `signal`:对应`notify()`
1. `signalAll`:对应`notifyAll()`
3. 操作`Condition`的代码,必须在`lock`的保护之下,也就是说必须在`lock`和`unlock`代码之间
<a name="ERpMf"></a>
### 4.3.4 源码分析
<a name="S6wnT"></a>
#### 4.3.4.1 tryLock()
```java
public class demo {
/**
* ReentrantLock的tryLock()方法源码
*
* @param acquires 默认是1,前面调用的时候传递的
* @return
*/
final boolean nonfairTryAcquire(int acquires) {
// 获取到当前线程
final Thread current = Thread.currentThread();
// 返回同步状态的当前值,也就是获取到一个状态,指的是锁的计数器
int c = getState();
// 如果这个状态是0,代表的是还没有线程拿到锁
if (c == 0) {
// 尝试通过CAS操作将当前线程设置为独占线程,也就是指拿到锁
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// ReentrantLock可重入性的实现: 如果当前线程已经拿到锁,那么状态就+1
// 所以这里很自然的再unlock方法中需要将State-1
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) {
throw new Error("Maximum lock count exceeded,翻译一下: 超过最大锁定计数");
}
setState(nextc);
return true;
}
return false;
}
}
4.3.4.1 lock()
public class demo {
/**
* 入口实现
*/
public final void lock() {
// 先尝试使用CAS操作尝试获取锁,失败执行acquire逻辑
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
} else {
acquire(1);
}
}
/**
* 获取方法,属于AQS
*
* @param arg
*/
public final void acquire(int arg) {
// 先尝试tryAcquire,其实就是tryLock的源码
// 进入acquireQueued获取队列的方法
if (!tryAcquire(arg) && acquireQueued(addWaiter(AbstractQueuedSynchronizer.Node.EXCLUSIVE), arg)) {
// 不通过,线程就阻断一直等待
selfInterrupt();
}
}
/**
* 获取队列,属于AQS
*
* @param node AbstractQueuedSynchronizer.Node.EXCLUSIVE传递的是独占模式
* @param arg
* @return
*/
final boolean acquireQueued(final AbstractQueuedSynchronizer.Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (; ; ) {
final AbstractQueuedSynchronizer.Node p = node.predecessor();
// 总的来说就是一个死循环,什么时候node在头结点并且尝试获取锁成功,然后才返回
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
}
4.3.5 比较
- 两种锁
Synchronized
和ReentrantLock
- 相同点:两者都实现互斥性和可重入性
- 实现机制的区别
Synchronized
是由JVM
实现的,是C
语言编写的ReentrantLock
是由JDK
实现的
- 性能上的区别
Synchronized
在JDK1.6
之前的Mutex Lock
版本性能很差JDK1.7
之后两者性能差不多
- 使用的区别
- 前者使用简洁,有JVM控制解锁
- 后者手动加解锁
功能区别
也称为读写锁,适合读多写少的场景,它包含两种锁
ReadLock
:读锁,也称共享锁WriteLokc
:写锁,也称排它锁
- 线程允许获取读锁的条件
- 不能有其它线程的写锁,写锁是具有排它性所以不允许
- 没有写请求,说明当前请求全部是读请求,只会产生读锁,因为读锁具有共享性
- 有写请求,但是调用线程和持有锁的线程是一个线程
- 线程允许获得写锁的条件
- 必须没有其它线程的读锁
- 没有其它线程的写锁
- 特性
- 互斥性:写锁支持,一个线程占有锁之后,后续的线程必须等待锁的释放,所有的锁都应该都这个特性
- 公平性:支持公平锁和非公平锁,默认是非公平锁,查看本文章
1-3、1-4
记录信息 - 可重入性:什么是可重入锁查看本文章
1-7
记录的信息 - 锁降级:看下面的代码
写线程“饥饿”
- 比如在大量的读请求的情况下,代码中全都是读锁,这个时候突然来了一个写请求,那么想要拿到写锁,就需要系统中不存在读锁,由于是大量的读请求,可能导致写线程一直拿不到锁
- 如果设置为公平锁,上述的问题可以解决,那么性能就会丢失 ```java /**
- 读写锁测试 *
- @author YiHua
@date 2022/6/15 */ public class ReentrantReadWriteLockTest {
/**
- 缓存数据 */ private Object data;
/**
- 缓存释放过期, 没过期 true 过期 false */ private volatile boolean cacheValid;
/**
- 读写锁 */ private final ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
/**
- 模拟缓存是否过期的场景:
*/
public void processCachedData() {
// 获取读锁
this.reentrantReadWriteLock.readLock().lock();
// 如果进入方法内,说明缓存过期,需要更新缓存,要切换为写锁
if (!this.cacheValid) {
} // 读取缓存 System.out.println(“已经有缓存并且没有过期, cacheValid = true”); // 释放读锁 this.reentrantReadWriteLock.readLock().unlock(); } } ```// 释放读锁
this.reentrantReadWriteLock.readLock().unlock();
// 获取写锁,注意: 获取写锁前必须释放读锁
this.reentrantReadWriteLock.writeLock().lock();
// 更新缓存
if (!this.cacheValid) {
data = new Object();
this.cacheValid = true;
}
// 获取读锁: 锁降级,释放写锁前获取读锁
this.reentrantReadWriteLock.readLock().lock();
// 释放写锁,依然持有读锁
this.reentrantReadWriteLock.writeLock().unlock();
4.5 StampedLock
是JDK 8引入的,它相当于是ReentrantReadWriteLock的增强版本
- 加锁解锁的流程
- 获得锁的方法,都会返回stamp,如果stamp=0表示操作失败,其它则表示成功
- 释放锁的方法,需要使用stamp作为参数,参数的值必须和获取锁时返回的一样才会成功
提供了三种访问模式
减少锁的持有时间,要求加锁代码执行比较快
- 较少锁的粒度,经典案例
JDK8
之前CurrentHashMap
- 锁粗化
- 偏向锁:锁总是会偏向已经拿到锁的线程
- 逃逸分析:分析变量是否能够逃出它的作用域
- aqs中的node是用volatile修饰
- 互斥性、可重入性
- Synchronized以前性能差的原因之一:从内核态到用户态的转换
�