public class TicketDemo {
static Object lock = new Object();
int ticketNum = 10;
public synchronized void getTicket() {
synchronized (this) {
if (ticketNum <= 0) {
return;
}
System.out.println(Thread.currentThread().getName() + "抢到一张票,剩余:" + ticketNum);
// 非原子性操作
ticketNum--;
}
}
public static void main(String[] args) {
TicketDemo ticketDemo = new TicketDemo();
for (int i = 0; i < 20; i++) {
new Thread(() -> {
ticketDemo.getTicket();
}).start();
}
}
}
1.2 Monitor
Monitor 被翻译为监视器,是由 JVM 提供,C++ 语言实现
在代码中想要体现 Monitor 需要借助 javap 命令查看 class 的字节码,比如以下代码:
public class SyncTest {
static final Object lock = new Object();
static int counter = 0;
public static void main(String[] args) {
synchronized (lock) {
counter++;
}
}
}
找到这个类的 class 文件,在 class 文件目录下执行javap -v SyncTest.class
,反编译效果如下:
- monitorenter 上锁开始的地方
- monitorexit 解锁的地方
- 其中被 monitorenter 和 monitorexit 包围住的指令就是上锁的代码
- 有两个monitorexit的原因,第二个monitorexit是为了防止锁住的代码抛异常后不能及时释放锁
在使用了 synchornized 代码块时需要指定一个对象,所以 synchornized 也被称为对象锁
加锁的变量必须是“对象实例”,不能是基本数据类型的变量
Monitor 主要就是跟这个对象产生关联,如下图
Monitor内部具体的存储结构:
- Owner:存储当前获取锁的线程的,只能有一个线程可以获取
- EntryList:关联没有抢到锁的线程,处于 Blocked 状态的线程
- WaitSet:关联调用了 wait 方法的线程,处于 Waiting 状态的线程
具体的流程:
- 代码进入 synchorized 代码块,先让 lock(对象锁)关联的 monitor,然后判断 Owner 是否有线程持有
- 如果没有线程持有,则让当前线程持有,表示该线程获取锁成功;
- 如果有线程持有,则让当前线程进入 entryList 进行阻塞,如果Owner持有的线程已经释放了锁,在 EntryList中的线程去竞争锁的持有权(非公平);
- 如果代码块中调用了 wait() 方法,则会进去 WaitSet 中进行等待;
参考回答:
-
Synchronized【对象锁】采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】
-
它的底层由 Monitor 实现的,Monitor 是 JVM 级别的对象( C++实现),线程获得锁需要使用对象(锁)关联 Monitor
-
在 Monitor 内部有三个属性,分别是 Owner、EntryList、WaitSet,其中
- Owner是关联的获得锁的线程,并且只能关联一个线程;
- EntryList 关联的是处于 Blocked 状态的线程;
- WaitSet 关联的是处于 Waiting 状态的线程
2. Monitor 实现的锁属于重量级锁,你了解过锁升级吗?
- Monitor 实现的锁属于重量级锁,里面涉及到了用户态和内核态的切换、进程的上下文切换,成本较高,性能比较低。
- 在 JDK 1.6 引入了两种新型锁机制:
- 偏向锁
- 轻量级锁
- 它们的引入是为了解决在没有多线程竞争或基本没有竞争的场景下因使用传统锁机制带来的性能开销问题。
2.1 对象的内存结构
大部分的 Java 对象都会存储在 HotSpot 的堆内存中,至少可以加锁的对象都在;
在 HotSpot 虚拟机中,对象在内存中存储的布局可分为 3 块区域:
- 对象头(Header)
- 实例数据(Instance Data)
- 对齐填充
对象头(Header) 还分为两个部分:
- Mark Word
- Klass Word
2.2 Mark Word
- hashcode:25位的对象标识 Hash 码
- age:对象分代年龄占 4 位
- biased_lock:偏向锁标识,占 1 位 ,0 表示没有开始偏向锁,1 表示开启了偏向锁
- thread:持有偏向锁的线程 ID,占 23 位
- epoch:偏向时间戳,占 2 位
- ptr_to_lock_record:轻量级锁状态下,指向栈中锁记录的指针,占 30 位
- ptr_to_heavyweight_monitor:重量级锁状态下,指向对象监视器 Monitor 的指针,占 30 位
我们可以通过lock的标识,来判断是哪一种锁的等级
- 后三位是 001 表示无锁
- 后三位是 101 表示偏向锁
- 后两位是 00 表示轻量级锁
- 后两位是 10 表示重量级锁
11 代表表示被标记为不可达,要被回收力
2.3 Monitor重量级锁
每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的Mark Word 中就被设置指向 Monitor 对象的指针
简单说就是:每个对象的对象头都可以设置monoitor的指针,让对象与monitor产生关联
2.4 轻量级锁
在很多的情况下,在 Java 程序运行时,同步块(synchronized 代码块)中的代码都是 不存在竞争的,不同的线程交替的执行同步块中的代码。
这种情况下,用重量级锁是没必要的。因此 JVM 引入了轻量级锁的概念。
static final Object obj = new Object();
public static void method1() {
synchronized (obj) {
// 同步块 A
method2();
}
}
public static void method2() {
synchronized (obj) {
// 同步块 B
}
}
加锁的流程(了解)
- 在线程栈中创建一个 Lock Record,将其 obj 字段指向锁对象。
- 通过 CAS 指令将 Lock Record 的地址存储在对象头的 mark word 中,如果对象处于无锁状态则修改成功,代表该线程获得了轻量级锁。
- 如果是当前线程已经持有该锁了,代表这是一次锁重入。设置 Lock Record 第一部分为 null,起到了一个重入计数器的作用。
- 如果 CAS 修改失败,说明发生了竞争,需要膨胀为重量级锁。
解锁过程(了解)
- 遍历线程栈,找到所有 obj 字段等于当前锁对象的 Lock Record。
- 如果 Lock Record 的 Mark Word 为 null,代表这是一次重入,将obj设置为 null 后 continue。
- 如果 Lock Record 的 Mark Word 不为 null,则利用CAS指令将对象头的 Mark Word 恢复成为无锁状态。如果失败则膨胀为重量级锁。
2.5 偏向锁
轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行 CAS 操作。
Java 6 中引入了偏向锁来做进一步优化:
- 只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头;
- 之后发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS;
- 以后只要不发生竞争,这个对象就归该线程所有;
static final Object obj = new Object();
public static void m1() {
synchronized (obj) {
// 同步块 A
m2();
}
}
public static void m2() {
synchronized (obj) {
// 同步块 B
m3();
}
}
public static void m3() {
synchronized (obj) {
}
}
加锁的流程(了解)
- 在线程栈中创建一个 Lock Record,将其 obj 字段指向锁对象。
- 通过 CAS 指令将 Lock Record 的线程 id 存储在对象头的 mark word 中,同时也设置偏向锁的标识为 101,如果对象处于无锁状态则修改成功,代表该线程获得了偏向锁。
- 如果是当前线程已经持有该锁了,代表这是一次锁重入。设置 Lock Record 第一部分为 null,起到了一个重入计数器的作用。与轻量级锁不同的时,这里不会再次进行 cas 操作,只是判断对象头中的线程 id 是否是自己,因为缺少了 cas 操作,性能相对轻量级锁更好一些。
- 解锁流程参考轻量级锁
解析的偏向锁升级流程(了解)(忽略一些细节)
示例:线程1当前拥有偏向锁对象,线程2是需要竞争到偏向锁。
- 线程 2 来竞争锁对象;
- 判断当前对象头是否是偏向锁;
- 判断拥有偏向锁的线程 1 是否还存在;
- 线程 1 不存在,直接设置偏向锁标识为 0 (线程 1 执行完毕后,不会主动去释放偏向锁);
- 使用 cas 替换偏向锁线程 ID 为线程 2,锁不升级,仍为偏向锁;
- 线程 1 仍然存在,暂停线程 1;
- 设置锁标志位为 00(变为轻量级锁),偏向锁为 0;
- 从线程 1 的空闲 lock record 中读取一条,放至线程 1 的当前 lock record 中;
- 更新 mark word,将 mark word指向线程 1 中 lock record的指针;
- 继续执行线程 1 的代码;
- 锁升级为轻量级锁;
- 线程 2 自旋来获取锁对象;
2.6 参考回答
Java 中的 synchronized 有
- 偏向锁
- 轻量级锁
- 重量级锁
三种形式,分别对应了锁
- 只被一个线程持有
- 不同线程交替持有锁
- 多线程竞争锁
三种情况。
描述 | |
---|---|
重量级锁 | 底层使用的Monitor实现,里面涉及到了用户态和内核态的切换、进程的上下文切换,成本较高,性能比较低。 |
轻量级锁 | 线程加锁的时间是错开的(也就是没有竞争),可以使用轻量级锁来优化。轻量级修改了对象头的锁标志,相对重量级锁性能提升很多。每次修改都是 CAS 操作,保证原子性 |
偏向锁 | 一段很长的时间内都只被一个线程使用锁,可以使用了偏向锁,在第一次获得锁时,会有一个CAS操作,之后该线程再获取锁,只需要判断 mark word 中是否是自己的线程id即可,而不是开销相对较大的 CAS 命令 |
一旦锁发生了竞争,都会升级为重量级锁
锁升级就是,无锁-偏向锁-轻量级锁-重量级锁
3. 你谈一谈JMM(Java 内存模型)
JMM (Java Memory Model),Java内存模型,是 java 虚拟机规范中所定义的一种内存模型。
描述了 Java 程序中各种变量(线程共享变量)的访问规则,以及在 JVM 中将变量存储到内存和从内存中读取变量这样的底层细节。
特点:
- 所有的共享变量都存储于主内存(计算机的RAM)这里所说的变量指的是实例变量和类变量。不包含局部变量,因为局部变量是线程私有的,因此不存在竞争问题。
- 每一个线程还存在自己的工作内存,线程的工作内存,保留了被线程使用的变量的工作副本。
- 线程对变量的所有的操作(读,写)都必须在工作内存中完成,而不能直接读写主内存中的变量,不同线程之间也不能直接访问对方工作内存中的变量,线程间变量的值的传递需要通过主内存完成。
4. CAS 你了解吗?
4.1 基本工作流程
CAS 的全称是: Compare And Swap,比较再交换,它体现的一种乐观锁的思想,在无锁情况下保证线程操作共享数据的原子性。
在 JUC 包下实现的很多类都用到了 CAS 操作
- AbstractQueuedSynchronizer(AQS框架)
- AtomicXXX类
例子:
我们还是基于刚才学习过的 JMM 内存模型进行说明
- 线程1与线程2都从主内存中获取变量 int a = 100,同时放到各个线程的工作内存中
三个参数,当前主内存值公共值 V、工作内存旧值 A、即将更新的值 B
当且仅当旧值 A 和内存值 V 相同时,将内存值修改为B并返回 true,否则什么都不做,并返回 false。如果CAS操作失败,通过自旋的方式等待并再次尝试,直到成功;
线程1操作:V:int a = 100,A:int a = 100,B:修改后的值:int a = 101 (a++)
- 线程1 拿旧值 A 的值与当前的公共值 V 进行比较,判断是否相等;
- 如果相等,则把 B 的值 101 更新到主内存中 ;
线程2操作:V:int a = 100,A:int a = 100,B:修改后的值:int a = 99 (a–)
- 线程 2 拿旧值 A 的值与当前的公共值 V 进行比较,判断是否相等;(目前不相等,因为线程 1 已更新 V 的值99)
- 不相等,则线程 2 更新失败,自旋重试…
自旋锁操作
- 因为没有加锁,所以线程不会陷入阻塞,效率较高,但特耗 CPU
- 如果竞争激烈,重试频繁发生,效率会受影响
伪代码:
class AtomicInteger {
private int V;
public void decrement() {
int A = V;
int B = A - 1;
while (CAS(V, A, B) != true) {
A = V;
B = A - 1;
}
}
}
需要不断尝试获取共享内存 V 中最新的值,然后再在新的值的基础上进行更新操作,如果失败就继续尝试获取新的值,直到更新成功
4.2 CAS 底层实现
CAS 底层依赖于一个 Unsafe 类来直接调用操作系统底层的 CAS 指令,是一条指令,原子的
都是 native 修饰的方法,由系统提供的接口执行,并非 java 代码实现,一般的思路也都是自旋锁实现
伪代码:
class AtomicInteger {
private int V;
public void decrement() {
int A = V;
int B = A - 1;
while (CAS(V, A, B) != true) {
A = V;
B = A - 1;
}
}
}
在 java 中比较常见使用有很多,比如 ReentrantLock 和 Atomic 开头的线程安全类,都调用了 Unsafe 中的方法
- ReentrantLock中 的一段 CAS 代码
4.3 乐观锁和悲观锁
- CAS 是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,我吃亏点再重试呗。
- synchronized 是基于悲观锁的思想:最悲观的估计,得防着其它线程来修改共享变量,我上了锁你们都别想改,我改完了解开锁,你们才有机会。
5. 谈一谈你对 volatile 的理解
一旦一个共享变量(类的成员变量、类的静态成员变量)被 volatile 修饰之后,代表变量不稳定,JVM 做出的优化可能会出错;
5.1 保证线程间的可见性
保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的,volatile 的变量每次修改会立即写入主内存,其他线程每次读 volatile 的变量都会读主内存到工作内存。
一个典型的例子:永不停止的循环
package com.itheima.basic;
// 可见性例子
// -Xint
public class ForeverLoop {
static boolean stop = false;
public static void main(String[] args) {
new Thread(() -> {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
stop = true;
System.out.println("modify stop to true...");
}).start();
foo();
}
static void foo() {
int i = 0;
while (!stop) {
i++;
}
System.out.println("stopped... c:"+ i);
}
}
当执行上述代码的时候,发现 foo() 方法中的循环是结束不了的,也就说读取不到共享变量的值结束循环。
主要是因为在 JVM 虚拟机中有一个 JIT(即时编辑器)给代码做了优化。
上述代码
while (!stop) { i++; }
代码块中并没有对变量的修改,在很短的时间内,这个代码执行的次数太多了,当达到了一个阈值,JIT 就会优化此代码,如下:
while (true) { i++; }
当把代码优化成这样子以后,及时
stop
变量改变为了false
也依然停止不了循环
解决方案:
-
在程序运行的时候加入vm参数
-Xint
表示禁用即时编辑器,不推荐,得不偿失(其他程序还要使用) -
在修饰
stop
变量的时候加上volatile
,表示当前代码禁用了即时编辑器,问题就可以解决,代码如下:static volatile boolean stop = false;
-
5.2 禁止指令重排序
用 volatile 修饰共享变量会在读、写共享变量时加入不同的屏障,阻止其他读写操作越过屏障,从而达到阻止重排序的效果;
在去获取上面的结果的时候,有可能会出现4种情况
情况一:先执行actor2获取结果—>0,0(正常)
情况二:先执行actor1中的第一行代码,然后执行actor2获取结果—>0,1(正常)
情况三:先执行actor1中所有代码,然后执行actor2获取结果—>1,1(正常)
情况四:先执行actor1中第二行代码,然后执行actor2获取结果—>1,0(发生了指令重排序,影响结果)
可以用压测工具 jcstress 去观察指令重排序,正常情况下是几乎看不到的,概率很低;
解决方案
- 在变量上添加volatile,禁止指令重排,则可以解决问题
- 写操作加的屏障是阻止上方其它写操作越过屏障排到 volatile 变量写之下
- 读操作加的屏障是阻止下方其它读操作越过屏障排到 volatile 变量读之上
其他补充
我们上面的解决方案是把 volatile 加在了 int y 这个变量上,我们能不能把它加在 int x 这个变量上呢?
下面代码使用 volatile 修饰了x 变量
这样显然是不行的,主要是因为下面两个原则:
- 写操作加的屏障是阻止上方其它写操作越过屏障排到 volatile 变量写之下
- 读操作加的屏障是阻止下方其它读操作越过屏障排到 volatile 变量读之上
所以,现在我们就可以总结一个 volatile 使用的小妙招:
- 写变量让 volatile 修饰的变量的在代码最后位置
- 读变量让 volatile 修饰的变量的在代码最开始位置
6. 什么是 AQS
全称是 AbstractQueuedSynchronizer,是阻塞式锁和相关的同步器工具的框架,它是构建锁或者其他同步组件的基础框架
6.1 AQS 与 synchronized 的区别
synchronized | AQS |
---|---|
关键字,c++ 语言实现 | java 语言实现 |
悲观锁,自动释放锁 | 悲观锁,手动开启和关闭 |
锁竞争激烈都是重量级锁,性能差 | 锁竞争激烈的情况下,提供了多种解决方案 |
AQS 常见的实现类
- ReentrantLock 阻塞式锁
- Semaphore 信号量
- CountDownLatch 倒计时锁
6.2 工作机制
- 在 AQS 中维护了一个使用了 volatile 修饰的 state 属性来表示资源的状态,0 表示无锁,1 表示有锁
- 提供了基于先进先出的等待队列,类似于 Monitor 的 EntryList
- 条件变量来实现等待、唤醒机制,支持多个条件变量,类似于 Monitor 的 WaitSet
如果多个线程共同去抢这个资源是如何保证原子性的呢?
在去修改 state 状态的时候,使用的 cas 自旋锁来保证原子性,确保只能有一个线程修改成功,修改失败的线程将会进入队列中等待
AQS是公平锁吗,还是非公平锁?
- 新的线程与队列中的线程共同来抢资源,是非公平锁
- 新的线程到队列中等待,只让队列中的 head 线程获取锁,是公平锁
比较典型的 AQS 实现类 ReentrantLock,它默认就是非公平锁,新的线程与队列中的线程共同来抢资源
7. ReentrantLock 可重入锁实现原理
ReentrantLock 翻译过来是可重入锁,相对于 synchronized 它具备以下特点:
- BLOCKED的线程可中断(将因为争夺 ReentrantLock 而正在阻塞中的线程打断)
- 可以设置超时时间
- 可以设置公平锁
- 支持多个条件变量
与 synchronized 一样,都支持重入
实现原理
ReentrantLock 主要利用 CAS + AQS 队列来实现。它支持公平锁和非公平锁,两者的实现类似
构造方法接受一个可选的公平参数(默认非公平锁)
- 当设置为 true 时,表示公平锁,否则为非公平锁
- 公平锁的效率往往没有非公平锁的效率高,在许多线程访问的情况下,公平锁表现出较低的吞吐量。
最后
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数网络安全工程师,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年网络安全全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上网络安全知识点!真正的体系化!
如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
9891a61e6f4301a4543aebede81c9d.png)
实现原理
ReentrantLock 主要利用 CAS + AQS 队列来实现。它支持公平锁和非公平锁,两者的实现类似
构造方法接受一个可选的公平参数(默认非公平锁)
- 当设置为 true 时,表示公平锁,否则为非公平锁
- 公平锁的效率往往没有非公平锁的效率高,在许多线程访问的情况下,公平锁表现出较低的吞吐量。
最后
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数网络安全工程师,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年网络安全全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
[外链图片转存中…(img-NExvWe4l-1715567224619)]
[外链图片转存中…(img-zKRGw1LJ-1715567224620)]
[外链图片转存中…(img-qeuR6JXf-1715567224620)]
[外链图片转存中…(img-G30hLjZA-1715567224620)]
[外链图片转存中…(img-mOABciee-1715567224620)]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上网络安全知识点!真正的体系化!
如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!