synchronize基础篇
Java共享模型带来的线程安全问题
问题分析
两个线程对初始值为 0 的静态变量一个做自增,一个做自减,各做 5000 次,结果是 0 吗? public class SyncDemo {
private static int counter = 0;
public static void increment() {
counter++;
}
public static void decrement() {
counter--;
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
increment();
}
}, &#34;t1&#34;);
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
decrement();
}
}, &#34;t2&#34;);
t1.start();
t2.start();
t1.join();
t2.join();
//思考: counter=?
log.info(&#34;{}&#34;, counter);
}
- 以上的结果可能是正数、负数、零——因为 Java 中对静态变量的自增,自减并不是原子操作
- 可以查看 i++和 i--(i 为静态变量)的 JVM 字节码指令 ( 可以在idea中安装一个jclasslib插件)
getstatic i // 获取静态变量i的值
iconst_1 // 将int常量1压入操作数栈
iadd // 自增
getstatic i // 获取静态变量i的值
iconst_1 // 将int常量1压入操作数栈
isub // 自减
- 由上可看出
- 若是单线程以上代码是顺序执行(不会交错),结果没有问题
- 若是多线程下代码可能交错运行,结果则不确定
临界区( Critical Section)
- 一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区,其共享资源为临界资源
- 一个程序运行多个线程本身是没有问题的
- 问题出在多个线程访问共享资源
- 多个线程读共享资源其实也没有问题
- 在多个线程对共享资源读写操作时发生指令交错,就会出现问题
//临界资源
private static int counter = 0;
public static void increment() { //临界区
counter++;
}
public static void decrement() {//临界区
counter--;竞态条件( Race Condition )
- 多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件
- 避免临界区的竞态条件发生的手段:
- 阻塞式的解决方案:synchronized,Lock
- 非阻塞式的解决方案:原子变量
【注意】
虽然 java 中互斥和同步都可以采用 synchronized 关键字来完成,但它们还是有区别的:
- 互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码
- 同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点
synchronize的使用
synchronize加锁方式
- synchronized 同步块是 Java 提供的一种原子性内置锁
- Java 中的每个对象都可以把 synchronize 当作一个同步锁来使用,这些 Java 内置的,而使用者看不到的锁,称为内置锁,也叫监视器锁
加锁方式分类
- 方法
- 代码块
- 实例对象
- class对象
- 任意实例对象Object
分类 | 具体分类 | 被锁的对象 | 伪代码 | 方法 | 实例方法 | 类的实例对象 | public synchronize void method(){} | 静态方法 | 类对象 | public static synchronize void method(){} | 代码块 | 实例对象 | 类的实例对象 | synchronize(this){} | class对象 | 类对象 | synchronize(SynchronizeDemo.class){} | 任意实例对象object | 实例对象object | // String 对象作锁String lock = &#34;&#34;;synchronize(lock){} | 解决共享问题方法
public static synchronized void increment() {
counter++;
}
public static synchronized void decrement() {
counter--;
}
private static String lock = &#34;&#34;;
public static void increment() {
synchronized (lock){
counter++;
}
}
public static void decrement() {
synchronized (lock) {
counter--;
}
}
- synchronized 实际是用对象锁保证了临界区内代码的原子性
synchronize高级篇——底层原理
synchronize底层原理
JVM指令层面synchronize实现原理
- synchronized是JVM内置锁,基于Monitor机制实现
- 其依赖底层操作系统的互斥原语Mutex(互斥量)
- 它是一个重量级锁,性能较低
- JVM内置锁在1.5之后版本做了重大的优化,如锁粗化(Lock Coarsening)、锁消除(Lock Elimination)、轻量级锁(Lightweight Locking)、偏向锁(Biased Locking)、自适应自旋(Adaptive Spinning)等技术来减少锁操作的开销,内置锁的并发性能已经基本与Lock持平
- Java虚拟机通过一个同步结构支持方法和方法中的指令序列的同步:monitor。
- 同步方法,是通过方法中的access_flags中设置ACC_SYNCHRONIZED标志来实现;
- 同步代码块,是通过monitorenter和monitorexit来实现
- 两个指令的执行是JVM通过调用操作系统的互斥原语mutex来实现,被阻塞的线程会被挂起、等待重新调度,会导致“用户态和内核态”两个态之间来回切换,对性能有较大影响
管程(Monitnor)之MESA模型详解
管程
- 管程是指管理共享变量以及对共享变量操作的过程,让它们支持并发
- Java 1.5之前,Java语言提供的唯一并发语言就是管程
- synchronized关键字和wait()、notify()、notifyAll()这三个方法是Java中实现管程技术的组成部分
管程实现模型
- Hasen模型
- Hoare模型
- MESA模型——现在广泛使用的
MESA模型
- 管程中引入了条件变量的概念,而且每个条件变量都对应有一个等待队列
- 条件变量和等待队列的作用是解决线程之间的同步问题
wait()的正确使用姿势
while(条件不满足) {
wait();
}
- 唤醒的时间和获取到锁继续执行的时间是不一致的,被唤醒的线程再次执行时可能条件又不满足了,所以循环检验条件
- MESA模型的wait()方法还有一个超时参数,为了避免线程进入等待队列永久阻塞
notify()和notifyAll()分别何时使用
- 满足以下三个条件时,可以使用notify(),其余情况尽量使用notifyAll():
- 所有等待线程拥有相同的等待条件;
- 所有等待线程被唤醒后,执行相同的操作;
- 只需要唤醒一个线程
Java的内置管程synchronize
- Java 参考了 MESA 模型,语言内置的管程(synchronized)对 MESA 模型进行了精简
- MESA 模型中,条件变量可以有多个,Java 语言内置的管程里只有一个条件变量
Monitnor机制在Java中的实现
- java.lang.Object 类定义了 wait(),notify(),notifyAll() 方法,这些方法的具体实现,依赖于 ObjectMonitor 实现——这是 JVM 内部基于 C++ 实现的一套机制
- 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;
在获取锁时,是将当前线程插入到cxq的头部,而释放锁时,默认策略(QMode=0)是:如果EntryList为空,则将cxq中的元素按原有顺序插入到EntryList,并唤醒第一个线程,也就是当EntryList为空时,是后来的线程先获取锁。_EntryList不为空,直接从_EntryList中唤醒线程 对象内存布局&对象头详解
对象内存布局
Hotspot虚拟机中,对象在内存中存储的布局可以分为三块区域:
- 对象头(Header):比如 hash码,对象所属的年代,对象锁,锁状态标志,偏向锁(线程)ID,偏向时间,数组长度(数组对象才有)等
- 实例数据(Instance Data):存放类的属性数据信息,包括父类的属性信息
- 对齐填充(Padding):虚拟机要求对象起始地址必须是8字节的整数倍,所以为了字节对齐引入对齐填充,非必须
对象头
HotSpot虚拟机的对象头包括:
- Mark Word
- 用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等他,这部分数据官方称它为“Mark Word”
- 这部分数据的长度
- 在32位和64位的虚拟机中分别为32bit和64bit
- Klass Pointer
- klass类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例
- 32位4字节,64位开启指针压缩或最大堆内存<32g时4字节,否则8字节
- jdk1.8默认开启指针压缩后为4字节,当在JVM参数中关闭指针压缩(-XX:-UseCompressedOops)后,长度为8字节
- 数组长度(只有数组对象有)
- 如果对象是一个数组, 那在对象头中还必须有一块数据用于记录数组长度
- 长度
Mark word是如何记录锁状态的
- 简单点:MarkWord 结构搞得复杂,是因为需要节省内存,让同一个内存区域在不同阶段有不同的用处
Mark Word的结构
- hash: 保存对象的哈希码
- 运行期间调用System.identityHashCode()来计算,延迟计算,并把结果赋值到这里
- age: 保存对象的分代年龄
- 表示对象被GC的次数,当该次数到达阈值的时候,对象就会转移到老年代
- biased_lock: 偏向锁标识位
- 由于无锁和偏向锁的锁标识都是 01,没办法区分,这里引入一位的偏向锁标识位
- lock: 锁状态标识位
- 区分锁状态,比如11时表示对象待GC回收状态, 只有最后2位锁标识(11)有效
- JavaThread*: 保存持有偏向锁的线程ID(线程的地址)
- 偏向模式的时候,当某个线程持有对象的时候,对象这里就会被置为该线程的ID。 在后面的操作中,就无需再进行尝试获取锁的动作。
- 这个线程ID并不是JVM分配的线程ID号,和Java Thread中的ID是两个概念。
- epoch: 保存偏向时间戳。偏向锁在CAS锁操作过程中,偏向性标识,表示对象更偏向哪个锁
- ptr_to_lock_record:轻量级锁状态下,指向栈中锁记录的指针
- 当锁获取是无竞争时,JVM使用原子操作而不是OS互斥,这种技术称为轻量级锁定
- 在轻量级锁定的情况下,JVM通过CAS操作在对象的Mark Word中设置指向锁记录的指针
- ptr_to_heavyweight_monitor:重量级锁状态下,指向对象监视器Monitor的指针
- 如果两个不同的线程同时在同一个对象上竞争,则必须将轻量级锁定升级到Monitor以管理等待的线程
- 在重量级锁定的情况下,JVM在对象的ptr_to_heavyweight_monitor设置指向Monitor的指针
32位JVM下的对象结构描述
64位JVM下的对象结构描述
Mark Word中锁标记枚举
enum { locked_value = 0, //00 轻量级锁
unlocked_value = 1, //001 无锁
monitor_value = 2, //10 监视器锁,也叫膨胀锁,也叫重量级锁
marked_value = 3, //11 GC标记
biased_lock_pattern = 5 //101 偏向锁
利用JOL工具跟踪锁标记变化
偏向锁
- 偏向锁是一种针对加锁操作的优化手段
- 在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,因此为了消除数据在无竞争情况下锁重入(CAS操作)的开销而引入偏向锁
- 对于没有锁竞争的场合,偏向锁有很好的优化效果
/***StringBuffer内部同步***/
public synchronized int length() {
return count;
}
//System.out.println 无意识的使用锁
public void println(String x) {
synchronized (this) {
print(x); newLine();
}
}
- 当JVM启用了偏向锁模式(jdk6默认开启),新创建对象的Mark Word中的Thread Id为0,说明此时处于可偏向但未偏向任何线程,也叫做匿名偏向状态(anonymously biased)
偏向锁延迟偏向
- 偏向锁模式存在偏向锁延迟机制:HotSpot 虚拟机在启动后有个 4s 的延迟才会对每个新建的对象开启偏向锁模式
- JVM启动时会进行一系列的复杂活动(比如装载配置,系统类初始化等等),在这个过程中会使用大量synchronized关键字对对象加锁,且这些锁大多数都不是偏向锁。为了减少初始化时间,JVM默认延时加载偏向锁
//关闭延迟开启偏向锁
-XX:BiasedLockingStartupDelay=0
//禁止偏向锁
-XX:-UseBiasedLocking
//启用偏向锁
@Slf4j
public class LockEscalationDemo{
public static void main(String[] args) throws InterruptedException {
log.debug(ClassLayout.parseInstance(new Object()).toPrintable());
Thread.sleep(4000);
log.debug(ClassLayout.parseInstance(new Object()).toPrintable());
}
}偏向锁状态跟踪
public class LockEscalationDemo {
public static void main(String[] args) throws InterruptedException {
log.debug(ClassLayout.parseInstance(new Object()).toPrintable());
//HotSpot 虚拟机在启动后有个 4s 的延迟才会对每个新建的对象开启偏向锁模式
Thread.sleep(4000);
Object obj = new Object();
new Thread(new Runnable() {
@Override
public void run() {
log.debug(Thread.currentThread().getName()+&#34;开始执行。。。\n&#34;
+ClassLayout.parseInstance(obj).toPrintable());
synchronized (obj){
log.debug(Thread.currentThread().getName()+&#34;获取锁执行中。。。\n&#34;
+ClassLayout.parseInstance(obj).toPrintable());
}
log.debug(Thread.currentThread().getName()+&#34;释放锁。。。\n&#34;
+ClassLayout.parseInstance(obj).toPrintable());
}
},&#34;thread1&#34;).start();
Thread.sleep(5000);
log.debug(ClassLayout.parseInstance(obj).toPrintable());
}
}偏向锁撤销场景(升级或释放)
调用对象HashCode
调用锁对象的obj.hashCode()或System.identityHashCode(obj)方法会导致该对象的偏向锁被撤销
- 因为对于一个对象,其HashCode只会生成一次并保存,偏向锁是没有地方保存hashcode
- 轻量级锁会在锁记录中记录 hashCode
- 重量级锁会在 Monitor 中记录 hashCode
当对象处于可偏向(也就是线程ID为0)和已偏向的状态下,调用HashCode计算将会使对象再也无法偏向:
- 当对象可偏向时,MarkWord将变成未锁定状态,并只能升级成轻量锁;
- 当对象正处于偏向锁时,调用HashCode将使偏向锁强制升级成重量锁
调用wait/notify
- 偏向锁状态执行obj.notify()会升级为轻量级锁
- 调用obj.wait(timeout) 会升级为重量级锁
轻量级锁
- 倘若偏向锁失败,虚拟机并不会立即升级为重量级锁,它还会尝试使用一种称为轻量级锁的优化手段,此时Mark Word 的结构也变为轻量级锁的结构
- 轻量级锁所适应的场景
- 线程交替执行同步块的场景
- 如果存在同一时间多个线程访问同一把锁的场合,就会导致轻量级锁膨胀为重量级锁
轻量级锁跟踪
public class LockEscalationDemo {
public static void main(String[] args) throws InterruptedException {
log.debug(ClassLayout.parseInstance(new Object()).toPrintable());
//HotSpot 虚拟机在启动后有个 4s 的延迟才会对每个新建的对象开启偏向锁模式
Thread.sleep(4000);
Object obj = new Object();
// 思考: 如果对象调用了hashCode,还会开启偏向锁模式吗
obj.hashCode();
//log.debug(ClassLayout.parseInstance(obj).toPrintable());
new Thread(new Runnable() {
@Override
public void run() {
log.debug(Thread.currentThread().getName()+&#34;开始执行。。。\n&#34;
+ClassLayout.parseInstance(obj).toPrintable());
synchronized (obj){
log.debug(Thread.currentThread().getName()+&#34;获取锁执行中。。。\n&#34;
+ClassLayout.parseInstance(obj).toPrintable());
}
log.debug(Thread.currentThread().getName()+&#34;释放锁。。。\n&#34;
+ClassLayout.parseInstance(obj).toPrintable());
}
},&#34;thread1&#34;).start();
Thread.sleep(5000);
log.debug(ClassLayout.parseInstance(obj).toPrintable());
}
}锁升级场景:TEST
偏向锁升级为轻量级锁
轻量级锁升级(膨胀)为重量级锁
总结:锁对象状态转换
synchronize(一)梳理
深入理解synchronize
- 应用场景:解决线程安全问题
- 加锁,目的:序列化的访问临界资源
- Java中哪些实现锁机制
- 阻塞式
- synchronize
- reentrantLock
- 非阻塞式
synchronize的使用
- 使用方式
- 原理
- JVM指令层面
- 方法上:acc_synchronize
- 代码块:monitorenter、monitorexit
- 管程(Monitor)
- Java 锁体系的设计思想、设计理论基础
- 实现模型
- MESA(最常用)
- 入口等待队列(互斥)
- 多个条件对象(同步,阻塞唤醒机制)
- Java中实现monitor(简化版的MESA)
- 基于Object实现,方法:wait/notify/notifyAll,这些方法依赖ObjectMonitor实现
- 入口等待队列:_EntryList、_cxq(栈结构);不公平机制
- 条件队列:_waitSet
- 管程操作是重量级操作
- 涉及到用户态到内核态的切换
- 重量级锁
- 竞争激烈的场景
- 膨胀期间创建一个monitor对象
- monitor对象可复用
- CAS 自旋,若失败则阻塞
- 基本都到内核态处理
- 因优化重量级锁,引入
- 偏向锁
- 偏向某个线程,始终只有一个线程可进入到同步块
- 如偏向thread1,其后续进入同步块的逻辑,没有加锁解锁的开销
- 不存在竞争的场景,用偏向锁
- 偏向锁解锁,还是偏向锁状态(结合锁状态轮转图看)
- 偏向锁撤销锁且当前对象未锁定,才变成无锁状态;若当前对象锁定,则变为轻量级锁
- 轻量级锁
- 线程间存在轻微的竞争(即线程交替执行,临界区逻辑简单,可很快执行)
- 通过 CAS 获取锁,若失败,则膨胀为重量级锁——无自旋
- 会创建锁记录,通过CAS将markword内容写入锁记录,并将锁状态指向markword,双向指向
- 加锁/解锁
- 加锁解锁的标记
- 识别是哪种锁(锁状态)
- ——由synchronize加锁在对象上,是如何记录锁的?引出对象内存布局
- 对象内存布局
- 跟踪锁的状态
- 理解误区
- 关于偏向锁、轻量级锁、重量级锁存在的误区
- 1、无锁-->偏向锁-->轻量级锁-->重量级锁
- 不存在无锁-->偏向锁
- 且反过来锁没有降级,是转到无锁
- 2、轻量级锁自旋获取失败,会膨胀为重量级锁
- 轻量级锁不存在自旋
- 3、重量级锁不存在自旋
- 重量级锁存在自旋
锁升级的原理分析
轻量级锁源码分析
重量级锁源码分析——Synchronized重量级锁加锁解锁执行逻辑
synchronize进阶篇——synchronize锁的优化
针对偏向锁的优化
批量重偏向(bulk rebias)和批量撤销(bulk revoke)机制
- 从偏向锁的加锁解锁过程中可看出,当只有一个线程反复进入同步块时,偏向锁带来的性能开销基本可以忽略,但是当有其他线程尝试获得锁时,就需要等到safe point时,再将偏向锁撤销为无锁状态或升级为轻量级,会消耗一定的性能
- 所以在多线程竞争频繁的情况下,偏向锁不仅不能提高性能,还会导致性能下降
- 于是,就有了批量重偏向(bulk rebias)与批量撤销(bulk revoke)机制
原理
每个class对象会有一个对应的epoch字段,每个处于偏向锁状态对象的Mark Word中也有该字段,其初始值为创建该对象时class中的epoch的值。每次发生批量重偏向时,就将该值+1,同时遍历JVM中所有线程的栈,找到该class所有正处于加锁状态的偏向锁,将其epoch字段改为新值。下次获得锁时,发现当前对象的epoch值和class的epoch不相等,那就算当前已经偏向了其他线程,也不会执行撤销操作,而是直接通过CAS操作将其Mark Word的Thread Id 改成当前线程Id。
- 批量重偏向原理
- 以class为单位,为每个class维护一个偏向锁撤销计数器(epoch字段),每一次该class的对象发生偏向撤销操作时,该计数器+1,当这个值达到重偏向阈值(默认20)时,JVM就认为该class的偏向锁有问题,因此会进行批量重偏向
- 即——当撤销偏向锁阈值超过 20 次后,jvm 会这样觉得,我是不是偏向错了,于是会在给这些对象加锁时重新偏向至加锁线程,重偏向会重置对象 的 Thread ID
- 批量撤销原理
- 当达到重偏向阈值(默认20)后,假设该class计数器继续增长,当其达到批量撤销的阈值后(默认40),JVM就认为该class的使用场景存在多线程竞争,会标记该class为不可偏向,之后,对于该class的锁,直接走轻量级锁的逻辑
应用场景
- 批量重偏向机制场景
- 为了解决:一个线程创建了大量对象并执行了初始的同步操作,后来另一个线程也来将这些对象作为锁对象进行操作,这样会导致大量的偏向锁撤销操作
- 批量撤销场景
- 为了解决:在明显多线程竞争剧烈的场景下使用偏向锁是不合适的
JVM参数设置
- 设置JVM参数-XX:+PrintFlagsFinal,在项目启动时即可输出JVM的默认参数值
- 可以通过-XX:BiasedLockingBulkRebiasThreshold 和 -XX:BiasedLockingBulkRevokeThreshold 来手动设置阈值
intx BiasedLockingBulkRebiasThreshold = 20 //默认偏向锁批量重偏向阈值
intx BiasedLockingBulkRevokeThreshold = 40 //默认偏向锁批量撤销阈值测试:批量重偏向
- 当撤销偏向锁阈值超过 20 次后,jvm 会这样觉得:
- 是不是偏向错了,于是会在给这些对象加锁时重新偏向至加锁线程,重偏向会重置对象 的 Thread ID
@Slf4j
public class BiasedLockingTest {
//延时产生可偏向对象
Thread.sleep(5000);
// 创建一个list,来存放锁对象
List<Object> list = new ArrayList<>();
// 线程1
new Thread(() -> {
for (int i = 0; i < 50; i++) {
// 新建锁对象
Object lock = new Object();
synchronized (lock) {
list.add(lock);
}
}
try {
//为了防止JVM线程复用,在创建完对象后,保持线程thead1状态为存活
Thread.sleep(100000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, &#34;thead1&#34;).start();
//睡眠3s钟保证线程thead1创建对象完成
Thread.sleep(3000);
log.debug(&#34;打印thead1,list中第20个对象的对象头:&#34;);
log.debug((ClassLayout.parseInstance(list.get(19)).toPrintable()));
// 线程2
new Thread(() -> {
for (int i = 0; i < 40; i++) {
Object obj = list.get(i);
synchronized (obj) {
if(i>=15&&i<=21||i>=38){
log.debug(&#34;thread2-第&#34; + (i + 1) + &#34;次加锁执行中\t&#34;+
ClassLayout.parseInstance(obj).toPrintable());
}
}
if(i==17||i==19){
log.debug(&#34;thread2-第&#34; + (i + 1) + &#34;次释放锁\t&#34;+
ClassLayout.parseInstance(obj).toPrintable());
}
}
try {
Thread.sleep(100000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, &#34;thead2&#34;).start();
LockSupport.park();
}测试:批量撤销
- 当撤销偏向锁阈值超过 40 次后,jvm 会认为不该偏向
- 于是整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的
- 注意:时间-XX:BiasedLockingDecayTime=25000ms范围内没有达到40次,撤销次数清为0,重新计时
@Slf4j
public class BiasedLockingTest {
public static void main(String[] args) throws InterruptedException {
//延时产生可偏向对象
Thread.sleep(5000);
// 创建一个list,来存放锁对象
List<Object> list = new ArrayList<>();
// 线程1
new Thread(() -> {
for (int i = 0; i < 50; i++) {
// 新建锁对象
Object lock = new Object();
synchronized (lock) {
list.add(lock);
}
}
try {
//为了防止JVM线程复用,在创建完对象后,保持线程thead1状态为存活
Thread.sleep(100000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, &#34;thead1&#34;).start();
//睡眠3s钟保证线程thead1创建对象完成
Thread.sleep(3000);
log.debug(&#34;打印thead1,list中第20个对象的对象头:&#34;);
log.debug((ClassLayout.parseInstance(list.get(19)).toPrintable()));
// 线程2
new Thread(() -> {
for (int i = 0; i < 40; i++) {
Object obj = list.get(i);
synchronized (obj) {
if(i>=15&&i<=21||i>=38){
log.debug(&#34;thread2-第&#34; + (i + 1) + &#34;次加锁执行中\t&#34;+
ClassLayout.parseInstance(obj).toPrintable());
}
}
if(i==17||i==19){
log.debug(&#34;thread2-第&#34; + (i + 1) + &#34;次释放锁\t&#34;+
ClassLayout.parseInstance(obj).toPrintable());
}
}
try {
Thread.sleep(100000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, &#34;thead2&#34;).start();
Thread.sleep(3000);
new Thread(() -> {
for (int i = 0; i < 50; i++) {
Object lock =list.get(i);
if(i>=17&&i<=21||i>=35&&i<=41){
log.debug(&#34;thread3-第&#34; + (i + 1) + &#34;次准备加锁\t&#34;+
ClassLayout.parseInstance(lock).toPrintable());
}
synchronized (lock){
if(i>=17&&i<=21||i>=35&&i<=41){
log.debug(&#34;thread3-第&#34; + (i + 1) + &#34;次加锁执行中\t&#34;+
ClassLayout.parseInstance(lock).toPrintable());
}
}
}
},&#34;thread3&#34;).start();
Thread.sleep(3000);
log.debug(&#34;查看新创建的对象&#34;);
log.debug((ClassLayout.parseInstance(new Object()).toPrintable()));
LockSupport.park();
}小结
- 批量重偏向和批量撤销是针对类的优化,和对象无关
- 偏向锁重偏向一次之后不可再次重偏向
- 当某个类已经触发批量撤销机制后,JVM会默认当前类产生了严重的问题,剥夺了该类的新实例对象使用偏向锁的权利
针对重量级锁的优化
自旋优化——(1.6之后有了自适应自旋)
- 重量级锁竞争的时候,可使用自旋来进行优化——如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞
- 自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。
- 在 Java 6 之后自旋是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,比较智能
- Java 7 之后不能控制是否开启自旋功能
- 注意
- 自旋的目的是为了减少线程挂起的次数,尽量避免直接挂起线程(挂起操作涉及系统调用,存在用户态和内核态切换,这才是重量级锁最大的开销)
锁粗化
- 假设一系列的连续操作都会对同一个对象反复加锁及解锁,甚至加锁操作是出现在循环体中的,即使没有出现线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗
- 如果JVM检测到有一连串零碎的操作都是对同一对象的加锁,将会扩大加锁同步的范围(即锁粗化)到整个操作序列的外部
StringBuffer buffer = new StringBuffer();
/**
* 锁粗化
*/
public void append(){
buffer.append(&#34;aaa&#34;).append(&#34; bbb&#34;).append(&#34; ccc&#34;);
}锁消除
- 锁消除即删除不必要的加锁操作
- 锁消除是Java虚拟机在JIT编译期间,通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁
- 通过锁消除,可以节省毫无意义的请求锁时间
public class LockEliminationTest {
/**
* 锁消除
* -XX:+EliminateLocks 开启锁消除(jdk8默认开启)
* -XX:-EliminateLocks 关闭锁消除
* @param str1
* @param str2
*/
public void append(String str1, String str2) {
StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append(str1).append(str2);
}
public static void main(String[] args) throws InterruptedException {
LockEliminationTest demo = new LockEliminationTest();
long start = System.currentTimeMillis();
for (int i = 0; i < 100000000; i++) {
demo.append(&#34;aaa&#34;, &#34;bbb&#34;);
}
long end = System.currentTimeMillis();
System.out.println(&#34;执行时间:&#34; + (end - start) + &#34; ms&#34;);
}
}
- 示例中,StringBuffer的append是个同步方法,但是append方法中的 StringBuffer 属于一个局部变量,不可能从该方法中逃逸出去,因此其实这过程是线程安全的,可以将锁消除
逃逸分析
- 逃逸分析——JDK6 之后加入该技术
- 一种可有效减少Java 程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法
- 通过逃逸分析,Java Hotspot编译器能够分析出一个新的对象的引用的使用范围从而决定是否要将这个对象分配到堆上
- 逃逸分析的基本行为就是分析对象动态作用域
方法逃逸(对象逃出当前方法)
- 当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他地方中
线程逃逸((对象逃出当前线程)
- 这个对象甚至可能被其它线程访问到,例如赋值给类变量或可以在其它线程中访问的实例变量
逃逸分析使用/作用
使用逃逸分析,编译器可以对代码做如下优化
- 同步省略或锁消除(Synchronization Elimination)。如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步。
- 将堆分配转化为栈分配(Stack Allocation)。如果一个对象在子程序中被分配,要使指向该对象的指针永远不会逃逸,对象可能是栈分配的候选,而不是堆分配。
- 分离对象或标量替换(Scalar Replacement)。有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在CPU寄存器中。
JVM参数指定是否开启逃逸分析
-XX:+DoEscapeAnalysis //表示开启逃逸分析 (jdk1.8默认开启)
-XX:-DoEscapeAnalysis //表示关闭逃逸分析。
-XX:+EliminateAllocations //开启标量替换(默认打开) |