文章目录

  • 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 常见线程安全类

  1. String
  2. 基本类型包装类
  3. StringBuffer
  4. Random
  5. Vector
  6. Hashtable
  7. java.util.concurrent包下的类
  8. 没有成员变量的类一般都是线程安全的

这里说它们是线程安全的是指,多个线程调用它们同一个实例的某个方法时,是线程安全的。

每个方法都是原子的。多个方法组合在一起不是原子的。

下面是一个多个方法组合不是原子的例子:

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) {}
}
  1. 创建锁记录(Lock Record)对象,每个线程的栈都会包含一个锁记录的结构,内部可以存储锁定对象的Mark Word

    JVM 的Lock Record简介

  2. 让锁记录中Object reference指向锁对象,并尝试用cas替换Object的Mark Word,将Mark Word的值存入锁记录

    对象头结尾01表示无锁,可以进行加锁。

  3. 如果cas替换成功了,对象头中存储了锁记录地址和状态00,表示由该线程给对象加锁,这时图示如下:
  4. 如果cas失败,有两种情况:
  • 如果是其他线程已经持有了该Object的轻量级锁,这时表明有竞争,进入锁膨胀过程
  • 如果是自己执行了synchronized锁重入,那么再添加一条Lock Record作为重入的计数
  1. 解锁

    • 当退出synchronized代码块(解锁)时如果有取值为null的锁记录,表示有重入,这时重置锁记录,表示重入计数减一
    • 当退出synchronized代码块(解锁)时锁记录的值不为null,这时使用cas将Mark Word的值恢复给对象头
      • 成功,则解锁成功
      • 失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程

5.2 锁膨胀

如果再尝试加轻量级锁的过程中,CAS操作无法成功,这时一种情况就是其他线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁

  1. 当Thread-1进行轻量级加锁时,Thread-0已经对该对象加了轻量级锁:
    这时Thread-1加轻量级锁失败,进入锁膨胀流程:

    • 为Object对象申请Monitor锁,让Object指向重量级锁地址
    • 然后自己进入Monitor的EntryList BLOCKED

    轻量级锁没有阻塞的概念

  2. 当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 批量撤销

视频中的例子:

  1. t1运行: dog0 ~ dog38偏向于t1
  2. t2运行:dog0 ~ dog18升级为轻量级锁并释放为0x1普通对象
    ::此时共撤销了19个偏向锁::
  3. t2继续运行:dog19 ~ dog38批量重偏向于t2
    ::dog19进行计数+1,达到阈值20后再和剩余对象重偏向::
    ::此时共撤销了19 + 1 = 20个偏向锁::
  4. t3运行:dog0 ~ dog18轻量级锁(0x1处于不可偏向状态)
  5. t3继续运行:dog19 ~ dog38从偏向于t2升级为轻量级锁
    ::此时共撤销了20 + 20 = 40个偏向锁::
  6. 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并发笔记】三、互斥与同步(上)相关推荐

  1. Java并发编程:线程的同步

    <?xml version="1.0" encoding="utf-8"?> Java并发编程:线程的同步 Java并发编程:线程的同步 Table ...

  2. java学习笔记(三):前端miniUI控件库入门

    java学习笔记(三):前端miniUI控件库入门 最近在一家公司实习学习,一上来就需要学习了解相关的前端内容--miniUI.而这个内容自己本身并没有了解学习过,上手也是遇到了不少的问题,于是想把自 ...

  3. 进阶笔记——java并发编程三特性与volatile

    欢迎关注专栏:Java架构技术进阶.里面有大量batj面试题集锦,还有各种技术分享,如有好文章也欢迎投稿哦.微信公众号:慕容千语的架构笔记.欢迎关注一起进步. 前言 前面讲过使用synchronize ...

  4. Java并发体系-第二阶段-锁与同步-[1]-【万字文系列】

    文章目录 并发编程中的三个问题 可见性 可见性概念 可见性演示 原子性 原子性概念 原子性演示 有序性 有序性概念 有序性演示 指令重排 为什么指令重排序可以提高性能? as-if-serial语义 ...

  5. C++多线程并发(三)---线程同步之条件变量

    文章目录 一.何为条件变量 二.为何引入条件变量 三.如何使用条件变量 更多文章: 一.何为条件变量 在前一篇文章<C++多线程并发(二)-线程同步之互斥锁>中解释了线程同步的原理和实现, ...

  6. Java并发(三)并发工具

    https://www.cnblogs.com/jinshuai86/p/9226164.html Java编程的逻辑 Java并发编程的艺术 极客时间:Java并发编程实战 并发工具1:同步协作工具 ...

  7. Java并发学习三:银行转账的死锁问题解决及示例

    Java并发学习系列文章:Java并发学习-博客专栏 今天在学习极客时间专栏:<Java并发编程实战> 从03 | 互斥锁(上):解决原子性问题到06 | 用"等待-通知&quo ...

  8. java闭锁_【Java并发编程三】闭锁

    1.什么是闭锁? 闭锁(latch)是一种Synchronizer(Synchronizer:是一个对象,它根据本身的状态调节线程的控制流.常见类型的Synchronizer包括信号量.关卡和闭锁). ...

  9. java并发编程(三十五)——公平与非公平锁实战

    前言 在 java并发编程(十六)--锁的七大分类及特点 一文中我们对锁有各个维度的分类,其中有一个维度是公平/非公平,本文我们来探讨下公平与非公平锁. 公平|非公平 首先,我们来看下什么是公平锁和非 ...

最新文章

  1. 当NLPer爱上CV:后BERT时代生存指南之VL-BERT篇
  2. 巴克码相关器的verilog HDL设计
  3. 【Java基础】基本类型与运算
  4. SQL点滴20—T-SQL中的排名函数
  5. Channel使用技巧
  6. 两个重要极限_算法数学基础-概率论最重要两个结论:大数定律及中心极限定理...
  7. 完成css的切图 图片任意,css切图是什么意思
  8. TP5解析html 回显到页面上
  9. linux防火墙之牛刀小试
  10. View 绘制体系知识梳理(7) getMeasuredWidth 和 getWidth 的区别
  11. C++ 构造函数体内赋值与初始化列表的区别
  12. SRP6针对于网游登录服的应用
  13. (七)对Jmeter进行参数化的俩种方式
  14. 高级与低级编程语言的解释,哪一种更容易上手?
  15. 看不看?这就是程序员996的真实内幕!
  16. python 存根_pyi文件是干吗的?(一文读懂Python的存根文件和类型检查)
  17. 为什么计算机和一些电子产品的时间选择在1970.1.1
  18. 如何在2015年后的MacBook Air上安装双系统
  19. 谷歌Extensions安装进手机浏览器里
  20. 用免費的電腦資源協助數學的教學,學習與探索_復華中學教師營_中山大學應數系高中數學人才班_2021

热门文章

  1. 不越狱将ipa安装到iphone
  2. 「GoCN酷Go推荐」​QQ机器人 go-cqhttp
  3. 字符串转换成数字的三种方法 js
  4. IMU定位/位姿跟踪(IMU_localization or IMU_pose_tracking)
  5. Android记账系统可行性分析,毕业设计论文-基于安卓的大学生记账管理系统的设计与实现.doc...
  6. Facebook推广引流工具,Facebook潜客挖掘推广系统
  7. head first java 最新版_Head First Java.(第2版)
  8. WPS文字表格外计算功能配合书签使用公式轻松实现
  9. 动态规划之最长递增子序列 最长不重复子串 最长公共子序列
  10. win10部署docker后无法启用VMware虚拟机