基本概念
- 线程安全问题三个要素:多线程、共享资源、非原子性操作;
- 产生的根本原因:多条线程同时对一个共享资源进行非原子性操作;
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.java
javap -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
性能并不差;