基本概念
- 线程安全问题三个要素:多线程、共享资源、非原子性操作;
- 产生的根本原因:多条线程同时对一个共享资源进行非原子性操作;
Synchronized解决线程安全问题的方式:通过互斥锁将多线程的并行执行变为单线程串行执行,同一时刻只让一条线程执行,也就是当多条线程同时执行一段被互斥锁保护的代码(临界资源)时,需要先获取锁,这时只会有一个线程获取到锁资源成功执行,其他线程将陷入等待的状态,直到当前线程执行完毕释放锁资源之后,其他线程才能执行;Synchronized可以保证可见性和有序性,但无法禁止指令重排序;
Synchronized锁粒度及应用方式
Synchronized锁的三种粒度
- 锁粒度:
synchronized本质上是通过对象来加锁的,根据不同的对象类型可分为不同的锁粒度;this锁:当前实例锁;object锁:对象实例锁;class锁:对象锁;
应用方式
-
修饰实例成员方法:使用的是
this锁类型,这个this代表的是当前new出来的对象;synchronized void method() {//业务代码 } -
修饰静态成员方法:使用的是
this锁类型,但由于静态成员属于类对象,所以这个this代表的是class对象;synchronized static void method() {//业务代码 } -
修饰代码块:修饰代码块时,可以指定锁对象,可以将任意
class类对象做为锁资源;synchronized(SyncIncrDemo.class) {//业务代码 }-
使用
String对象加锁:因为修饰代码块时,可以将任意class类对象做为锁资源,而JAVA中字符串是以Sting对象存储使用的,并且在JVM里有个字符串常量池,用于存储字符串,那么相同值的字符串变量的引用地址可以是同一个,基于这个特性我们可以将synchronized锁粒度变细;如:将每一个订单的ID转化为字符串然后进行加锁,这样就能降低锁粒度提高系统并发,但在加锁时需要考虑加锁的
String对象是不是同一个,需要考虑String对象使用的是堆中对象还是字符串常量池中的对象,若是堆中对象需要使用.intern()将堆中对象刷入字符串常量池中;/*** @author xrl* @date 2024/6/25 22:47*/ public class Demo1 {public static void main(String[] args) {new Thread(() ->t1(111L), "AAA").start();new Thread(() ->t2(111L), "BBB").start();new Thread(() -> t2(111L), "CCC").start();}public static void t1(Long orderId){// 不能使用 String lock = orderId + "; 原因是orderId是入参,而方法可能在多个地方调用// 所以编译器不会使用常量折叠技术对其进行优化,编译后的代码会被转化为new StringBuilder().append(orderId).append("").toString()String lock = new String(orderId + "").intern();synchronized (lock){System.out.println(Thread.currentThread().getName() + "拿到锁");try {System.out.println("t1业务执行");Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + "释放锁");}}public static void t2(Long orderId){String lock = new String(orderId + "").intern();synchronized (lock){System.out.println(Thread.currentThread().getName() + "拿到锁");try {System.out.println("t2业务执行");Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + "释放锁");}} }
-
实现原理
Synchronized是基于Monitor(管程)对象实现的;- 获取锁:进入管程对象(显式型:
monitorenter指令); - 释放锁:退出管程对象(显示型:
monitorexit指令);
- 获取锁:进入管程对象(显式型:
synchronized修饰方法使用时是隐式同步的,是通过调用指令,读取运行时常量池中的方法ACC_SYNCHRONIZED标识实现的,也就无法通过javap反编译看到进入/退出管程对象的指令;
JAVA对象的内存布局
-
分为三个区域:对象头、实例数据、对齐填充;

-
对象头:存储
MarkWord和类型指针(ClassMetadataAddress/KlassWord);如果为数组对象,还会存数组长度(ArrayLength)-
主要包含:
unused未使用的空间、HashCode、age分代年龄、biased_lock是否偏向锁、lock锁标记位、ThreadID持有偏向锁的线程ID、epoch偏向锁时间戳、ptr_to_lock_record指向线程本地栈中lock_record的指针、ptr_to_heavyweight_monitor指向堆中monitor对象的指针; -
64位系统中
MarkWord结构
-
-
实例数据:存放当前对象的属性成员信息,以及父类属性成员信息;
-
对齐填充:虚拟机要求对象起始地址必须是
8byte的整数倍,避免减少堆内存的碎片空间,并且方便操作系统读取;
-
monitor对象
-
概念:
monitor本质是一个特殊的对象,存在于堆中,并且是线程私有的- 每个
java对象都存在一个monitor对象与之关联,当一个monitor被某个线程持有后,便会处于锁定状态;由ObjectMonitor实现,具体代码位于HotSpot源码的ObjectMonitor.hpp文件中;
-
与线程之间的关联:每个线程都有一个可用的
monitor record列表,同时也存在一个全局的可用列表,每一个锁住的对象,都会和monitor关联(对象头的MarkWord中的ptr_to_heavyweight_monitor,指向monitor的起始地址),同时monitor中有一个Owner字段,存放拥有该锁的线程唯一标识,表示该锁被这个线程占用; -
内部结构:

Contention List:竞争队列,**存放所有请求锁的线程(**后续1.8版本中的_cxq);Entry List:一个双向链表,存放Contention List中有资格成为候选资源的线程;Wait Set:哈希表,调用Object.wait()方法后,被阻塞的线程被放置在这里;OnDeck:任意时刻,最多只有一个线程正在竞争锁资源,该线程被称为OnDeck;Owner:拥有这个monitor record线程的唯一标识,为NULL说明没有被占用;!Owner:当前释放锁的线程;RcThis:表示blocked阻塞或waiting等待在该monitor record上的线程个数;Nest:用来实现重入锁的计数。Candidate:用来避免不必要的阻塞或等待线程唤醒,因为每一次只有一个线程能够成功拥有锁,如果每次前一个释放锁的线程,唤醒所有正在阻塞或等待的线程,会引起不必要的上下文切换(从阻塞到就绪,然后因为竞争锁失败又被阻塞),从而导致性能严重下降。Candidate只有两种可能的值,0表示没有需要唤醒的线程;1表示要唤醒一个继任线程来竞争锁;HashCode:保存从对象头拷贝过来的HashCode值(可能还包含GC age)。EntryQ:关联一个系统互斥锁(semaphore),阻塞所有试图锁住monitor record失败的线程;
-
使用流程:
- 每个获取锁或等待锁的线程都会被封装成
ObjectWaiter对象; Monitor有两个队列_WaitSet和_EntryList,用来保存ObjectWaiter对象列表;- 当多个线程同时访问一段同步代码时,获取到锁的对象会进入到
_owner区域,并将owner变量设置为当前线程的唯一标识,同时nest计数器加1,没有获取到锁的对象会加入_EntryList队列中等待; - 若线程调用
Object.wait()方法,会释放当前持有的monitor,owenr变量回复为null,同时nest计数器减1,并将线程放入到waitSet集合中等待被唤醒; - 当调用
Monitor对象的notify()或notifyAll()方法来唤醒WaitSet中的等待线程时,会将等待线程移动到EntryList队列中等待获取锁的机会;
- 每个获取锁或等待锁的线程都会被封装成
修饰代码块的原理
-
反编译代码:
-
源代码:
public class SyncDemo{int i;public void incr(){synchronized(this){i++;}} } -
字节码,
javac SyncDemo.javajavap -p -v -c SyncDemo.class:Classfile /Users/xrl/nacos/SyncDemo.classLast modified 2024-5-16; size 388 bytesMD5 checksum 0c87d23a96c5f67c7d97908dbd338f95Compiled from "SyncDemo.java" public class SyncDemominor version: 0major version: 52flags: ACC_PUBLIC, ACC_SUPER Constant pool:#1 = Methodref #4.#18 // java/lang/Object."<init>":()V#2 = Fieldref #3.#19 // SyncDemo.i:I#3 = Class #20 // SyncDemo#4 = Class #21 // java/lang/Object#5 = Utf8 i#6 = Utf8 I#7 = Utf8 <init>#8 = Utf8 ()V#9 = Utf8 Code#10 = Utf8 LineNumberTable#11 = Utf8 incr#12 = Utf8 StackMapTable#13 = Class #20 // SyncDemo#14 = Class #21 // java/lang/Object#15 = Class #22 // java/lang/Throwable#16 = Utf8 SourceFile#17 = Utf8 SyncDemo.java#18 = NameAndType #7:#8 // "<init>":()V#19 = NameAndType #5:#6 // i:I#20 = Utf8 SyncDemo#21 = Utf8 java/lang/Object#22 = Utf8 java/lang/Throwable {int i;descriptor: Iflags:public SyncDemo();descriptor: ()Vflags: ACC_PUBLICCode:stack=1, locals=1, args_size=10: aload_01: invokespecial #1 // Method java/lang/Object."<init>":()V4: returnLineNumberTable:line 1: 0/*-------synchronized修饰incr()中代码块,反汇编之后得到的字节码文件--------*/public void incr();descriptor: ()Vflags: ACC_PUBLICCode:stack=3, locals=3, args_size=10: aload_01: dup2: astore_13: monitorenter // monitorenter进入同步4: aload_05: dup6: getfield #2 // Field i:I9: iconst_110: iadd11: putfield #2 // Field i:I14: aload_115: monitorexit // monitorexit退出同步16: goto 2419: astore_220: aload_121: monitorexit // monitorexit退出同步22: aload_223: athrow24: returnException table:from to target type4 16 19 any19 22 19 anyLineNumberTable:line 5: 0line 6: 4line 7: 14line 8: 24StackMapTable: number_of_entries = 2frame_type = 255 /* full_frame */offset_delta = 19locals = [ class SyncDemo, class java/lang/Object ]stack = [ class java/lang/Throwable ]frame_type = 250 /* chop */offset_delta = 4 }
-
-
从字节码中可知,
synchronized修饰代码块,是基于进入管程monitorenter和退出管程monitorexit指令实现的,其中monitorenter指令指向同步代码块的开始位置,monitorexit指令则指明同步代码块的结束位置;
有两条monitorexit指令的原因是解决方法异常结束时,锁释放问题;
修饰方法的原理
-
反编译代码:
-
源代码:
public class SyncDemo {int i;public synchronized void incr() {i++;} } -
字节码:
Classfile /Users/xrl/nacos/SyncDemo.classLast modified 2024-5-16; size 388 bytesMD5 checksum 0c87d23a96c5f67c7d97908dbd338f95Compiled from "SyncDemo.java" public class SyncDemominor version: 0major version: 52flags: ACC_PUBLIC, ACC_SUPER Constant pool:#1 = Methodref #4.#18 // java/lang/Object."<init>":()V#2 = Fieldref #3.#19 // SyncDemo.i:I#3 = Class #20 // SyncDemo#4 = Class #21 // java/lang/Object#5 = Utf8 i#6 = Utf8 I#7 = Utf8 <init>#8 = Utf8 ()V#9 = Utf8 Code#10 = Utf8 LineNumberTable#11 = Utf8 incr#12 = Utf8 StackMapTable#13 = Class #20 // SyncDemo#14 = Class #21 // java/lang/Object#15 = Class #22 // java/lang/Throwable#16 = Utf8 SourceFile#17 = Utf8 SyncDemo.java#18 = NameAndType #7:#8 // "<init>":()V#19 = NameAndType #5:#6 // i:I#20 = Utf8 SyncDemo#21 = Utf8 java/lang/Object#22 = Utf8 java/lang/Throwable {int i;descriptor: Iflags:public SyncDemo();descriptor: ()Vflags: ACC_PUBLICCode:stack=1, locals=1, args_size=10: aload_01: invokespecial #1 // Method java/lang/Object."<init>":()V4: returnLineNumberTable:line 1: 0public void incr();descriptor: ()Vflags: ACC_PUBLICCode:stack=3, locals=3, args_size=10: aload_01: dup2: astore_13: monitorenter4: aload_05: dup6: getfield #2 // Field i:I9: iconst_110: iadd11: putfield #2 // Field i:I14: aload_115: monitorexit16: goto 2419: astore_220: aload_121: monitorexit22: aload_223: athrow24: returnException table:from to target type4 16 19 any19 22 19 anyLineNumberTable:line 5: 0line 6: 4line 7: 14line 8: 24StackMapTable: number_of_entries = 2frame_type = 255 /* full_frame */offset_delta = 19locals = [ class SyncDemo, class java/lang/Object ]stack = [ class java/lang/Throwable ]frame_type = 250 /* chop */offset_delta = 4 } SourceFile: "SyncDemo.java" xrl@xrldeMacBook-Air nacos % open 。/ The file /Users/xrl/nacos/。 does not exist. xrl@xrldeMacBook-Air nacos % open ./ xrl@xrldeMacBook-Air nacos % javac SyncDemo.java xrl@xrldeMacBook-Air nacos % javap -p -v -c SyncDemo.class Classfile /Users/xrl/nacos/SyncDemo.classLast modified 2024-5-16; size 276 bytesMD5 checksum ef45d44f86eef93281fefeda549e1283Compiled from "SyncDemo.java" public class SyncDemominor version: 0major version: 52flags: ACC_PUBLIC, ACC_SUPER Constant pool:#1 = Methodref #4.#14 // java/lang/Object."<init>":()V#2 = Fieldref #3.#15 // SyncDemo.i:I#3 = Class #16 // SyncDemo#4 = Class #17 // java/lang/Object#5 = Utf8 i#6 = Utf8 I#7 = Utf8 <init>#8 = Utf8 ()V#9 = Utf8 Code#10 = Utf8 LineNumberTable#11 = Utf8 incr#12 = Utf8 SourceFile#13 = Utf8 SyncDemo.java#14 = NameAndType #7:#8 // "<init>":()V#15 = NameAndType #5:#6 // i:I#16 = Utf8 SyncDemo#17 = Utf8 java/lang/Object {int i;descriptor: Iflags:public SyncDemo();descriptor: ()Vflags: ACC_PUBLICCode:stack=1, locals=1, args_size=10: aload_01: invokespecial #1 // Method java/lang/Object."<init>":()V4: returnLineNumberTable:line 1: 0public synchronized void incr();descriptor: ()Vflags: ACC_PUBLIC, ACC_SYNCHRONIZEDCode:stack=3, locals=1, args_size=10: aload_01: dup2: getfield #2 // Field i:I5: iconst_16: iadd7: putfield #2 // Field i:I10: returnLineNumberTable:line 5: 0line 6: 10 }
-
-
由字节码可知
synchronized修饰的方法,并没有出现monitorenter指令和monitorexit指令,取得代之的是:在flags: ACC_PUBLIC之后增加了一个ACC_SYNCHRONIZED标识。这个标识指明了当前方法是一个同步方法,JVM通过这个ACC_SYNCHRONIZED访问标志,来辨别一个方法是否为同步方法,从而执行相应的同步调用;
JVM对synchronized的优化
锁状态
JDK1.6之后,synchronized锁的状态总共有四种:无锁状态、偏向锁、轻量级锁和重量级锁;随着线程的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁,但是锁的升级一般是单向的,也就是说只能从低到高升级,通常不会出现锁的降级(只针对用户线程,在STW可能会发生锁降级)。
无锁状
new一个对象时,会默认启动匿名偏向锁,但为了避免JVM在启动阶段大量创建对象,从而导致偏向锁竞争过多影响性能,则在JVM启动阶段会次用延迟偏向锁策略,也就是等待一定时间后再开启偏向锁,默认为4秒,可通过-XX:BiasedLockingStartupDelay = xx设置;- 对于一个新创建的对象,由于在没有成为真正偏向锁之前,对象头
markword中的线程ID会一直为空,这种被称为概念上的无锁对象,但markword的锁标识为101;
偏向锁
- 为了减少同一线程获取锁的代价,如
CAS操作带来的耗时等; - 核心思想:如果一个线程获得了锁,那么锁就进入偏向模式,此时
Mark Word的结构也变为偏向锁结构,当这个线程再次请求锁时,无需再做任何同步操作; - 操作流程:
Load-and-test,就是简单判断一下当前线程id是否与Markword中的线程id是否一致;- 如果一致,则说明此线程持有的偏向锁,没有被其他线程覆盖,直接执行后续代码;
- 如果不一致,则要检查一下对象是否还属于可偏向状态,即检查“是否偏向锁”标志位;
- 如果还未偏向,则利用
CAS操作来竞争锁,再次将ID放进去,即重复第一次获取锁的动作;
- 偏向锁主要是在锁竞争不激烈的场合对性能的提升,但是对于锁竞争比较激烈的场合,偏向锁作用就很小了,甚至成为累赘,可以通过
XX:-UseBiasedLocking命令关闭;
撤销过程
- 在一个安全点停止拥有锁的线程;
- 遍历线程栈,如果存在锁记录的话,需要修复锁记录和
Markword,使其变成无锁状; - 唤醒当前线程,将当前锁升级成轻量级锁;
膨胀过程
- 当首个线程进程尝试获取锁时,会通过
CAS操作,将自己的threadID设置到MarkWord中,如果设置成功,则证明拿到偏向锁; - 当线程再次尝试获取锁时,发现自己的线程ID和对象头中的偏向线程ID一致,则在当前线程栈的
lock record锁记录中添加一个空的Displaced Mark Word(表示的是原先持有偏向锁的线程ID和相关信息被移动到哪里的标记),并不需要CAS操作; - 重新偏向,当其他线程进入同步块时,发现偏向线程不是自己,则进入偏向锁撤销的逻辑;当达到全局安全点时,如果发现偏向线程挂了,那就把偏向锁撤销,并将对象头内的
MarkWord修复为无锁状态,自己尝试获取偏向锁; - 可如果原本的偏向线程还存活,重新偏向失败后,锁开始膨胀为轻量级锁,原来的线程仍然持有锁;
轻量级锁
锁膨胀过程
- 根据
markWork判断是否有线程持有锁,如果有则在当前线程栈中创建一个lock record复制markWord,并通过CAS将当前线程栈的lock record地址放入对象头中,如果成功则说明获取到轻量级锁; - 如果失败则说明锁已经被其他线程持有了,此时记录线程的重入次数(把
ock record的markword设置为null),并进入自适应自旋; - 如果自旋到一定的次数后还未获取到锁,则说明目前竞争较重,则膨胀为重量级锁;
自旋
- 自旋锁:是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环;
- 存在的问题:
- 若加锁代码执行慢或锁竞争激烈,则需要等待较长时间,会导致CPU空转,消耗大量CPU资源;
- 非公平锁,若存在多个线程自旋等待同一把锁,可能对导致有些线程一致获取不到锁;
- 解决办法:通过
-XX:PreBliockSpin给线程空循环设置一个次数,默认是10次,或者自旋线程数超过CPU核数的一半时,锁会再次膨胀升级为重量级锁;
- 存在的问题:
- 自适应自旋锁:线程空循环次数不固定,而是会根据实际情况动态调整;
- 在大部分情况下,已经获取到锁的线程再次尝试获取锁,大概率能成功拿到该锁,所以虚拟机会延长这个线程的自旋次数;
重量级锁
- 当出现较大竞争锁膨胀为重量级锁时,对象头的
markword指向堆中的monitor,此时线程会封装为一个ObjectWaiter对象,并插入到monitor的_cxq竞争队列中然后挂起线程,等待锁释放; - 当持有锁的线程释放后,会将
_cxq竞争队列中的ObjectWaiter对象,移到EntryList中,并随机挑选一个对象也就是一个线程唤醒,被选中的线程称之为Heir Presumptive假定继承人,之后Heir Presumptive会去尝试获取锁,但在这段期间其他线程页可能尝试获取锁,所以Heir Presumptive不一定可以获取到锁,当没有获取到锁后那么它会退回到等待队列中,成为EntryList中的最后一个对象; - 当线程获取到锁后,调用
Object.wait()方法后,会将线程加入到WaitSet中,当被Object.notify()唤醒后,会将线程从WaitSet移动到_cxq或EntryList中去,并且由于Object.wait()、Object.notify()方法主要依赖于Monitor对象实现的,所以锁对象调用这两个方法时的锁状态如果为偏向锁或轻量级锁,则会先膨胀成重量级锁;
锁膨胀过程
- 无锁态:
JVM启动后-XX:BiasedLockingStartupDelay(默认四秒)内的普通对象和四秒后的匿名偏向锁对象; - 偏向锁:只有一个线程进入临界区;
- 偏向锁未开启:直接膨胀为轻量级锁;
- 将
MarkWord中锁标识位信息除外的其他所有信息copy到自己的栈内存的Lock Record中,再尝试通过CAS将MarkWord中的ptr_to_lock_record指向自己的栈内Lock Record,替换成功则获取到锁,没有则继续自旋;
- 将
- 匿名偏向锁:
- 将
MarkWord中锁标识位信息除外的其他所有信息copy到自己的栈内存的Lock Record中,并尝试通过CAS将自己的线程ID设置到MarkWord中;这个线程,后续再次使用这个锁时,无需进行加锁和锁释放操作,只需要在Lock Record添加一个空的MarkWord; - 重新偏向:当尝试获取锁时,发现线程ID与
MarkWord中记录的线程ID不相同,则进入偏向锁撤销的逻辑;当达到全局安全点时,发现之前持有锁的线程执行完毕,则会发生偏向锁撤销(清除锁记录回归无锁态),然后线程可以通过CAS将自己重新设置到MarkWord中 - 重新偏向失败:锁膨胀为轻量级锁,尝试通过
CAS将MarkWord中的ptr_to_lock_record指向自己的栈内Lock Record,替换成功则获取到锁,没有则继续自旋;
- 将
- 调用
Object.wait()方法,直接膨胀为重量级锁;
- 偏向锁未开启:直接膨胀为轻量级锁;
- 轻量级锁:多个线程交替进入临界区:
- 当出现重度竞争、耗时过长、自旋过多等情况时会膨胀为重量级锁;
- 自旋线程数超过
CPU核数的一半; - 自旋超过
-XX:PreBliockSpin默认10次;
- 自旋线程数超过
- 调用
Object.wait()方法,直接膨胀为重量级锁;
- 当出现重度竞争、耗时过长、自旋过多等情况时会膨胀为重量级锁;
- 重量级锁:多个线程同时进入临界区;
锁状态的内存布局分析
-
引入架包:
<dependency><groupId>org.openjdk.jol</groupId><artifactId>jol-core</artifactId><version>0.16</version> </dependency> -
MarkWord结构:
-
运行代码:
package com.xrl.test;import org.openjdk.jol.info.ClassLayout;/*** @version [v1.0]* @author: [xrl]* @create: [2024/05/21 16:14]**/ public class ObjectHead {public String str;public static void main(String[] args) throws InterruptedException {/**无锁态:虚拟机刚启动时 new 出来的对象处于无锁状态**/ObjectHead obj = new ObjectHead();System.out.println(ClassLayout.parseInstance(obj).toPrintable());/**com.xrl.test.ObjectHead object internals:OFF SZ TYPE DESCRIPTION VALUE0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0) // 对象头 non-biasable (转化为二进制最后三位表示,锁标志位状态:001)8 4 (object header: class) 0xf800c105 // ClassPointer指针12 4 java.lang.String ObjectHead.str null // 成员变量Instance size: 16 bytes // 共占用16个字节Space losses: 0 bytes internal + 0 bytes external = 0 bytes total*//**轻量级锁:对于真正的无锁态对象obj加锁之后的对象处于轻量级锁状态**/synchronized (obj) {// 查看对象内部信息System.out.println(ClassLayout.parseInstance(obj).toPrintable());/**com.xrl.test.ObjectHead object internals:OFF SZ TYPE DESCRIPTION VALUE0 8 (object header: mark) 0x0000000309dcd9d8 (thin lock: 0x0000000309dcd9d8) // (转化为二进制最后三位表示,锁标志位状态:000)8 4 (object header: class) 0xf800c10512 4 java.lang.String ObjectHead.str nullInstance size: 16 bytesSpace losses: 0 bytes internal + 0 bytes external = 0 bytes total*/}/**匿名偏向锁:休眠4S后再创建出来的对象处于匿名偏向锁状态**/Thread.sleep(4000);ObjectHead obj1 = new ObjectHead();System.out.println(ClassLayout.parseInstance(obj1).toPrintable());/**com.xrl.test.ObjectHead object internals:OFF SZ TYPE DESCRIPTION VALUE0 8 (object header: mark) 0x0000000000000005 (biasable; age: 0) //(转化为二进制最后三位表示,锁标志位状态:101)8 4 (object header: class) 0xf800c10512 4 java.lang.String ObjectHead.str nullInstance size: 16 bytesSpace losses: 0 bytes internal + 0 bytes external = 0 bytes total*//**重量级锁:调用wait方法之后锁对象直接膨胀为重量级锁状态**/new Thread(() -> {try {obj.wait();} catch (InterruptedException e) {e.printStackTrace();}}).start();Thread.sleep(1);synchronized (obj) {// 查看对象内部信息System.out.println(ClassLayout.parseInstance(obj).toPrintable());/*** com.xrl.test.ObjectHead object internals:* OFF SZ TYPE DESCRIPTION VALUE* 0 8 (object header: mark) 0x00007fe748016c0a (fat lock: 0x00007fe748016c0a) (转化为二进制最后三位表示,锁标志位状态:010)* 8 4 (object header: class) 0xf800c105* 12 4 java.lang.String ObjectHead.str null* Instance size: 16 bytes* Space losses: 0 bytes internal + 0 bytes external = 0 bytes total*/}} } /*** 抛出异常原因:违法的监控状态异常。当某个线程试图等待一个自己并不拥有的对象(Obj)的监控器或者通知其他线程等待该对象(Obj)的监控器时,抛出该异常* Exception in thread "Thread-1" java.lang.IllegalMonitorStateException* at java.lang.Object.wait(Native Method)* at java.lang.Object.wait(Object.java:502)* at com.xrl.test.ObjectHead.lambda$main$0(ObjectHead.java:53)* at java.lang.Thread.run(Thread.java:750)*/
同步消除
- Java虚拟机在编译代码时,通过会对运行上下文进行扫描,从而去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以节省毫无意义的获取锁开销;
锁的可重入
-
一个线程获得一个对象锁后,运行他再次请求该对象锁;
-
synchronized是基于monitor实现的,每次重入monitor中的计数器会加一; -
当子类继承父类时,子类可以通过可重入锁,调用父类的同步方法;
class Parent {public synchronized void parentMethod() {System.out.println("Parent method");} }class Child extends Parent {public synchronized void childMethod() {System.out.println("Child method");parentMethod(); // 在子类中调用父类的同步方法} }public class Main {public static void main(String[] args) {Child child = new Child();child.childMethod();} }
其他机制
等待/唤醒机制
wait()、notify()、notifyAll()这三个方法在使用时,必须处在synchronized代码块或方法中,否则会抛出IllegalMonitorStateException异常,这是由于这三个方法都依赖于monitor对象实现这也是三个方法处在Object对象中的原因,而synchronized关键字决定着一个JAVA对象会不会生成monitor对象;wait()、sleep()方法的区别:wait()方法会释放当前持有的锁,并将线程移入waitSet中;sleep()方法只会让线程休眠并不会释放锁(类似于执行for(;;){}死循环);
线程中断机制
-
JDK1.2遗弃Thread.stop()后,JAVA就没有提供强制性停止执行中线程的方法,而是提供了协调式的方式;//中断线程(实例方法) public void Thread.interrupt(); //判断线程是否被中断(实例方法) public boolean Thread.isInterrupted(); //判断是否被中断并清除当前中断状态(静态方法) public static boolean Thread.interrupted(); -
使用:我们可以通过调用
Thread.interrupt()来进行线程中断,但由于线程中断是协调式的,所以他并不会去停止线程,而是需要我们手动进行中断检测并结束线程;并且当线程处于阻塞状态或者尝试执行一个阻塞操作时,我们调用线程中断方法,执行中断操作后会抛出InterruptedException异常;public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(){@Overridepublic void run(){try {// 判断当前线程是否已中断,执行后会对中断状态进行复位while (!Thread.interrupted()) {System.out.println("1111");// 线程阻塞,抛出异常// TimeUnit.SECONDS.sleep(1);}System.out.println("线程中断");// 输出false 说明中断状态复位System.out.println(this.isInterrupted());} catch (Exception e) {System.out.println(e);}}};t1.start();TimeUnit.SECONDS.sleep(2);// 中断线程t1.interrupt();} -
synchronized与线程中断:对于synchronized而言,一个线程的执行只有两种状态,一种是获取了锁正在执行,一种是没获取到锁在阻塞额等待;那么他们即使调用中断线程的方法,也不会生效;
synchronized为什么不禁止指令重排序
synchronized是通过互拆锁的方式来保证线程安全的,它的本质是将多线程并行执行变成单线程的串行执行,而指令重排序可能导致的问题是多线程环境程序乱序执行的问题,所以指令重排序对synchronized而言并不会存在乱序问题,反而可以提升串行执行时的性能;
synchronized性能不佳的原因
synchronized是基于进入和退出Monitor管程实现的,而Monitor底层时依赖于操作系统的Mutex Lock,所以在其获取锁或者释放锁的时候都需要经过操作系统的调用,会涉及到频繁的用户态与内核态之间的切换,从而导致性能不佳;- 但在并发竞争不高的情况下,由于
synchronized几种锁状态、锁消除等技术的优化,synchronized性能并不差;