synchronized基础篇
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();
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
decrement();
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
//思考: counter=?
log.info("{}", counter);
}
}
问题分析
以上的结果可能是正数、负数、零。为什么呢?因为 Java 中对静态变量的自增,自减并不是原子操作。
我们可以查看 i++和 i–(i 为静态变量)的 JVM 字节码指令 ( 可以在idea中安装一个jclasslib插件)
i++的JVM 字节码指令
getstatic i // 获取静态变量i的值
iconst_1 // 将int常量1压入操作数栈
iadd // 自增
putstatic i // 将修改后的值存入静态变量i
i–的JVM 字节码指令
getstatic i // 获取静态变量i的值
iconst_1 // 将int常量1压入操作数栈
isub // 自减
putstatic i // 将修改后的值存入静态变量i
如果是单线程以上 8 行代码是顺序执行(不会交错)没有问题。
但多线程下这 8 行代码可能交错运行:
临界区( Critical Section)
- 一个程序运行多个线程本身是没有问题的
- 问题出在多个线程访问共享资源
多个线程读共享资源其实也没有问题
在多个线程对共享资源读写操作时发生指令交错,就会出现问题
一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区,其共享资源为临界资源
//临界资源
private static int counter = 0;
public static void increment() { //临界区
counter++;
}
public static void decrement() {//临界区
counter--;
}
竞态条件( Race Condition )
多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件
为了避免临界区的竞态条件发生,有多种手段可以达到目的:
- 阻塞式的解决方案:synchronized,Lock
- 非阻塞式的解决方案:原子变量
注意:
虽然 java 中互斥和同步都可以采用 synchronized 关键字来完成,但它们还是有区别的:
互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码
同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点
synchronized的使用
synchronized 同步块是 Java 提供的一种原子性内置锁,Java 中的每个对象都可以把它当作一个同步锁来使用,这些 Java 内置的使用者看不到的锁被称为内置锁,也叫作监视器锁。
加锁方式
解决之前的共享问题
方式一:
public static synchronized void increment() {//锁的是类对象
counter++;
}
public static synchronized void decrement() {
counter--;
}
方式二:
private static String lock = "";
public static void increment() {//锁的配置的实例对象
synchronized (lock){
counter++;
}
}
public static void decrement() {
synchronized (lock) {
counter--;
}
}
synchronized 实际是用对象锁保证了临界区内代码的原子性
synchronized高级篇
synchronized底层原理
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来实现,被阻塞的线程会被挂起、等待重新调度,会导致“用户态和内核态”两个态之间来回切换,对性能有较大影响。
查看synchronized的字节码指令序列
Method access and property flags:
加起来就是29
这种就是在同步代码块中加锁的方式
Monitor(管程/监视器)
Monitor,直译为“监视器”,而操作系统领域一般翻译为“管程”。管程是指管理共享变量以及对共享变量操作的过程,让它们支持并发。在Java 1.5之前,Java语言提供的唯一并发语言就是管程,Java 1.5之后提供的SDK并发包也是以管程为基础的。除了Java之外,C/C++、C#等高级语言也都是支持管程的。synchronized关键字和wait()、notify()、notifyAll()这三个方法是Java中实现管程技术的组成部分。
MESA模型
在管程的发展史上,先后出现过三种不同的管程模型,分别是Hasen模型、Hoare模型和MESA模型。现在正在广泛使用的是MESA模型。下面我们便介绍MESA模型:
管程中引入了条件变量的概念,而且每个条件变量都对应有一个等待队列。条件变量和等待队列的作用是解决线程之间的同步问题。wait()的正确使用姿势
对于MESA管程来说,有一个编程范式:
while(条件不满足) {
wait();
}
唤醒的时间和获取到锁继续执行的时间是不一致的,被唤醒的线程再次执行时可能条件又不满足了,所以循环检验条件。MESA模型的wait()方法还有一个超时参数,为了避免线程进入等待队列永久阻塞。
notify()和notifyAll()分别何时使用
满足以下三个条件时,可以使用notify(),其余情况尽量使用notifyAll():
1.所有等待线程拥有相同的等待条件;
2.所有等待线程被唤醒后,执行相同的操作;
3.只需要唤醒一个线程。
Java语言的内置管程synchronized
Java 参考了 MESA 模型,语言内置的管程(synchronized)对 MESA 模型进行了精简。MESA 模型中,条件变量可以有多个,Java 语言内置的管程里只有一个条件变量。模型如下图所示。
Monitor机制在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中唤醒线程。
1.如果A线程先获取锁。然后调用wait方法,就会进入entryList里面,B线程执行完成以后,线程C进来以后,不会执行,因为EntryList不为空,则先让线程A拿到锁,执行结束。
2.如果A先进来,拿到锁,B和C线程同时竞争,A线程释放锁以后,会发现EntryList为空,则C线程后进来,先拿到锁
思考:synchronized加锁加在对象上,锁对象是如何记录锁状态的?是在对象头上面记录的
携程面试题: new Object()在内存中占几个字节?16个字节 对象头占用八个字节 实例数据占用四个字节 对齐填充占用四个字节 总共12 要对齐填充8的整数倍,是16个字节
对象的内存布局
Hotspot虚拟机中,对象在内存中存储的布局可以分为三块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。
- 对象头:比如 hash码,对象所属的年代,对象锁,锁状态标志,偏向锁(线程)ID,偏向时间,数组长度(数组对象才有)等。
- 实例数据:存放类的属性数据信息,包括父类的属性信息;
- 对齐填充:由于虚拟机要求 对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐。
对象头详解
HotSpot虚拟机的对象头包括:
-
Mark Word
用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄(占4位)、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等(和批量偏向锁有关),这部分数据的长度在32位和64位的虚拟机中分别为32bit和64bit,官方称它为“Mark Word”。 -
Klass Pointer 类元数据
对象头的另外一部分是klass类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。 32位4字节,64位开启指针压缩或最大堆内存<32g时4字节,否则8字节。jdk1.8默认开启指针压缩后为4字节,当在JVM参数中关闭指针压缩(-XX:-UseCompressedOops)后,长度为8字节。 -
数组长度(只有数组对象有)
如果对象是一个数组, 那在对象头中还必须有一块数据用于记录数组长度。 4字节
使用JOL工具查看内存布局
给大家推荐一个可以查看普通java对象的内部布局工具JOL(JAVA OBJECT LAYOUT),使用此工具可以查看new出来的一个java对象的内部布局,以及一个普通的java对象占用多少字节。
引入maven依赖
<!-- 查看Java 对象布局、大小工具 -->
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.10</version>
</dependency>
使用方法
//查看对象内部信息
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
测试:
public static void main(String[] args) throws InterruptedException {
Object obj = new Object();
//查看对象内部信息
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
- 利用jol查看64位系统java对象(空对象),默认开启指针压缩,总大小显示16字节,前12字节为对象头
OFFSET:偏移地址,单位字节;
SIZE:占用的内存大小,单位为字节;
TYPE DESCRIPTION:类型描述,其中object header为对象头;
VALUE:对应内存中当前存储的值,二进制32位;
- 关闭指针压缩后,对象头为16字节:-XX:-UseCompressedOops
思考: 下面例子中obj对象占多少个字节?
不用考虑,24个字节。8+4+8=20 对其填充以后就是24
public class ObjectTest {
public static void main(String[] args) throws InterruptedException {
Object obj = new Test();
//查看对象内部信息
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
}
class Test{
private long p;
}
回到之前的问题: synchronized加锁加在对象上,对象是如何记录锁状态的?
锁状态被记录在每个对象的对象头的Mark Word中
Mark Word是如何记录锁状态的
Hotspot通过markOop类型实现Mark Word,具体实现位于markOop.hpp文件中。由于对象需要存储的运行时数据很多,考虑到虚拟机的内存使用,markOop被设计成一个非固定的数据结构,以便在极小的空间存储尽量多的数据,根据对象的状态复用自己的存储空间。
简单点理解就是: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锁操作过程中,偏向性标识,表示对象更偏向哪个锁。
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 偏向锁
};
更直观的理解方式:
总结:锁对象状态转换
进阶:synchronized锁优化
偏向锁批量重偏向&批量撤销
从偏向锁的加锁解锁过程中可看出,当只有一个线程反复进入同步块时,偏向锁带来的性能开销基本可以忽略,但是当有其他线程尝试获得锁时,就需要等到safe point时,再将偏向锁撤销为无锁状态或升级为轻量级,会消耗一定的性能,所以在多线程竞争频繁的情况下,偏向锁不仅不能提高性能,还会导致性能下降。于是,就有了批量重偏向与批量撤销的机制。
原理
以class为单位,为每个class维护一个偏向锁撤销计数器,每一次该class的对象发生偏向撤销操作时,该计数器+1,当这个值达到重偏向阈值(默认20)时,JVM就认为该class的偏向锁有问题,因此会进行批量重偏向。
每个class对象会有一个对应的epoch字段,每个处于偏向锁状态对象的Mark Word中也有该字段,其初始值为创建该对象时class中的epoch的值。每次发生批量重偏向时,就将该值+1,同时遍历JVM中所有线程的栈,找到该class所有正处于加锁状态的偏向锁,将其epoch字段改为新值。下次获得锁时,发现当前对象的epoch值和class的epoch不相等,那就算当前已经偏向了其他线程,也不会执行撤销操作,而是直接通过CAS操作将其Mark Word的Thread Id 改成当前线程Id。
当达到重偏向阈值(默认20)后,假设该class计数器继续增长,当其达到批量撤销的阈值后(默认40),JVM就认为该class的使用场景存在多线程竞争,会标记该class为不可偏向,之后,对于该class的锁,直接走轻量级锁的逻辑。
应用场景
- **批量重偏向(bulk rebias)机制是为了解决:一个线程创建了大量对象并执行了初始的同步操作,后来另一个线程也来将这些对象作为锁对象进行操作,这样会导致大量的偏向锁撤销操作。
- 批量撤销(bulk revoke)机制是为了解决:在明显多线程竞争剧烈的场景下使用偏向锁是不合适的。**
总结
- 批量重偏向和批量撤销是针对类的优化,和对象无关。
- 偏向锁重偏向一次之后不可再次重偏向。
- 当某个类已经触发批量撤销机制后,JVM会默认当前类产生了严重的问题,剥夺了该类的新实例对象使用偏向锁的权利
自旋优化
重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。
- 自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。
- 在 Java 6 之后自旋是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,比较智能。
- Java 7 之后不能控制是否开启自旋功能
注意:自旋的目的是为了减少线程挂起的次数,尽量避免直接挂起线程(挂起操作涉及系统调用,存在用户态和内核态切换,这才是重量级锁最大的开销)
锁粗化
假设一系列的连续操作都会对同一个对象反复加锁及解锁,甚至加锁操作是出现在循环体中的,即使没有出现线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。如果JVM检测到有一连串零碎的操作都是对同一对象的加锁,将会扩大加锁同步的范围(即锁粗化)到整个操作序列的外部。
StringBuffer buffer = new StringBuffer();
/**
* 锁粗化
*/
public void append(){
buffer.append("aaa").append(" bbb").append(" ccc");
}
上述代码每次调用 buffer.append 方法都需要加锁和解锁,如果JVM检测到有一连串的对同一个对象加锁和解锁的操作,就会将其合并成一次范围更大的加锁和解锁操作,即在第一次append方法时进行加锁,最后一次append方法结束后进行解锁。
锁消除
锁消除即删除不必要的加锁操作。锁消除是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("aaa", "bbb");
}
long end = System.currentTimeMillis();
System.out.println("执行时间:" + (end - start) + " ms");
}
}
StringBuffer的append是个同步方法,但是append方法中的 StringBuffer 属于一个局部变量,不可能从该方法中逃逸出去,因此其实这过程是线程安全的,可以将锁消除。
测试结果: 关闭锁消除执行时间4688 ms 开启锁消除执行时间:2601 ms
逃逸分析(Escape Analysis)
逃逸分析,是一种可以有效减少Java 程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。通过逃逸分析,Java Hotspot编译器能够分析出一个新的对象的引用的使用范围从而决定是否要将这个对象分配到堆上。逃逸分析的基本行为就是分析对象动态作用域。
方法逃逸(对象逃出当前方法)
当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他地方中。
线程逃逸((对象逃出当前线程)
这个对象甚至可能被其它线程访问到,例如赋值给类变量或可以在其它线程中访问的实例变量。
使用逃逸分析,编译器可以对代码做如下优化:
1.同步省略或锁消除(Synchronization Elimination)。如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步。
2.将堆分配转化为栈分配(Stack Allocation)。如果一个对象在子程序中被分配,要使指向该对象的指针永远不会逃逸,对象可能是栈分配的候选,而不是堆分配。
3.分离对象或标量替换(Scalar Replacement)。有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在CPU寄存器中。
jdk6才开始引入该技术,jdk7开始默认开启逃逸分析。在Java代码运行时,可以通过JVM参数指定是否开启逃逸分析:
-XX:+DoEscapeAnalysis //表示开启逃逸分析 (jdk1.8默认开启)
-XX:-DoEscapeAnalysis //表示关闭逃逸分析。
-XX:+EliminateAllocations //开启标量替换(默认打开)
-XX:+EliminateLocks //开启锁消除(jdk1.8默认开启)
/**
* @author Fox
*
* 进行两种测试
* 关闭逃逸分析,同时调大堆空间,避免堆内GC的发生,如果有GC信息将会被打印出来
* VM运行参数:-Xmx4G -Xms4G -XX:-DoEscapeAnalysis -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError
*
* 开启逃逸分析 jdk8默认开启
* VM运行参数:-Xmx4G -Xms4G -XX:+DoEscapeAnalysis -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError
*
* 执行main方法后
* jps 查看进程
* jmap -histo 进程ID
*
*/
@Slf4j
public class EscapeTest {
public static void main(String[] args) {
long start = System.currentTimeMillis();
for (int i = 0; i < 500000; i++) {
alloc();
}
long end = System.currentTimeMillis();
log.info("执行时间:" + (end - start) + " ms");
try {
Thread.sleep(Integer.MAX_VALUE);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
}
/**
* JIT编译时会对代码进行逃逸分析
* 并不是所有对象存放在堆区,有的一部分存在线程栈空间
* Ponit没有逃逸
*/
private static String alloc() {
Point point = new Point();
return point.toString();
}
/**
*同步省略(锁消除) JIT编译阶段优化,JIT经过逃逸分析之后发现无线程安全问题,就会做锁消除
*/
public void append(String str1, String str2) {
StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append(str1).append(str2);
}
/**
* 标量替换
*
*/
private static void test2() {
Point point = new Point(1,2);
System.out.println("point.x="+point.getX()+"; point.y="+point.getY());
// int x=1;
// int y=2;
// System.out.println("point.x="+x+"; point.y="+y);
}
}
@Data
@AllArgsConstructor
@NoArgsConstructor
class Point{
private int x;
private int y;
}
测试结果:开启逃逸分析,部分对象会在栈上分配
总结和自己的理解
# 深入理解synchronized
应用场景: 解决线程安全问题
加锁: 序列化的访问临界资源
java中的实现 : 阻塞: synchronized reentrantLock
非阻塞: cas+自旋
# [synchronized的使用](#3460-1631601809082)
方式: 方法上 代码块中 (锁对象)
## 原理
jvm指令: 方法上(acc_synchronized ) 代码块( monitorenter monitorexit)
管程(Monitor): java锁体系的设计思想,设计理论基础
MESA: 入口等待队列 (互斥) 多个条件队列 (同步 阻塞唤醒机制)
java实现monitor: Object wait/notify/notifyAll 重量级操作 : (用户态到内核态的切换)
优化: 重量级锁 偏向锁 轻量级锁
偏向锁: 不存在竞争 偏向某个线程 thread1,后续进入同步块的逻辑没有加锁解锁的开销
轻量级锁: 线程间存在轻微的竞争(线程交替执行,临界区逻辑简单) CAS获取锁,失败膨胀
重量级锁: 多线程竞争激烈的场景 膨胀期间创建一个monitor对象 CAS自旋 阻塞
加锁/解锁 加锁解锁的标记 识别是哪种锁(锁状态)
synchronized加锁加在对象上,锁对象是如何记录锁状态的?
**对象的内存布局** MarkWord
![img](G:\我的文档\云笔记\chaosbead@163.com(1)\1a5af8281c1a40e9ab664a285a4307a5\clipboard.png)
跟踪锁状态如何变化的?
误区:
关于偏向锁轻量级锁重量级锁存在的理解误区:
1. 无锁——>偏向锁——>轻量级锁——>重量级2锁 (不存在无锁——>偏向锁)
2.轻量级锁自旋获取锁失败,会膨胀升级为重量级锁 (轻量级锁不存在自旋)
3. 重量级锁不存在自旋 (重量级锁存在自旋 )
jvm对synchronized的优化
针对偏向锁(偏向锁撤销存在性能问题) 批量重偏向 批量撤销
针对重量级锁 : 自旋优化 自适应自旋
锁粗化, 锁消除
```handlebars
# 深入理解synchronized
应用场景: 解决线程安全问题
加锁: 序列化的访问临界资源
java中的实现 : 阻塞: synchronized reentrantLock
非阻塞: cas+自旋
# [synchronized的使用](#3460-1631601809082)
方式: 方法上 代码块中 (锁对象)
## 原理
jvm指令: 方法上(acc_synchronized ) 代码块( monitorenter monitorexit)
管程(Monitor): java锁体系的设计思想,设计理论基础
MESA: 入口等待队列 (互斥) 多个条件队列 (同步 阻塞唤醒机制)
java实现monitor: Object wait/notify/notifyAll 重量级操作 : (用户态到内核态的切换)
优化: 重量级锁 偏向锁 轻量级锁
偏向锁: 不存在竞争 偏向某个线程 thread1,后续进入同步块的逻辑没有加锁解锁的开销
轻量级锁: 线程间存在轻微的竞争(线程交替执行,临界区逻辑简单) CAS获取锁,失败膨胀
重量级锁: 多线程竞争激烈的场景 膨胀期间创建一个monitor对象 CAS自旋 阻塞
加锁/解锁 加锁解锁的标记 识别是哪种锁(锁状态)
synchronized加锁加在对象上,锁对象是如何记录锁状态的?
**对象的内存布局** MarkWord
![img](G:\我的文档\云笔记\chaosbead@163.com(1)\1a5af8281c1a40e9ab664a285a4307a5\clipboard.png)
跟踪锁状态如何变化的?
误区:
关于偏向锁轻量级锁重量级锁存在的理解误区:
1. 无锁——>偏向锁——>轻量级锁——>重量级2锁 (不存在无锁——>偏向锁)
2.轻量级锁自旋获取锁失败,会膨胀升级为重量级锁 (轻量级锁不存在自旋)
3. 重量级锁不存在自旋 (重量级锁存在自旋 )
jvm对synchronized的优化
针对偏向锁(偏向锁撤销存在性能问题) 批量重偏向 批量撤销
针对重量级锁 : 自旋优化 自适应自旋
锁粗化, 锁消除