【黑马Java并发笔记】三、互斥与同步(上)
文章目录
- 1 共享带来的问题
- 2 synchronized解决方案
- 3 常见线程安全类
- 4 Monitor锁
- 4.1 Java对象头
- 4.2 Monitor
- 5 synchronized优化
- 5.1 轻量级锁
- 5.2 锁膨胀
- 5.3 自旋优化
- 5.4 偏向锁
- 5.5 批量重偏向和批量撤销
- 5.5.1 批量重偏向
- 5.5.2 批量撤销
- 5.6 锁粗化和锁消除
- 5.6.1 锁粗化
- 5.6.2 锁消除
1 共享带来的问题
临界区
一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区
竞态条件
多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件
2 synchronized解决方案
synchronized实际上使用对象锁保证了临界区内代码的原子性,临近区内的代码对外是不可分割的,不会被线程切换所打断
3 常见线程安全类
- String
- 基本类型包装类
- StringBuffer
- Random
- Vector
- Hashtable
- java.util.concurrent包下的类
- 没有成员变量的类一般都是线程安全的
这里说它们是线程安全的是指,多个线程调用它们同一个实例的某个方法时,是线程安全的。
每个方法都是原子的。多个方法组合在一起不是原子的。
下面是一个多个方法组合不是原子的例子:
Hashtable table = new Hashtable();if (table.get("key") == null) {table.put("key", value);
}
不可变类线程安全性
String、Integer等都是不可变类,因为其内部状态是不可变的。
问: String为什么要设计成final的?
答: 防止子类继承后重写方法导致线程不安全。
4 Monitor锁
4.1 Java对象头
以32位虚拟机为例:
普通对象
通过Klass Word来判断对象的类型,是一个指针指向对象所属类的元数据
元数据区的概念出现在Java8以后,在Java8以前称为方法区,元数据区也是一块线程共享的内存区域,主要用来保存被虚拟机加载的类信息、常量、静态变量以及即时编译器编译后的代码等数据。
数组对象
其中,Mark Word结构为:
4.2 Monitor
Monitor被译为监视器或管程。当需要对一个对象使用重量级锁的时候,会被分配一个Monitor。结构如下:
synchronized内出现异常仍然会释放锁的原因
static final Object lock = new Object();
static int counter = 0;public static void main(String[] args) {synchronized (lock) {counter++;}
}
对应的字节码指令为:
public static void main(java.lang.String[]);...Code:stack=2, locals=3, args_size=10: getstatic #2 // lock引用, synchronized开始3: dup // 复制一份引用4: astore_1 // lock引用 -> slot 15: monitorenter // 将lock对象MarkWord置为Monitor指针6: getstatic #3 // <- counter9: iconst_1 // 准备常数110: iadd // +111: putstatic #3 // -> counter14: aload_1 // <- lock引用15: monitorexit // 将lock对象MarkWord重置,唤醒EntryList16: goto 2419: astore_2 // e -> slot 220: aload_1 // <- lock引用21: monitorexit // 将lock对象MarkWord重置,唤醒EntryList22: aload_2 // <- slot 2(e)23: athrow // throw e24: return// 代码6-16中间如果发生异常,会来到代码19,之后仍然会释放锁Exception table:from to target type6 16 19 any19 22 19 any
5 synchronized优化
5.1 轻量级锁
- 轻量级锁的使用场景:如果一个对象虽然有多线程访问,但多线程访问的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化
- 轻量级锁对使用者是透明的,即语法仍然是synchronized
假设有两个方法同步块,利用同一个对象加锁:
public static void method1() {synchronized (obj) {method2();}
}public static void method2() {synchronized (obj) {}
}
- 创建锁记录(Lock Record)对象,每个线程的栈都会包含一个锁记录的结构,内部可以存储锁定对象的Mark Word
JVM 的Lock Record简介
- 让锁记录中Object reference指向锁对象,并尝试用cas替换Object的Mark Word,将Mark Word的值存入锁记录
对象头结尾01表示无锁,可以进行加锁。
- 如果cas替换成功了,对象头中存储了锁记录地址和状态00,表示由该线程给对象加锁,这时图示如下:
- 如果cas失败,有两种情况:
- 如果是其他线程已经持有了该Object的轻量级锁,这时表明有竞争,进入锁膨胀过程
- 如果是自己执行了synchronized锁重入,那么再添加一条Lock Record作为重入的计数
- 解锁
- 当退出synchronized代码块(解锁)时如果有取值为null的锁记录,表示有重入,这时重置锁记录,表示重入计数减一
- 当退出synchronized代码块(解锁)时锁记录的值不为null,这时使用cas将Mark Word的值恢复给对象头
- 成功,则解锁成功
- 失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程
5.2 锁膨胀
如果再尝试加轻量级锁的过程中,CAS操作无法成功,这时一种情况就是其他线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁
- 当Thread-1进行轻量级加锁时,Thread-0已经对该对象加了轻量级锁:
这时Thread-1加轻量级锁失败,进入锁膨胀流程:
- 为Object对象申请Monitor锁,让Object指向重量级锁地址
- 然后自己进入Monitor的EntryList BLOCKED
轻量级锁没有阻塞的概念
- 当Thread-0退出同步代码块解锁时,使用cas将Mark Word的值恢复给对象头,失败。这时会进入重量级锁解锁流程,即按照Monitor地址找到Monitor对象,设置Owner为null,唤醒EntryList中BLOCKED线程
5.3 自旋优化
重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。
自旋重试成功的情况:
线程 1(Core 1上) | 对象Mark | 线程2(Core 2上) |
---|---|---|
- | 10(重量锁) | - |
访问同步块,获取monitor | 10(重量锁)重量锁指针 | - |
成功加锁 | 10(重量锁)重量锁指针 | - |
执行同步块 | 10(重量锁)重量锁指针 | - |
执行同步块 | 10(重量锁)重量锁指针 | 访问同步块,获取monitor |
执行同步块 | 10(重量锁)重量锁指针 | 自旋重试 |
执行完毕 | 10(重量锁)重量锁指针 | 自旋重试 |
成功解锁 | 01(无锁) | 自旋重试 |
- | 10(重量锁)重量锁指针 | 成功加锁 |
- | 10(重量锁)重量锁指针 | 执行同步块 |
… | … | … |
自旋重试失败的情况:
线程 1(Core 1上) | 对象Mark | 线程2(Core 2上) |
---|---|---|
- | 10(重量锁) | - |
访问同步块,获取monitor | 10(重量锁)重量锁指针 | - |
成功加锁 | 10(重量锁)重量锁指针 | - |
执行同步块 | 10(重量锁)重量锁指针 | |
执行同步块 | 10(重量锁)重量锁指针 | 访问同步块,获取monitor |
执行同步块 | 10(重量锁)重量锁指针 | 自旋重试 |
执行同步块 | 10(重量锁)重量锁指针 | 自旋重试 |
执行同步块 | 10(重量锁)重量锁指针 | 自旋重试 |
执行同步块 | 10(重量锁)重量锁指针 | 阻塞 |
- | … | … |
- 自旋会占用CPU时间,单核CPU自旋就是浪费,多核CPU自旋才能发挥优势
- 在Java6之后自旋锁是自适应的,比如对象刚刚的一次自旋成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之比较智能
- Java7之后不能控制是否开启自旋功能
5.4 偏向锁
轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行CAS操作。
Java6中引入了偏向锁来做进一步优化:
只有第一次使用CAS将线程ID(OS对线程的唯一标识,不是Java中的)设置到对象的Mark Word头,之后发现这个线程ID是自己的就表示没有竞争,不用重新CAS。以后只要不发生竞争,这个对象就归该线程所有
偏向状态
回忆对象头格式:
一个对象创建时:
public static void main(String[] args) {Dog d1 = new Dog(); // 对象头 后三位0x1 正常Thread.sleep(4000);Dog d2 = new Dog(); // 0x5 偏向
}
- 如果开启了偏向锁(默认开启),那么对象创建后,markword值为0x05即最后三位为
101
,这时它的thread、epoch、age都为0 - 偏向锁是默认延迟的,不会在程序启动时立即生效。如果想避免延迟,可以加VM参数
XX:BiasedLockingStartupDelay=0
来禁用延迟
public static void main(String[] args) {Dog d1 = new Dog(); // 0x5 偏向
}
- 如果没有开启偏向锁,那么对象创建后,markword值为0x01即最后三位为
001
,这时它的hashcode、age都为0,第一次用到hashcode时才会赋值
偏向锁例子
public static void main(String[] args) {Dog d = new Dog(); // 0x5synchronized (d) {// 进入到同步块后// [thread_id 54] [epoch 2] [unused 1] [age 4] 101}// 离开后对象头和同步块中相同// [thread_id 54] [epoch 2] [unused 1] [age 4] 101
}
处于偏向锁的对象解锁后,线程id仍存处于对象头中
禁用偏向锁
-XX:-UseBiasedLocking
第二个减号表示禁用,加号启用(默认)
public static void main(String[] args) {Dog d = new Dog(); // 0x1synchronized (d) {// 进入到同步块后// [ptr_to_lock_report 54] 00 (轻量级锁)}// 解锁后// 0x1
}
偏向锁撤销
- 调用对象的
hashCode()
方法会禁用这个对象的偏向锁(对象头存不下了) - 重写的
hashCode()
不会
问: 为什么轻量级锁和重量级锁对象头不保存hashcode字段呢?
答: 因为保存在Lock Record和Monitor中 - 偏向锁冲突升级为轻量级锁也会撤销偏向
- wait/notify属于重量级锁的方法,也会撤销偏向锁
偏向锁的撤销需要在全局安全点进行(全局安全点可以参考这篇文章聊聊JVM(九)理解进入safepoint时如何让Java线程全部阻塞)重偏向还是升级成轻量级锁取决于偏向线程的存活状态。
具体的偏向锁过程可以参考偏向锁居然也会用到lock record,其实偏向锁和轻量级锁都会使用Lock Record,用于记录锁重入。而在Monitor中有单独的字段来记录:
#ObjectMonitor.hppObjectMonitor() {//记录无锁状态的Mark Word_header = NULL;_count = 0;//等待锁的线程个数_waiters = 0,//线程重入次数_recursions = 0;//指向的对象头_object = NULL;//锁的本身,指向线程或者Lock Record_owner = NULL;//调用wait()方法后等待锁的队列_WaitSet = NULL;//等待队列的锁_WaitSetLock = 0 ;_Responsible = NULL ;_succ = NULL ;//ObjectWaiter 队列_cxq = NULL ;FreeNext = NULL ;//ObjectWaiter 队列_EntryList = NULL ;_SpinFreq = 0 ;_SpinClock = 0 ;OwnerIsThread = 0 ;_previous_owner_tid = 0;}
5.5 批量重偏向和批量撤销
5.5.1 批量重偏向
如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程T1的对象仍然有机会重新偏向T2,充偏向会重置对象的Thread ID
当撤销偏向锁阈值超过20次后,JVM认为偏向出错,于是会在给这些对象加锁时重新偏向至加锁线程
Vector<Dog> list = new Vector();
new Thread(() -> {for (int i = 0; i < 30; i++) {Dog d = new Dog();list.add(d);synchronized (d) {// [thread1_id] 0...0 101}}synchronized (list) {list.notify();}
}).start();new Thread(() -> {synchronized (list) {list.wait();}for (int i = 0; i < 30; i++) {Dog d = list.get(i); // [thread1_id] 0...0 101synchronized (d) { // 前19次会升级为轻量级锁 [record] 00// 从第20次开始,会将剩下的对象重偏向至t2// 弹幕:这是类和线程组合级别计数,而不是对象级别,对象之间是独立的}// 轻量级锁解锁后的对象变为 0x1}
}).start();
5.5.2 批量撤销
视频中的例子:
t1
运行:dog0
~dog38
偏向于t1
t2
运行:dog0
~dog18
升级为轻量级锁并释放为0x1普通对象
::此时共撤销了19个偏向锁::t2
继续运行:dog19
~dog38
批量重偏向于t2
::dog19
进行计数+1,达到阈值20后再和剩余对象重偏向::
::此时共撤销了19 + 1 = 20个偏向锁::t3
运行:dog0
~dog18
轻量级锁(0x1处于不可偏向状态)t3
继续运行:dog19
~dog38
从偏向于t2
升级为轻量级锁
::此时共撤销了20 + 20 = 40个偏向锁::main
运行:创建dog39
直接不可偏向
JVM启发式源码
源码解析-触发批量撤销或批量重偏向的条件
static HeuristicsResult update_heuristics(oop o, bool allow_rebias) {markOop mark = o->mark();// 如果不是偏向模式直接返回if (!mark->has_bias_pattern()) {return HR_NOT_BIASED;}// 获取锁对象的类元数据Klass* k = o->klass();// 当前时间jlong cur_time = os::javaTimeMillis();// 该类上一次批量重偏向的时间jlong last_bulk_revocation_time = k->last_biased_lock_bulk_revocation_time();// 该类单个偏向撤销的计数int revocation_count = k->biased_lock_revocation_count();// 按默认参数来说:// 如果撤销计数大于等于 20,且小于 40,// 且距上次批量撤销的时间大于等于 25 秒,就会重置计数。if ((revocation_count >= BiasedLockingBulkRebiasThreshold) &&(revocation_count < BiasedLockingBulkRevokeThreshold) &&(last_bulk_revocation_time != 0) &&(cur_time - last_bulk_revocation_time >= BiasedLockingDecayTime)) {// This is the first revocation we've seen in a while of an// object of this type since the last time we performed a bulk// rebiasing operation. The application is allocating objects in// bulk which are biased toward a thread and then handing them// off to another thread. We can cope with this allocation// pattern via the bulk rebiasing mechanism so we reset the// klass's revocation count rather than allow it to increase// monotonically. If we see the need to perform another bulk// rebias operation later, we will, and if subsequently we see// many more revocation operations in a short period of time we// will completely disable biasing for this type.k->set_biased_lock_revocation_count(0);revocation_count = 0;}if (revocation_count <= BiasedLockingBulkRevokeThreshold) {// 自增计数revocation_count = k->atomic_incr_biased_lock_revocation_count();}// 此时,如果达到批量撤销阈值,则进行批量撤销。if (revocation_count == BiasedLockingBulkRevokeThreshold) {return HR_BULK_REVOKE;}// 此时,如果达到批量重偏向阈值,则进行批量重偏向。if (revocation_count == BiasedLockingBulkRebiasThreshold) {return HR_BULK_REBIAS;}// 否则,仅进行单个对象的撤销偏向return HR_SINGLE_REVOKE;
}
简单总结
对于一个类,按默认参数来说:
- 单个偏向撤销的计数达到 20,就会进行批量重偏向。
- 距上次批量重偏向 25 秒内,计数达到 40,就会发生批量撤销。
- 每隔 (>=) 25 秒,会重置在 [20, 40) 内的计数,这意味着可以发生多次批量重偏向。
**注意:**对于一个类来说,批量撤销只能发生一次,因为批量撤销后,该类禁用了可偏向属性,后面该类的对象都是不可偏向的,包括新创建的对象。
5.6 锁粗化和锁消除
5.6.1 锁粗化
for(int i=0;i<100000;i++){synchronized(this){do();
}//在锁粗化之后运行逻辑如下列代码
synchronized(this){for(int i=0;i<100000;i++){do();
}
5.6.2 锁消除
- Java:解释+编译
- JIT即时编译器优化
static int x = 0;public void b() {Object o = new Object();synchronized (o) {x++;}
}
真正执行时,会被JIT把加锁操作优化掉。
-XX:-EliminateLocks
禁用锁消除优化。
【黑马Java并发笔记】三、互斥与同步(上)相关推荐
- Java并发编程:线程的同步
<?xml version="1.0" encoding="utf-8"?> Java并发编程:线程的同步 Java并发编程:线程的同步 Table ...
- java学习笔记(三):前端miniUI控件库入门
java学习笔记(三):前端miniUI控件库入门 最近在一家公司实习学习,一上来就需要学习了解相关的前端内容--miniUI.而这个内容自己本身并没有了解学习过,上手也是遇到了不少的问题,于是想把自 ...
- 进阶笔记——java并发编程三特性与volatile
欢迎关注专栏:Java架构技术进阶.里面有大量batj面试题集锦,还有各种技术分享,如有好文章也欢迎投稿哦.微信公众号:慕容千语的架构笔记.欢迎关注一起进步. 前言 前面讲过使用synchronize ...
- Java并发体系-第二阶段-锁与同步-[1]-【万字文系列】
文章目录 并发编程中的三个问题 可见性 可见性概念 可见性演示 原子性 原子性概念 原子性演示 有序性 有序性概念 有序性演示 指令重排 为什么指令重排序可以提高性能? as-if-serial语义 ...
- C++多线程并发(三)---线程同步之条件变量
文章目录 一.何为条件变量 二.为何引入条件变量 三.如何使用条件变量 更多文章: 一.何为条件变量 在前一篇文章<C++多线程并发(二)-线程同步之互斥锁>中解释了线程同步的原理和实现, ...
- Java并发(三)并发工具
https://www.cnblogs.com/jinshuai86/p/9226164.html Java编程的逻辑 Java并发编程的艺术 极客时间:Java并发编程实战 并发工具1:同步协作工具 ...
- Java并发学习三:银行转账的死锁问题解决及示例
Java并发学习系列文章:Java并发学习-博客专栏 今天在学习极客时间专栏:<Java并发编程实战> 从03 | 互斥锁(上):解决原子性问题到06 | 用"等待-通知&quo ...
- java闭锁_【Java并发编程三】闭锁
1.什么是闭锁? 闭锁(latch)是一种Synchronizer(Synchronizer:是一个对象,它根据本身的状态调节线程的控制流.常见类型的Synchronizer包括信号量.关卡和闭锁). ...
- java并发编程(三十五)——公平与非公平锁实战
前言 在 java并发编程(十六)--锁的七大分类及特点 一文中我们对锁有各个维度的分类,其中有一个维度是公平/非公平,本文我们来探讨下公平与非公平锁. 公平|非公平 首先,我们来看下什么是公平锁和非 ...
最新文章
- 当NLPer爱上CV:后BERT时代生存指南之VL-BERT篇
- 巴克码相关器的verilog HDL设计
- 【Java基础】基本类型与运算
- SQL点滴20—T-SQL中的排名函数
- Channel使用技巧
- 两个重要极限_算法数学基础-概率论最重要两个结论:大数定律及中心极限定理...
- 完成css的切图 图片任意,css切图是什么意思
- TP5解析html 回显到页面上
- linux防火墙之牛刀小试
- View 绘制体系知识梳理(7) getMeasuredWidth 和 getWidth 的区别
- C++ 构造函数体内赋值与初始化列表的区别
- SRP6针对于网游登录服的应用
- (七)对Jmeter进行参数化的俩种方式
- 高级与低级编程语言的解释,哪一种更容易上手?
- 看不看?这就是程序员996的真实内幕!
- python 存根_pyi文件是干吗的?(一文读懂Python的存根文件和类型检查)
- 为什么计算机和一些电子产品的时间选择在1970.1.1
- 如何在2015年后的MacBook Air上安装双系统
- 谷歌Extensions安装进手机浏览器里
- 用免費的電腦資源協助數學的教學,學習與探索_復華中學教師營_中山大學應數系高中數學人才班_2021
热门文章
- 不越狱将ipa安装到iphone
- 「GoCN酷Go推荐」​QQ机器人 go-cqhttp
- 字符串转换成数字的三种方法 js
- IMU定位/位姿跟踪(IMU_localization or IMU_pose_tracking)
- Android记账系统可行性分析,毕业设计论文-基于安卓的大学生记账管理系统的设计与实现.doc...
- Facebook推广引流工具,Facebook潜客挖掘推广系统
- head first java 最新版_Head First Java.(第2版)
- WPS文字表格外计算功能配合书签使用公式轻松实现
- 动态规划之最长递增子序列 最长不重复子串 最长公共子序列
- win10部署docker后无法启用VMware虚拟机