一、乐观锁和悲观锁
1、悲观锁
悲观锁: 以为本身在利用数据的时间肯定有别的线程来修改数据,因此在获取数据的时间会先加锁,确保数据不会被别的线程修改,synchronized和Lock的实现类都是悲观锁,恰当写操作多的场景,先加锁可以保证写操作时数据精确,体现的锁定之后再操作同步资源-----狼性锁
2、乐观锁
乐观锁: 以为本身在利用数据的时间不会有别的线程修改数据或资源,不会添加锁,Java中利用无锁编程来实现,只是在更新的时间去判定,之前有没有别的线程更新了这个数据,如果这个数据没有被更新,当前线程将本身修改的数据乐成写入,如果已经被其他线程更新,则根据差别的实现方式实行差别的操作,好比:放弃修改、重试抢锁等等。判定规则有:版本号机制Version,最常接纳的是CAS算法,Java原子类中的递增操作就通过CAS自旋实现的。-----恰当读操作多的场景,不加锁的特性能够使其读操作的性能大幅提升,乐观锁则直接去操作同步资源,是一种无锁算法,得之我幸不得我
🌟概念区别
特性乐观锁(Optimistic Lock)悲观锁(Pessimistic Lock)头脑以为并发辩论很少,操作时先不加锁,提交时再检测是否有辩论。以为并发辩论很常见,操作前先加锁,确保别人无法同时修改。实现方式版本号机制 或 时间戳机制:更新前先比力版本,差别等则更新失败。数据库锁机制:如 SELECT...FOR UPDATE,大概 synchronized / ReentrantLock。性能影响辩论少时,性能很好;辩论多时,频仍重试会影响性能。并发高时,线程等候多,吞吐量较低。应用场景读多写少,辩论概率低的场景。写多,辩论概率高的场景。
🔥实际案例
乐观锁:
- 数据库表有一个 version 字段。
- 取数据时,连同 version 一起读出。
- 更新时,利用:
- UPDATE table_name SET value = ?, version = version + 1 WHERE id = ? AND version = ?;
复制代码 - 如果 version 差别等,分析有别的线程修改了,更新失败,可以重试。
悲观锁:
- 数据库级别:
- SELECT * FROM table_name WHERE id = ? FOR UPDATE;
复制代码 查询时就加锁,其他变乱不能修改,等当前变乱提交/回滚后才华解锁。
- Java并发:
- synchronized (obj) { // 临界区代码 }
复制代码- Lock lock = new ReentrantLock(); lock.lock();
- try { // 临界区代码 }
- finally { lock.unlock(); }
复制代码
✅总结一句话:
- 乐观锁:恰当辩论很少的业务,提升并发性能,失败时重试。
- 悲观锁:恰当辩论很频仍的业务,直接锁住资源,确保安全但断送并发。
二、synchronized关键字分析
1、阿里Java规范:
高并发时,同步调用应该去考置锁的性能消耗。能用无锁数据结构,就不要用锁;能锁区块,就不要锁整个方法体﹔能用对象锁,就不要用类锁。
分析︰尽可能使加锁的代码块工作量尽可能的小,制止在锁代码块中调用RPC方法。
2、案例分析
- /**
- * @author Guanghao Wei
- * @create 2023-04-10 14:57
- */
- class Phone {
- public synchronized void sendEmail() {
- try {
- TimeUnit.SECONDS.sleep(3);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.println("------sendEmail");
- }
- public synchronized void sendSMS() {
- System.out.println("------sendSMS");
- }
- public void hello() {
- System.out.println("------hello");
- }
- }
- /**
- * 现象描述:
- * 1 标准访问ab两个线程,请问先打印邮件还是短信? --------先邮件,后短信 共用一个对象锁
- * 2. sendEmail钟加入暂停3秒钟,请问先打印邮件还是短信?---------先邮件,后短信 共用一个对象锁
- * 3. 添加一个普通的hello方法,请问先打印普通方法还是邮件? --------先hello,再邮件
- * 4. 有两部手机,请问先打印邮件还是短信? ----先短信后邮件 资源没有争抢,不是同一个对象锁
- * 5. 有两个静态同步方法,一步手机, 请问先打印邮件还是短信?---------先邮件后短信 共用一个类锁
- * 6. 有两个静态同步方法,两部手机, 请问先打印邮件还是短信? ----------先邮件后短信 共用一个类锁
- * 7. 有一个静态同步方法 一个普通同步方法,请问先打印邮件还是短信? ---------先短信后邮件 一个用类锁一个用对象锁
- * 8. 有一个静态同步方法,一个普通同步方法,两部手机,请问先打印邮件还是短信? -------先短信后邮件 一个类锁一个对象锁
- */
- public class Lock8Demo {
- public static void main(String[] args) {
- Phone phone = new Phone();
- new Thread(() -> {
- phone.sendEmail();
- }, "a").start();
- try {
- TimeUnit.MILLISECONDS.sleep(200);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- new Thread(() -> {
- phone.sendSMS();
- }, "b").start();
- }
- }
复制代码 第1 第2中案例分析,只要我的一个类中有方法加了synchronized 关键字,这个synchronized 锁的并不是当前该方法,而是整个资源类,也就是说可能该类有多个方法都加了synchronized 关键字,但是多线程的情况中,只有一个线程能够进入众多加了synchronized 方法中的一个方法。然后依次列队。换句话说,某一个时间内,只能有唯一的一个线程去访问这些synchronized 方法,锁的是当前对象this,被锁定后,其他的线程都不能进入到当前对象的其他的synchronized 方法。
第3个案例分析,平凡方法不必要竞争锁,直接实行。
第4个案例分析,对于平凡的方法加上了synchronized关键字,我们锁的是当前实例对象(this),如果有两个差别的实例对象,即是说差别的资源,以是也不会竞争锁。
第5 第6种案例分析,如果在静态方法上加锁,那么锁的是类,也就是说不管你有多少个实例,但实在都是同一个类,以是差别的实例也会被锁住。
第7 第8种案列分析,静态方法上加synchronized代表类锁,平凡方法加锁是实例锁,这两种锁不产生辩论,不会相互竞争。
3、synchronized的三种应用方式:
1)作用于实例方法,当前实例加锁,进入同步代码前要得到当前实例的锁;
2)作用于代码块,对括号里设置的对象加锁。
3)作用于静态方法,当前类加锁,进去同步代码前要得到当前类对象的锁;
第一 第三种跟上述案例分析的是一样的,偏重说一下第二中静态代码块:
对于代码块:
代码块的锁也有两种,一种是synchronized中加一个实例对象,一种是加类.class.这两种锁也分别代表实例锁跟类锁。
⚙️重点区别:
场景锁对象影响范围synchronized(obj)平凡对象实例 obj同一个对象的线程互斥,差别对象互不影响。synchronized(类名.class)类的Class对象(全局唯一)全部线程,不管用哪个对象实例,都会互斥。 📌形象表明:
1️⃣ synchronized(obj):
如果你 new 出许多个对象,每个对象本身用synchronized(this),那每个对象都像是本身的小屋,互不干扰。
2️⃣ synchronized(类名.class):
锁住的是整个类,哪怕差别的对象实例,线程也必须抢同一个锁,雷同一个“工厂总门”,只有拿到钥匙的人才华进去,别人都得等。
🌰举个例子:- class MyClass {
- public void instanceMethod() {
- synchronized(this) {
- System.out.println("对象锁:锁住的是当前实例");
- }
- }
- public static void staticMethod() {
- synchronized(MyClass.class) {
- System.out.println("类锁:锁住的是整个类");
- }
- }
- }
复制代码
- synchronized(this):只会锁住这个对象实例,差别实例之间互不干扰。
- synchronized(MyClass.class):锁住整个类,无论用哪个对象调用这个代码,只要一个线程进了,其他线程必须等!
4、表明分析
在java假造机种,class loader类加载器把 Car.class文件读进来,Car class就是类锁,这个就是模板,由一份模板可以天生 car1、car2、car3 三个实例对象,这是三个差别的对象但是均来自于一个模板。以是类锁对应的就是Car Class,在方法区中有且仅有一份,但是对于我们的对象锁,new出来的实例对象,在jvm的堆中。以是类锁跟对象锁,加锁的对象跟地方都不一样,自然就会产生差别的效果。
5、从字节码角度分析synchronized实现
从字节码角度分析,必要借助两个下令:
javap -c ***.class 对代码进行反编译
javap -v ***.calss 对文件进行反编译,但是会输出更多附加信息(包罗行号、当地变量表、反汇编等具体信息)
1)利用javap -c 反编译一个同步代码块的class文件:
代码如下:- public void m1() {
- synchronized(object) {
- System.out.println("----hello synchronized code block");
- }
- }
复制代码 输出如下:
上图就是改代码反编译后的源码,可以看到编译后的代码中,进入m1方法后,monitorenter代表得到锁并进入,正常实行完代码后monitorexit 代表开释锁退出。
以是可以得出结论对于java synchronized同步代码块,底层是靠monitorenter和monitorexit指令来保证锁的获取和开释。
第二个 monitorexit 代表有非常发生时,也会正常开释锁。正常情况下走前面的monitorexit,非常情况走背面的monitorexit。极度情况下,也会出现一对一的情况,在退出同步代码前抛出非常,此时就是一对一的情况,由于就没有正常情况了,无论那种情况都会抛出非常。
2)利用javap -v 反编译一个加了synchronized平凡同步方法的class文件:
由于必要看到具体的内容,以是利用javap -v.
代码如下:- public synchronized void m2() {
- System.out.println("----hello synchronized code block");
- }
复制代码 反编译后的代码如下所示:
根据反编译后的代码可以代码可以发现,只要方法上加了synchronized关键字,该方法上面就会加一个 ACC_SYNCHRONIZED 的标识 ,java假造机它来读这些字节码,发现有这个标识他就会这个方法加锁,保证只能有一个线程访问。
调用指令将会查抄方法的ACC_SYNCHRONIZED访问标志是否被设置,如果设置了,实行线程会将现持有monitor锁,然后再实行该方法,末了在方法完成(无论是否正常竣事)时开释monitor
3)利用javap -v 反编译一个 加了synchronized静态同步方法的class文件:
可以发现,静态同步方法反编译后也加上了 ACC_SYNCHRONIZED 标识,但是除了ACC_SYNCHRONIZED标识,还加上了一个ACC_STATIC 标识,这个标识就是区分类锁 和 对象锁的标识。
ACC_STATIC、ACC_SYNCHRONIZED访问标志区分该方法是否是静态同步方法
6、反编译synchronized锁的是什么
为什么任何一个对象都可以成为一个锁?
6.1 起首表明这个标题必要先明确什么是管程(Monitors)?
概念层面:管程(Monitor)是一种高层同步原语,可以看成是操作体系或步调语言中,专门用来办理多线程安全访问共享资源标题的一个抽象结构。
它的重要作用是:在多线程并发访问共享资源时,确保同一时间只有一个线程能够访问,并通过条件变量机制和谐线程之间的实行顺序。
结构层面:管程 = 锁 + 条件队列 + 资源
一个完整的管程由三部门构成:
构成部门作用互斥锁(Lock)保证在同一时候,只有一个线程能访问共享资源。条件变量(Condition Variable)用来实现线程间的等候/关照机制(wait/notify)。共享资源(Shared Resource)被多个线程并发访问的资源,管程掩护的焦点对象。
普通明确:
1)管程就像一个房间;
2)共享资源放在房间里;
3)房间只有一把锁,同一时候只能让一个线程进来,其他线程要么列队(进入互斥队列),要么自动 wait() 等候某个条件满意;
3)当条件满意,调用 notify() 或 notifyAll(),唤醒等候的线程。
以是,管程不但负责互斥访问,还负责线程协作。
但实在上述的明确还是有点抽象,我们可以做一个形象的比喻:
宰衡我们先捋一下涉及到的部门:
1) 管程的3个构成部门:互斥锁(Lock)、条件变量(Condition Variable)、共享资源(Shared Resource)构成了管程的焦点框架。
2)管程中提供的三个方法:wait()、notify()、notifyAll()管程提供的方法,用来共同条件变量完成“等候”和“唤醒”的逻辑。
我们要先明确一下这三个构成部门跟这三个方法之间是怎么协同工作的:
场景:食堂窗口打饭体系:
想象一个学校食堂窗口,门生列队打饭,窗口一次只能欢迎一个人,而且饭要做好才华领!
各元素比喻:
Java并发概念食堂比喻共享资源(Shared Resource)食堂窗口里的热饭,全部人都想要。互斥锁(Lock)打饭窗口——同一时间只能一个人站在窗口打饭。条件变量(Condition Variable)窗口旁边的“等候关照区”,专门给没饭的人等候利用。wait()饭没好,站在窗口的人去“等候关照区”列队等消息,并让出窗口。notify()饭做好了,服务员喊:“下一位可以来打饭啦!”notifyAll()饭做好了,服务员喊:“全部等着的同学都可以过来了!” 一个完整的流程:
1️⃣ 互斥访问资源:
- 同学想打饭(线程访问临界区)。
- 必要先站到窗口(获取锁)。
- 窗口一次只能有一个人(锁的互斥性)。
2️⃣ 条件不满意,等候:
- 如果饭还没做好(共享资源不可用)。
- 服务员会说:“去旁边等,等我叫你再来。”(调用 wait())。
- 同学自动走到“等候关照区”,并且让出窗口(开释锁)。
- 别的同学可以来实验列队。
3️⃣ 资源可用,关照唤醒:
- 饭终于做好了,服务员喊:“可以打饭啦!”
- 如果用 notify():随机唤醒一个等候关照区的同学。
- 如果用 notifyAll():关照全部等候区的人都可以返来打饭了。
4️⃣ 重新竞争锁:
- 被叫到的同学们重新列队,等候窗口空出来(重新竞争锁)。
- 谁先站上窗口(获取锁),谁先打饭。
5️⃣ 正常打饭:
🎯 总结一句话:
共享资源 = 热饭,各人都想要。
锁 = 窗口,防止多人同时打饭。
条件变量 = 旁边的等候关照区,饭没好时在这列队。
wait() = 自动脱离窗口,去等关照区列队,开释窗口。
notify() = 唤醒一个列队的同学,返来实验打饭。
notifyAll() = 唤醒全部列队的同学,各人抢着返来列队打饭。
🧠 💡 英华影象法:- 没有锁 → 没法进食堂;
- 饭没好 → 被服务员赶去“等待区” → wait();
- 饭好了 → 服务员喊人 → notify() / notifyAll();
- 被喊到的人 → 重新去排队 → 获取锁;
- 打到饭的人 → 走人,释放锁;
- 其他人 → 继续排队,循环往复。
复制代码 以是管程包罗了互斥访问 + 等候关照机制,帮我们自动完成列队、等候和唤醒。
管程(Monitors):可以看做一个软件模块,它是将共享的变量和对于这些共享变量的操作封装起来,形成一个具有肯定接口的功能模块,进程可以调用管程来实现进程级别的并发控制。
Java假造机可以支持方法级的同步和方法内部一段指令序列的同步,这两种同步结构都是利用管程(Monitor,更常见的是直接将它称为“锁”)来实现的。
方法级的同步是隐式的,无须通过字节码指令来控制,它实如今方法调用和返回操作之中。假造机可以从方法常量池中的方法表结构中的ACC_SYNCHRONIZED访问标志得知一个方法是否被声明为同步方法。当方法调用时,调用指令将会查抄方法的
ACC_SYNCHRONIZED访问标志是否被设置,如果设置了,实行线程就要求先乐成持有管程,然后才华实行方法,末了当方法完成(无论是正常完成还黑白正常完成)时开释管程。在方法实行期间,实行线程持有了管程,其他任何线程都无法再获取到同一个管程。如果一个同步方法实行期间抛出了非常,并且在方法内部无法处置惩罚此非常,那这个同步方法所持有的管程将在非常抛到同步方法界限之外时自动开释。
6.2 假造机中是怎样实现的
如果要弄清楚假造机中的synchronized是怎样实现的,必要先弄清楚几个类的界说:
Object.java
ObjectMonitor
ObjectMonitor.cpp
ObjectMonitor.hpp
1)Object
起首object是全部类的根类,全部类默认都继承它,但在底层,它不但仅是一个类名,而是再内存中的一段数据结构。
JVM中Object的内存结构:
注意这是在底层jvm中的结构,并不是java代码中类结构。
┌─────────────────────┐
│ 对象头 (Object Header) │ ←重点!锁信息、GC信息都在这里
├─────────────────────┤
│ 实例数据 (Instance Data) │
├─────────────────────┤
│ 对齐填充 (Padding) │
└─────────────────────┘
对象头 (Object Header) :一样平常有两部门构成
部门形貌Mark Word存储运行时数据(锁状态、哈希码、GC标志等)。Klass Pointer指向类元数据的指针,分析对象的范例(对应哪个Java类)。 注意:我们所要相识的锁的相关信息就存储在 Mark Word 中!
实例数据(Instance Data):存储对象的真正字段内容,例如:- public class Person {
- int age;
- String name;
- }
复制代码 实例数据就会包罗 age 和 name 的具体值,按照 JVM 对齐规则排列。
填充(Padding):为了让对象的内存地点按照 8 字节或 16 字节对齐,JVM会根据实际情况补充空白字节,以提升 CPU 访问服从。
这是我们最底子的object的结构。
2)ObjectMonitor
ObjectMonitor 并不是java内部的类,而是底层HotSpot JVM 内部实现的锁对象,它是利用c++代码实现的。也称为独立的对象监督器。
Object 跟 ObjectMonitor 的关系:
起首Java中的Object对象跟ObjectMonitor是逐一对应的关系,但是并不是在Java代码中的逐一对应,而是当创建了一个对象时,在JVM的底层会有一个ObjectMonitor与Object对象进行对应。
上面我们先容过Object在jvm中的结构是 对象头 (Object Header)、实例数据 (Instance Data) 、对齐填充 (Padding)吗,那我们Object与ObjectMonitor对应就是通过对象头中的 Mark Word 来进行关联的。
Mark Word :
- 如果对象处于无锁状态,Mark Word 存储哈希码。
- 如果被线程加锁,Mark Word 存储线程ID或 ObjectMonitor 地点。
- 如果对象被GC标志,Mark Word 存储 GC 标志位。
关系图:
ObjectMonitor 的作用:
ObjectMonitor是 JVM 底层负责线程等候、唤醒、列队的看门人,意思就是说Java 对象本身不实行加锁逻辑,真正的加锁控制是由 ObjectMonitor 完成的。当锁升级为“重量级锁”,Mark Word 中会保存一个指针,指向一个 ObjectMonitor 对象。
重量级锁 涉及到锁的状态,下面具体说。
总之我们可以总结一下,在java层面利用 synchronized(obj) 关键,进行加锁时,加锁的操作并不是在java层面实现的,而是在jvm的底层 ObjectMonitor 来实现线程之间的等候、唤醒、列队的功能,以是可以说ObjectMonitor是重要负责加锁相关逻辑的实现的。
ObjectMonitor 的结构:
ObjectMonitor其重要数据结构如下(hotspot源码ObjectMonitor.hpp):- ObjectMonitor() {
- _header = NULL; //对象头 markOop
- _count = 0;
- _waiters = 0,
- _recursions = 0; // 锁的重入次数
- _object = NULL; //存储锁对象
- _owner = NULL; // 标识拥有该monitor的线程(当前获取锁的线程)
- _WaitSet = NULL; // 等待线程(调用wait)组成的双向循环链表,_WaitSet是第一个节点
- _WaitSetLock = 0;
- _Responsible = NULL ;
- _succ = NULL ;
- _cxq = NULL ; //多线程竞争锁会先存到这个单向链表中 (FILO栈结构)
- FreeNext = NULL ;
- _EntryList = NULL ; //存放在进入或重新进入时被阻塞(blocked)的线程 (也是存竞争锁失败的线程)
- _SpinFreq = 0;
- _SpinClock = 0;
- OwnerIsThread = 0;
- _previous_owner_tid = 0;
- }
复制代码
ObjectMonitor的基本工作机制:
1 当多个线程同时访问一段同步代码时,起首会进入 _EntryList 队列中。
2 当某个线程获取到对象的Monitor后进入临界地区,并把Monitor中的 _owner 变量设置为当前线程,同时Monitor中的计数器 _count 加1。即得到对象锁。
3 若持有Monitor的线程调用 wait() 方法,将开释当前持有的Monitor,_owner变量规复为null,_count自减1,同时该线程进入 _WaitSet 聚集中等候被唤醒。
4 在_WaitSet 聚集中的线程会被再次放到_EntryList 队列中,重新竞争获取锁。
5 若当前线程实行完毕也将开释Monitor并复位变量的值,以便其它线程进入获取锁
ObjectMonitor ObjectMonitor.cpp ObjectMonitor.hpp 这三个类之间的关系
这个涉及到C++编程的知识,我们只必要简朴相识知道:
ObjectMonitor→ObjectMonitor.cpp→objectMonitor.hpp
ObjectMonitor.cpp、objectMonitor.hpp 是真正实现 ObjectMonitor 底层逻辑的代码。
名称编写语言作用ObjectMonitor.hppC++头文件用C++语法声明 ObjectMonitor 类的结构、成员变量、方法接口。ObjectMonitor.cppC++源文件用C++语法实现 ObjectMonitor 的方法,完成锁的加锁、解锁、等候、唤醒等举动。ObjectMonitor 对象JVM运行时实例C++写好的 ObjectMonitor 类在JVM运行时根据必要创建的内存对象,管理线程同步。
管程与Object、ObjectMonitor ObjectMonitor.cpp ObjectMonitor.hpp的关系:
Object(Java对象)
ObjectMonitor(监督器对象)
ObjectMonitor.hpp/.cpp(实当代码)
管程是一种头脑模型,Java对象通过和 ObjectMonitor 的组合,把管程的功能落地了,ObjectMonitor.hpp/.cpp 则是实现这个机制的 C++代码。
7)相关口试题
1 ObjectMonitor 的 _object 字段有什么作用?
答:_object 字段指向被锁定的 Java 对象。当我们利用 synchronized 关键字时,被锁定的对象就会与一个 ObjectMonitor 关联,_object 就指向这个对象。
2 _owner 字段记录的是什么?它有什么用?
答:_owner 字段记录当前持有锁的线程。它雷同图书馆的借阅记录,记录着哪个线程正在利用这个锁。当一个线程乐成获取锁时,_owner 就会被设置为这个线程;当锁被开释时,_owner 被清空。
3 _WaitSet 和 _EntryList 分别存放什么样的线程?它们有什么区别?
答:_WaitSet 存放调用了 wait() 方法的线程,这些线程在等候特定条件满意。它雷同图书馆的苏息区,读者(线程)在这里等候被关照。
_EntryList 存放正在竞争锁的线程。它雷同图书馆门口的列队队伍,读者(线程)在这里列队等候获取锁。
它们的重要区别在于,_WaitSet 中的线程是自动开释锁并等候条件满意,而 _EntryList 中的线程是在竞争锁的过程中被壅闭。
4 _recursions 字段的作用是什么?为什么必要记录重入次数?
答:_recursions 字段记录锁的重入次数。重入是指同一个线程多次获取它已经持有的锁。_recursions 的作用就是跟踪同一线程重复获取锁的次数,雷同记录同一读者多次借阅同一本书。
记录重入次数是为了支持锁的重入特性。由于 Java 的 synchronized 是可重入的,允许一个线程多次获取同一个锁。记录重入次数可以制止重入时出现死锁,同时也简化了编程。
5 当多个线程竞争同一个锁时,ObjectMonitor 是怎样和谐的?
答:当多个线程竞争锁时,ObjectMonitor 会按照如下方式和谐:
如果锁是空闲的,第一个到达的线程会直接得到锁,_owner 被设置为这个线程,_recursions 设为 1。
如果锁已经被其他线程持有,到达的线程会被放入 _EntryList,并被挂起(park)。
当锁被开释时,_owner 被清空,_recursions 被重置为 0,_EntryList 中的一个线程会被唤醒并重新实验获取锁。
如果一个线程在持有锁的情况下调用 wait(),它会被放入 _WaitSet,并开释锁。当它被 notify() 唤醒时,会重新进入 _EntryList 竞争锁。
6 ObjectMonitor 怎样实现锁的重入?
答:ObjectMonitor 通过 _recursions 字段实现锁的重入。当一个已经持有锁的线程再次获取锁时,ObjectMonitor 会查抄 _owner 是否为当前线程,如果是,就将 _recursions 加 1,允许线程重入。
当线程退出一层 synchronized 块时,_recursions 会减 1。只有当 _recursions 减为 0 时,锁才会被真正开释,其他线程才有机会得到锁。
7 ObjectMonitor 在 wait()/notify() 中饰演什么脚色?
答:ObjectMonitor 的 _WaitSet 字段是 wait()/notify() 机制的焦点。当一个线程调用 wait() 时,ObjectMonitor 会将这个线程移入 _WaitSet,并开释锁。这个线程会不停等候,直到其他线程调用 notify()。
当 notify() 被调用时,ObjectMonitor 会从 _WaitSet 中选一个线程,将其移入 _EntryList。这个线程then会重新到场锁的竞争。如果它乐成得到锁,就会从 wait() 调用中返回,继承实行。
8 wait和sleep的区别?
wait()方法属于Object类;sleep()方法属于Thread类的静态方法;
wait()方法让本身让出锁资源进入等候池等候,直接让出CPU,后续要继承竞争monitor锁才华可运行;sleep是继承占用锁(依赖于体系时钟和CPU调理机制),会让出CPU;
sleep()必须指定时间,wait()可以指定时间也可以不指定;sleep()时间到,线程处于可运行状态,超时大概interrupt()能唤醒
wait()方法只能在同步方法或同步代码块中调用,否则会报illegalMonitorStateException非常,需利用notify()方法来唤醒;而sleep()能在任何地方调用;
wait()方法只能在同步方法或同步代码块中调用缘故因由是:制止CPU切换到其它线程,而其它线程又提前实行了notify方法,那这样就达不到我们的预期(先wait,再由其它线程来notify),以是必要一个同步锁来掩护。
wait是对象的方法,java锁是对象级别的,而不是线程级别的;同步代码块中,利用对象锁来实现互斥效果
三、公平锁和非公平锁
1、什么是公平锁、非公平锁、
公平锁:
指多个线程按照中哀求锁的顺序来获取锁,这里雷同列队买票,先来的人先买厥后的人在队尾排着,这是公平的- //true表示公平锁,先来先得
- Lock lock = new ReentrantLock(true);
复制代码 非公平锁:
是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先中请的线程优先获取锁,在高并发情况下,有可能造成优先级翻转大概饥饿的状态(某个线程不停得不到锁)\- //false表示非公平锁,后来的也可能先获得锁。空参默认非公平锁
- Lock lock = new ReentrantLock(false);
复制代码
2、口试题:
为什么会有公平锁/非公平锁的操持?为什么默认非公平?
规复挂起的线程到真正锁的获取还是偶然间差的,从开辟职员来看这个时间微乎其微,但是从CPU的角度来看,这个时间差存在的还是很显着的。以黑白公平锁能更充实地利用CPU的时间片,只管减少CPU空间状态时间。利用多线程很紧张的考量点是线程切换的开销,当接纳非公平锁时,当一个线程哀求锁获取同步状态,然后开释同步状态,以是刚开释锁的线程在如今再次获取同步状态的概率就变得很大,以是就减少了线程的开销。
什么时间用公平?什么时间用非公平?
如果为了更高的吞吐量,很显然非公平锁是比力合适的,由于节省了许多线程切换的时间,吞吐量自然就上去了;否则就用公平锁,各人公平利用。
3、预埋伏AQS
后续深入分析
四、可重入锁(递归锁)
1、概念
可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时间,再进入该线程的内层方法会自动获取锁(前提,锁对象得是同一个对象),不会由于之前已经获取过还没开释而壅闭。
如果是1个有 synchronized修饰的递归调用方法,步调第2次进入被本身壅闭了岂不是天大的笑话,出现了作茧自缚。以是Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可肯定程度制止死锁。
一个线程中的多个流程可以获取同一把锁,持有这把同步锁可以再次进入。本身可以获取本身的内部锁
2、可重入锁的分类
1)隐式锁(即synchronized关键字利用的锁)默认是可重入锁
指的是可重复可递归调用的锁,在外层利用锁之后,在内层仍旧可以利用,并且不发存亡锁,这样的锁就叫做可重入锁。
简朴的来说就是:在一个synchronized修饰的方法或代码块的内部调用本类的其他synchronized修饰的方法或代码块时,是永久可以得到锁
2)显式锁(即Lock)也有ReentrantLock这样的可重入锁
我以为区别就是一个体现调用一个隐式调用,体现调用时,肯定要保证 lock 跟 unlock 逐一对应。
3)演示代码:
- /**
- * @author Guanghao Wei
- * @create 2023-04-10 16:05
- */
- public class ReEntryLockDemo {
- public static void main(String[] args) {
- final Object o = new Object();
- /**
- * ---------------外层调用
- * ---------------中层调用
- * ---------------内层调用
- */
- new Thread(() -> {
- synchronized (o) {
- System.out.println("---------------外层调用");
- synchronized (o) {
- System.out.println("---------------中层调用");
- synchronized (o) {
- System.out.println("---------------内层调用");
- }
- }
- }
- }, "t1").start();
- /**
- * 注意:加锁几次就需要解锁几次
- * ---------------外层调用
- * ---------------中层调用
- * ---------------内层调用
- */
- Lock lock = new ReentrantLock();
- new Thread(() -> {
- lock.lock();
- try {
- System.out.println("---------------外层调用");
- lock.lock();
- try {
- System.out.println("---------------中层调用");
- lock.lock();
- try {
- System.out.println("---------------内层调用");
- } finally {
- lock.unlock();
- }
- } finally {
- lock.unlock();
- }
- } finally {
- lock.unlock();
- }
- }, "t2").start();
- }
- }
复制代码 4)Synchronized的重入的实现机理
每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针。
当实行monitorenter时,如果目的锁对象的计数器为零,那么分析它没有被其他线程所持有,Java假造机会将该锁对象的持有线程设置为当前线程,并且将其计数器加1。
在目的锁对象的计数器不为零的情况下,如果锁对象的持有线程是当前线程,那么Java假造机可以将其计数器加1,否则必要等候,直至持有线程开释该锁。
当实行monitorexit时,Java假造机则需将锁对象的计数器减1。计数器为零代表锁已被开释。
五、死锁
1、概念
死锁是指两个或两个以上的线程在实行过程中,因争取资源而造成的一种相互等候的征象,若无外力干涉那它们都将无法推进下去,如果体系资源富足,进程的资源哀求都能够得到满意,死锁出现的可能性就很低,否则就会因争取有限的资源而陷入死锁。
产存亡锁重要缘故因由:
体系资源不足
资源分配不妥
进程运行推进的顺序不合适
2、死锁代码演示
- /**
- * @author Guanghao Wei
- * @create 2023-04-10 16:20
- */
- public class DeadLockDemo {
- static Object a=new Object();
- static Object b=new Object();
- public static void main(String[] args) {
- new Thread(() -> {
- synchronized (a){
- System.out.println("t1线程持有a锁,试图获取b锁");
- try {
- TimeUnit.SECONDS.sleep(1);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- synchronized (b){
- System.out.println("t1线程获取到b锁");
- }
- }
- },"t1").start();
- new Thread(() -> {
- synchronized (b){
- System.out.println("t2线程持有a锁,试图获取a锁");
- try {
- TimeUnit.SECONDS.sleep(1);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- synchronized (a){
- System.out.println("t2线程获取到a锁");
- }
- }
- },"t2").start();
- }
- }
复制代码 3、死锁怎样排查
1)纯下令行:
jps -l //查出当进步程编号是多少
jstack 进程编号 //打印出进程编号地点的栈信息
2)图形化
jconsole
六、小总结
synchronized 小总结(紧张)
指针指向monitor对象(也称为管程或监督器锁)的起始地点。每个对象都存在着一个monitor与之关联。当一个montor被某个线程持有后,它便处于锁定状态。在Java假造机(HotSpot)中,monitor是由ObjectMonitor实现的,其重要数据结构如下(位于HotSpot假造机源码ObjectMonitor.hpp文件,C++实现的)
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |