1.概述
syncronized是JAVA多线程开发中一个重要的知识点,涉及到多线程开发,多多少少都使用过。那么syncronized底层是如何实现的?为什么加了它就能实现资源串行访问?本文将基于上述问题,探索syncronized的用法及原理。
2.关键字syncronized的作用
2.1 JAVA并发编程中三大问题
1.原子性:在一次或多次操作中,要么所有的操作都执行并且不会受其它因素干扰而中断,要么所有的操作都不执行;
2.有序性:指的程序中代码的执行顺序,JAVA在编译时和运行时会对代码进行优化,导致程序最终的执行顺序不一定就是我们编写代码时的顺序;
3.可见性:当一个线程对共享变量进行了修改,其它线程能够立即看到修改后的值。
2.2 syncronized如何解决并发中三大问题
1.syncronized保证原子性:syncronized能够保证同一时刻最多只有一个线程执行该段代码,以达到并发安全的效果;
2.syncronized保证有序性:加关键字syncronized后,依然会发生指令重排序,由于有同步代码块,可以保证同一时间只有一个线程执行同步代码中的代码;
3.syncronized保证可见性:执行syncronized时,会对应lock原子操作,刷新工作内存中共享变量的值。
2.3 synchronized的使用场景
使用位置 | 作用范围 | 作用对象 | 案例代码 |
方法 | 普通实例方法 | 类的实例对象 |
|
静态方法 | 当前类对象 | 方法区中的类对象 |
|
Class对象 | 类对象 | 该Class对象 |
|
任意实例对象 object | 实例对象 | 该object对象 |
|
由上表可知,synchronized的具体功能包括:锁方法、锁对象和锁代码块。
2.4 synchronized的特性
2.4.1 可重入性
1.概念
一个线程可以多次执行synchronized,重复获取同一把锁。
2.案例
public class RecursionTest {
public static void main(String[] args) {
new syncThread().start();
new syncThread().start();
}
}
class syncThread extends Thread {
public void run() {
synchronized (RecursionTest.class) {
System.out.println(Thread.currentThread().getName() + "进入同步代码块1");
synchronized (RecursionTest.class) {
System.out.println(Thread.currentThread().getName() + "进入同步代码块2");
}
}
}
}
运行结果如下:
由上述代码可知,Thread-0分别打印并输出了同步代码块1和同步代码块2,说明获取并执行synchronized (RecursionTest.class) 这个代码两次,两次获取到同一把锁。
3.原理
synchronized的锁对象中有一个计数器(recursions变量)会记录线程获得几次锁。每次执行完同步代码块时,计数器的数量会减1,直到计算器为0才会释放这个锁。
4.优点
1.可以避免死锁;
2.可以更好地封装代码。
5.不可中断
一个线程获得锁后,另一个线程想要获得锁,必须处于阻塞或等待状态,如果第一个线程不释放锁,第二个线程会一直处于阻塞或等待,不可被中断。
2.5 synchronized原理
2.5.1 JAVA对象构成
要了解synchronized的工作原理,首先要了解JAVA对象的构成。对象在内存中的存储布局可以分为三个部分,分别是对象头、示例数据和填充数据。
存储区域 | 详细说明 |
实例数据 | 存放类的属性数据信息,包括父类的属性信息,这部分内存按4字节对齐 |
填充数据 | 由于虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐 |
对象头 | 在HotSpot虚拟机中,对象头又被分为两部分,分别为:Mark Word(标记字段)、Klass Pointer(类型指针)。如果是数组,那么还会有数组长度 |
接下来详细分析一下对象头,这块内容比较重要,也与我们今天的关键字synchronized息息相关。
- Mark Word(标记字段):默认存储对象的HashCode,分代年龄和锁标志位信息。它会根据对象的状态复用自己的存储空间,也就是说在运行期间Mark Word里存储的数据会随着锁标志位的变化而变化。
- Klass Point(类型指针):对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
每个对象实例都会关联一个Monitor对象,因此它也被称为管程或者监视器锁。Monitor对象既可以与对象一起创建销毁,也可以在线程视图获取对象锁时自动生成,当这个Monitor对象被线程持有时,它便处于锁定状态。关联关系图如下:
在HotSpot虚拟机中,Monitor是由ObjectMonitor的类实现的(底层C++),该类有很多属性,核心的属性主要包含以下几个:
- _ower :用来指向持有monitor的线程,它的初始值为NULL,表示当前没有任何线程持有monitor。当一个线程成功持有该锁之后会保存线程的ID标识,等到线程释放锁后_ower又会被重置为NULL;
- _WaitSet :调用了锁对象的wait方法后的线程会被加入到这个队列中;
- _cxq:是一个阻塞队列,线程被唤醒后根据决策判断是放入cxq还是EntryList;
- _EntryList:没有抢到锁的线程会被放到这个队列;
- count: 用于记录线程获取锁的次数,成功获取到锁后count会加1,释放锁时count减1。
ObjectMonitor的完整属性如下,来源于openjdk 8的jvm源码:
objectMonitor.cpp
ObjectMonitor() {
_header = NULL;
_count = 0; \\用来记录获取该锁的线程数
_waiters = 0,
_recursions = 0; \\锁的重入次数
_object = NULL;
_owner = NULL; \\当前持有ObjectMonitor的线程
_WaitSet = NULL; \\wait()方法调用后的线程等待队列
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ; \\阻塞等待队列
FreeNext = NULL ;
_EntryList = NULL ; \\synchronized 进来线程的排队队列
_SpinFreq = 0 ;
_SpinClock = 0 ; \\自旋计算
OwnerIsThread = 0 ;
}
2.5.2 对象头解析
对象头包括Mark Word和Klass Pointer。
1.Mark Word
下面是Hotspot对象头:
由上图可知,针对无锁状态和偏向锁,锁的标志位都是一样的,所以增加一个偏向锁位来进行区分(1:表示偏向锁,0:无锁状态)。
2.Klass Pointer
这一部分空间主要用来存储对象的类型指针,该指针指向它的类元数据,JVM通过这个指针确定对象是哪个类的实例。该指针的位长度为JVM的一个字大小,即32位的JVM为32位,64的JVM为64位。
如果应用的对象过多,使用64位的指针将会浪费大量内存,统计而言,64位的JVM将会比32位的JVM多耗费50%的内存。为了节约内存可以使用选项-xxX:+UseCompressedoops开启指针压缩,其中,oop即ordinary objectpointer普通对象指针。开启该选项后,下列指针将压缩至32位:
(1)每个Class的属性指针(即静态变量);
(2)每个对象的属性指针(即对象变量)3.普通对象数组的每个元素指针;
(3)一些特殊类型的指针JVM不会优化,比如指向PermGen的Class对象指针(JDK8中指向元空间的Class对象指针)、本地变量、堆栈元素、入参、返回值和NULL指针等。
在32位系统中,Mark Word = 4 bytes,类型指针 = 4bytes,对象头 = 8 bytes = 64 bits;
在64位系统中,Mark Word = 8 bytes,类型指针 = 8 bytes,对象头 = 16 bytes = 128 bits;
3.原理分析
3.1 修饰同步代码块
查看如下代码:
public class JAVAPSyncTest {
public static void main(String[] args) {
syncTest();
}
public static void syncTest() {
synchronized (JAVAPSyncTest.class) {
System.out.println("hello ");
}
}
}
利用javap指令反汇编上述代码,得到如下结果:
"C:\Program Files\java\jdk1.8.0_162\bin\javap.exe" -v com.eckey.lab.sync.JAVAPSyncTest
Classfile /F:/ideaworkspace/springboot-demos/springboot-juc/target/classes/com/eckey/lab/sync/JAVAPSyncTest.class
Last modified 2022-6-22; size 779 bytes
MD5 checksum b9991d2649bdb91fd9e81ec14f00837f
Compiled from "JAVAPSyncTest.java"
public class com.eckey.lab.sync.JAVAPSyncTest
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #7.#26 // java/lang/Object."<init>":()V
#2 = Methodref #3.#27 // com/eckey/lab/sync/JAVAPSyncTest.syncTest:()V
#3 = Class #28 // com/eckey/lab/sync/JAVAPSyncTest
#4 = Fieldref #29.#30 // java/lang/System.out:Ljava/io/PrintStream;
#5 = String #31 // hello
#6 = Methodref #32.#33 // java/io/PrintStream.println:(Ljava/lang/String;)V
#7 = Class #34 // java/lang/Object
#8 = Utf8 <init>
#9 = Utf8 ()V
#10 = Utf8 Code
#11 = Utf8 LineNumberTable
#12 = Utf8 LocalVariableTable
#13 = Utf8 this
#14 = Utf8 Lcom/eckey/lab/sync/JAVAPSyncTest;
#15 = Utf8 main
#16 = Utf8 ([Ljava/lang/String;)V
#17 = Utf8 args
#18 = Utf8 [Ljava/lang/String;
#19 = Utf8 MethodParameters
#20 = Utf8 syncTest
#21 = Utf8 StackMapTable
#22 = Class #34 // java/lang/Object
#23 = Class #35 // java/lang/Throwable
#24 = Utf8 SourceFile
#25 = Utf8 JAVAPSyncTest.java
#26 = NameAndType #8:#9 // "<init>":()V
#27 = NameAndType #20:#9 // syncTest:()V
#28 = Utf8 com/eckey/lab/sync/JAVAPSyncTest
#29 = Class #36 // java/lang/System
#30 = NameAndType #37:#38 // out:Ljava/io/PrintStream;
#31 = Utf8 hello
#32 = Class #39 // java/io/PrintStream
#33 = NameAndType #40:#41 // println:(Ljava/lang/String;)V
#34 = Utf8 java/lang/Object
#35 = Utf8 java/lang/Throwable
#36 = Utf8 java/lang/System
#37 = Utf8 out
#38 = Utf8 Ljava/io/PrintStream;
#39 = Utf8 java/io/PrintStream
#40 = Utf8 println
#41 = Utf8 (Ljava/lang/String;)V
{
public com.eckey.lab.sync.JAVAPSyncTest();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/eckey/lab/sync/JAVAPSyncTest;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=0, locals=1, args_size=1
0: invokestatic #2 // Method syncTest:()V
3: return
LineNumberTable:
line 5: 0
line 6: 3
LocalVariableTable:
Start Length Slot Name Signature
0 4 0 args [Ljava/lang/String;
MethodParameters:
Name Flags
args
public static void syncTest();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=0
0: ldc #3 // class com/eckey/lab/sync/JAVAPSyncTest
2: dup
3: astore_0
4: monitorenter
5: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
8: ldc #5 // String hello
10: invokevirtual #6 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
13: aload_0
14: monitorexit
15: goto 23
18: astore_1
19: aload_0
20: monitorexit
21: aload_1
22: athrow
23: return
Exception table:
from to target type
5 15 18 any
18 21 18 any
LineNumberTable:
line 9: 0
line 10: 5
line 11: 13
line 12: 23
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 18
locals = [ class java/lang/Object ]
stack = [ class java/lang/Throwable ]
frame_type = 250 /* chop */
offset_delta = 4
}
由上述反汇编代码可知,System.out.println("hello ");行对应的代码,分别被monitorenter和moniterexit两条指令包围。如下图所示:
中间的指令执行的就是打印hello的语句。出现这种情况的原因是:
每一个对象都会和一个监视器Monitor关联,监视器被占用时会被锁住,其它线程无法来获取该monitor。当JVM执行某个线程的某个方法内部的monitorenter时,它会尝试去获得当前对象对应的monitor所有权,其过程如下:
(1)若monitor的进入数为0,线程可以进入monitor,并将monitor的进入设置为1,当前线程成为monitor的owner(所有者);
(2)若线程已拥有monitor的所有权,允许它能重入monitor,则进入monitor的进入数加1,也就是_recursions属性加1;
(3)若其它线程已经占有monitor的所有权,那么当前尝试获取monitor的所有权线程会被阻塞,直到monitor的进入数变为0,才能重新尝试获取monitor的所有权。
3.2 修饰同步方法
案例代码如下:
public class JAVAPSyncTest {
public static void main(String[] args) {
syncTest();
}
public static synchronized void syncTest() {
System.out.println("hello ");
}
}
利用javap反编译得到结果如下:
"C:\Program Files\java\jdk1.8.0_162\bin\javap.exe" -v com.eckey.lab.sync.JAVAPSyncTest
Classfile /F:/ideaworkspace/springboot-demos/springboot-juc/target/classes/com/eckey/lab/sync/JAVAPSyncTest.class
Last modified 2022-6-23; size 672 bytes
MD5 checksum a75c1ee1c8a796d3ee3db04ff54afe9f
Compiled from "JAVAPSyncTest.java"
public class com.eckey.lab.sync.JAVAPSyncTest
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #7.#23 // java/lang/Object."<init>":()V
#2 = Methodref #6.#24 // com/eckey/lab/sync/JAVAPSyncTest.syncTest:()V
#3 = Fieldref #25.#26 // java/lang/System.out:Ljava/io/PrintStream;
#4 = String #27 // hello
#5 = Methodref #28.#29 // java/io/PrintStream.println:(Ljava/lang/String;)V
#6 = Class #30 // com/eckey/lab/sync/JAVAPSyncTest
#7 = Class #31 // java/lang/Object
#8 = Utf8 <init>
#9 = Utf8 ()V
#10 = Utf8 Code
#11 = Utf8 LineNumberTable
#12 = Utf8 LocalVariableTable
#13 = Utf8 this
#14 = Utf8 Lcom/eckey/lab/sync/JAVAPSyncTest;
#15 = Utf8 main
#16 = Utf8 ([Ljava/lang/String;)V
#17 = Utf8 args
#18 = Utf8 [Ljava/lang/String;
#19 = Utf8 MethodParameters
#20 = Utf8 syncTest
#21 = Utf8 SourceFile
#22 = Utf8 JAVAPSyncTest.java
#23 = NameAndType #8:#9 // "<init>":()V
#24 = NameAndType #20:#9 // syncTest:()V
#25 = Class #32 // java/lang/System
#26 = NameAndType #33:#34 // out:Ljava/io/PrintStream;
#27 = Utf8 hello
#28 = Class #35 // java/io/PrintStream
#29 = NameAndType #36:#37 // println:(Ljava/lang/String;)V
#30 = Utf8 com/eckey/lab/sync/JAVAPSyncTest
#31 = Utf8 java/lang/Object
#32 = Utf8 java/lang/System
#33 = Utf8 out
#34 = Utf8 Ljava/io/PrintStream;
#35 = Utf8 java/io/PrintStream
#36 = Utf8 println
#37 = Utf8 (Ljava/lang/String;)V
{
public com.eckey.lab.sync.JAVAPSyncTest();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/eckey/lab/sync/JAVAPSyncTest;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=0, locals=1, args_size=1
0: invokestatic #2 // Method syncTest:()V
3: return
LineNumberTable:
line 5: 0
line 6: 3
LocalVariableTable:
Start Length Slot Name Signature
0 4 0 args [Ljava/lang/String;
MethodParameters:
Name Flags
args
public static synchronized void syncTest();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
Code:
stack=2, locals=0, args_size=0
0: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #4 // String hello
5: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 9: 0
line 10: 8
}
由上述反汇编代码可知,利用synchronized修饰同步代码块,并没有出现monitorenter和moniterexit来修饰,这又是为啥?原因在于同步方法在被synchronized修饰后,会增加ACC_SYNCHRONIZED修饰,会隐式调用monitorenter和moniterexit。再执行完同步方法前会调用monitorenter,在执行完同步方法后会调用moniterexit。
3.3 synchronized锁升级过程
synchronized锁在JDK 1.5到JDK 1.6时进行了一个升级,HotSpot虚拟机开发团队花了大量的时间进行了锁的优化,包括偏向锁(Biased Locking)、轻量级锁(Lightweight Locking)、适应性自旋(Adaptive Spining)、锁消除(Lock Elimination)、锁粗化(Lock Coarsening)等,这些技术都是为了在线程之间更高效地共享数据、以及解决竞争问题,从而提高程序执行效率。
锁升级的主要流程如下:
(1) jvm会有4s的偏向锁开启延迟时间,在这个偏向延迟内对象处于为无锁态。如果关闭偏向锁启动延迟、或是经过4秒且没有线程竞争对象的锁,那么对象会进入无锁可偏向状态。准确来说,无锁可偏向状态应该叫做匿名偏向(Anonymously biased)状态,因为这时对象的mark word中后三位已经是101,但是threadId指针部分仍然全部为0,它还没有向任何线程偏向。综上所述,对象在刚被创建时,根据jvm的配置对象可能会处于 无锁 或 匿名偏向两个状态;
(2) 在无锁不可偏向的情况下,如果有线程获取锁,则会直接由无锁状态变为轻量级锁状态;
(3)偏向锁升轻量级锁,这一过程通常伴随另一个线程获取到了锁,也就意味着不仅仅只有当前一个线程来通过锁访问资源;
(4)轻量级锁升重量级,一般是多个线程竞争资源导致(重度竞争),偏向锁也可通过调用wait和hashCode方法来获取重量级锁(耗时过长wait)。
3.3.1 偏向锁原理
1.偏向锁使用场景
偏向锁是JDK1.6中引进的,由于大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低,引进了偏向锁。偏向锁会在对象头存储锁偏向的线程ID,以后该线程进入和退出同步块时只需要检查是否为偏向锁、锁标志位以及ThreadID即可。一旦出现多个线程竞争时,偏向锁就会被撤销。
2.偏向锁原理
当锁对象第一次被获取的时候,虚拟机将会把对象头中的标志位设为"01"(偏向模式),同时使用CAS操作把获取到这个锁的线程ID记录在对象的Mark Word之中,如果CAS操作成功,持有偏向锁的线程以后每次进入这个锁相关的同步块时,虚拟机都可以不再进行任何同步操作,偏向锁效率高。
3.偏向锁的撤销
1.偏向锁的撤销动作必须等待全局安全点;
2.暂停拥有偏向锁的线程,判断对象是否处于被锁定状态;
3.撤销偏向锁,恢复到无锁(标志位为01)或轻量级锁(标志位为00)的状态。
偏向锁是在应用程序启动几秒钟之后才激活,可以使用-xXx:BiasedLockingStartupDelay=0参数关闭延迟,如果确定应用程序中所有锁通常情况下处于竞争状态,可以通过xx:-UseBiasedLocking=false参数关闭偏向锁。
全局安全点指的是某一时刻线程全部停止。
3.偏向锁的优势
偏向锁是在只有一个线程执行同步块时提高性能,适用于一个线程反复获得同一把锁的情况。偏向锁可以提高带有同步但无竞争的程序性能。它同样是一个带有效益权衡性质的优化,也就是说,它并不一定总是对程序运行有利,如果程序中大多数的锁总是被多个不同的线程访问比如线程池,那偏向模式就是多余的。
3.3.2 轻量级锁(自旋锁)
1.什么是轻量级锁
轻量级锁是JDK 1.6之中加入的新型锁机制,“轻量级”是相对于使用monitor传统锁而言的,因此传统的锁机制就称为"重量级"锁。"轻量级锁"并不是用来代替重量级锁的,引入轻量级锁能够避免重量级锁引起的性能消耗。如果多个线程在同一时刻进入临界区,会导致锁膨胀升级重量级锁,所以轻量级锁的出现并非是要替代重量级锁。
2.轻量级锁原理
轻量级锁的升级步骤如下:
1.判断当前对象是否处于无锁状态(hashcode、0、01),如果是,则VM首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝(官方把这份拷贝加了一个Displaced前缀,即Displaced
Mark Word),将对象的Mark Word复制到栈帧中的Lock Record中,将LockReocrd中的owner指向当前对象;
2.JVM利用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,如果成功表示竞争到锁,则将锁标志位变成00,执行同步操作;
3.如果失败则判断当前对象的Mark Word是否指向当前线程的栈帧,如果是则表示当前线程已经持有当前对象的锁,则直接执行同步代码块;否则只能说明该锁对象已经被其他线程抢占了,这时轻量级锁需要膨胀为重量级锁,锁标志位变成10,后面等待的线程将会进入阻塞状态。
3.轻量级锁出现场景
锁内部执行的代码较少,能够快速执行完成,获取锁的时间不用太长。
4.为什么有轻量级锁还需要重量级锁
轻量级锁(自旋锁)是消耗CPU资源的,如果锁的时间较长,或者自旋线程较多,会占用大量CPU资源。重量级锁有等待队列,所有未获取到锁的线程会进入队列等待,不需要消耗CPU资源。
5.偏向锁是否一定比轻量级锁效率高
不一定。在明确知道会有多线程竞争的情况下,偏向锁会涉及锁撤销,消耗大量性能。所有如果一开始就知道涉及线程竞争资源,应使用轻量级锁。
6.锁优化-适应性自旋(Adaptive Spining)
从轻量级锁获取的流程中我们知道,当线程在获取轻量级锁的过程中执行CAS操作失败时,是要通过自旋来获取重量级锁的。问题在于,自旋是需要消耗CPU的,如果一直获取不到锁的话,那该线程就一直处在自旋状态,白白浪费CPU资源。
其中解决这个问题最简单的办法就是指定自旋的次数,例如让其循环10次,如果还没获取到锁就进入阻塞状态。但是JDK采用了更聪明的方式——适应性自旋,简单来说就是线程如果自旋成功了,则下次自旋的次数会更多,如果自旋失败了,则自旋的次数就会减少。
7.锁消除
锁消除即删除不必要的加锁操作。锁消除的主要判定依据来源于逃逸分析的数据支持,如果判断在一段代码中,堆上的所有数据都不会逃逸出去从而被其他线程访问到,那就可以把它们当做栈上数据对待,认为它们是线程私有的,同步加锁自然就无须进行。如下面这段代码:
public class LockReleaseDemo {
public static void main(String[] args) {
String str = appendStr();
System.out.println(str);
}
public static synchronized String appendStr() {
StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append("aa");
stringBuffer.append("bb");
stringBuffer.append("cc");
return stringBuffer.toString();
}
}
虽然StringBuffer的append是一个同步方法,但是这段程序中的StringBuffer属于一个局部变量,并且不会从该方法中逃逸出去,所以其实这过程是线程安全的,可以将锁消除。因此synchronized 修饰可去除。
8.锁粗化
锁粗化指的是将多次连接在一起的加锁、解锁操作合并为异常,并将多个连续的锁扩展成一个范围更大的锁。见下面代码:
public static void main(String[] args) {
StringBuffer stringBuffer = new StringBuffer();
for (int i = 0; i < 100; i++) {
stringBuffer.append("a");
}
System.out.println(stringBuffer.toString());
}
由于StringBuffer 的append方法被synchronized 修饰,for循环中执行100次append,按照理论会进行100次加锁解锁。事实上,JVM会探测到一连串细小的操作都使用同一个对象加锁,将同步代码块的范围放大,放到这串操作的外面,这样只需加锁一次即可。上述代码JVM会自动优化,在for循环外加一次锁。
3.3.3 重量级锁
一旦从轻量级锁切换为重量级锁,意味着所有获取锁的线程都会被防置在一个队列中,线程的阻塞和唤醒需要CPU从用户态切换到内核态,频繁的阻塞和唤醒对CPU来说是很耗费性能的一个操作。
4.小结
1.synchronized是一个关键字,底层全部由JVM帮助实现,且在1.6之后进行了一系列优化,性能有了极大地提升;
2.synchronized锁升级过程是逐级递增,包含无锁、偏向锁、轻量级锁和重量级锁等四种锁状态;
3.synchronized和Lock都可以加锁,一个是JVM层面,一个是JDK层面,synchronized会自动释放锁,而Lock必须手动释放锁;
4.synchronized能锁住方法和代码块,而Lock只能锁住代码块;
5.synchronized使用时应尽量较少synchronized的范围(减少synchronized代码块内部代码)、降低synchronized锁的粒度(锁拆分)、读写分离(读取时不加锁,写入和删除时加锁)。