目录
- 1、共享带来的问题
- 2、synchronized 用法
- 3、类加载器对 Class 锁的影响
- 4、synchronized 实现原理
- 4.1、同步方法、同步代码块
- 4.2、对象内存布局
- 4.3、Monitor 对象定义
- 5、synchronized 与原子性
- 6、synchronized 与可见性
- 7、synchronized 与有序性
- 8、synchronized 锁升级
- 8.1、概述
- 8.2、偏向锁
- 8.2.1、概念
- 8.2.2、偏向锁的实现
- 8.2.3、案例
- 8.2.4、偏向锁的撤销
- 8.3、轻量级锁
- 8.4、重量级锁
- 9、自旋锁
- 10、自适应自旋锁
- 11、锁消除
- 12、锁粗化
1、共享带来的问题
public class ThreadTest {
private static int count = 0;
public static void main(String[] args) throws Exception {
for (int i = 0; i < 100; i++) {
Thread addThread = new Thread(() -> {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
count++;
});
addThread.start();
}
Thread.sleep(3000);
System.out.println("count = " + count);
}
}
把上述代码执行多次后,发现每次的结果不一样(自增并非是原子操作)。
在多个线程对共享资源读写操作时发生指令交错,就会出现问题
2、synchronized 用法
synchronized:俗称【对象锁】。它采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住。这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换【保证一个共享资源在同一时间只会被一个线程访问到】
它有两种用法:同步方法和同步代码块。Java 中的每一个对象都可以作为锁。具体表现为以下 3 种形式:
- 对于普通同步方法,锁是当前实例对象
- 对于静态同步方法,锁是当前类的 Class 对象。多个线程同时访问静方法,线程会发生互斥(即一个线程访问,另一个线程只能等着),因为静态方法是依附于类对象而不是实例对象的
- 对于同步方法块,锁是 synchonized 括号里配置的对象
如:使用同步方法改造:
public class ThreadTest {
private static int count = 0;
public synchronized void incr() {
count++;
}
public static void main(String[] args) throws Exception {
ThreadTest threadTest = new ThreadTest();
for (int i = 0; i < 100; i++) {
Thread addThread = new Thread(() -> {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
threadTest.incr();
});
addThread.start();
}
Thread.sleep(3000);
System.out.println("count = " + count);
}
}
3、类加载器对 Class 锁的影响
在 JVM 里,Class 的唯一性是由 Class 全限定名和 classloader 决定的,同一个全限定名的 class 被不同的 classloader 加载,最终的 class 对象是不一样的
public class ThreadTest {
public void one() {
synchronized (this.getClass()) {
try {
System.out.println("start-" + Thread.currentThread().getName());
Thread.sleep(1000);
System.out.println("end-" + Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void two() {
synchronized (this.getClass()) {
try {
System.out.println("start-" + Thread.currentThread().getName());
Thread.sleep(1000);
System.out.println("end-" + Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
ThreadTest test = new ThreadTest();
ThreadTest test2 = new ThreadTest();
Runnable task1 = test::one;
Runnable task2 = test2::two;
new Thread(task1).start();
new Thread(task2).start();
}
}
执行结果如下:
start-Thread-0
end-Thread-0
start-Thread-1
end-Thread-1
结论:通过对 Class 对象(this.getClass()
) 加锁,即使调用的是不同的实例对象,也能达到互斥访问的效果,因为它们的 Class 是相同的,竞争的是同一把锁
当我们的类加载器使用的不是同一个的情况下,会出现不同的 Class 对象
自定义一个类加载器:
public class MyClassLoader extends URLClassLoader {
public MyClassLoader(URL[] urls) {
super(urls);
}
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
Class<?> c = findLoadedClass(name);
if (c == null) {
// 类名
if ("com.zzc.demos.threads.ThreadTest".equals(name)) {
c = findClass(name);
} else {
return super.loadClass(name, resolve);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
}
再次执行:
public static void main(String[] args) throws Exception {
URL url = new File("D:\\code\\test2\\test2\\target\\classes").toURL();
MyClassLoader myClassLoader = new MyClassLoader(new URL[]{url});
MyClassLoader myClassLoader2 = new MyClassLoader(new URL[]{url});
//分别使用 myClassLoader 和 myClassLoader 2加载
Class clazz1 = myClassLoader.loadClass("com.zzc.demos.threads.ThreadTest");
Class clazz2 = myClassLoader2.loadClass("com.zzc.demos.threads.ThreadTest");
Method method01 = clazz1.getMethod("one");
Method method02 = clazz2.getMethod("two");
Runnable task1 = () -> {
try {
method01.invoke(clazz1.newInstance());
} catch (Exception e) {
e.printStackTrace();
}
};
Runnable task2 = () -> {
try {
method02.invoke(clazz2.newInstance());
} catch (Exception e) {
e.printStackTrace();
}
};
new Thread(task1).start();
new Thread(task2).start();
}
执行结果如下:
start-Thread-1
start-Thread-0
end-Thread-1
end-Thread-0
发现此时的 synchronized 没有同步的作用了(两个线程同时执行)
4、synchronized 实现原理
4.1、同步方法、同步代码块
先看一段代码(同步方法、同步代码块):
public class Test3 {
public synchronized void test1() {}
public void test2() {
synchronized(this){}
}
}
编译后,找到该类 class 文件目录,然后在命令行执行 javap 命令:
javap -verbose Test3.class > test.txt
可以把对应的字节码疏导 test.txt 中,内容如下:
反编译后,我们可以看到 Java 编译器为我们生成的字节码,发现 JVM 对于同步方法和同步代码块的处理方式不同。
- 同步方法:JVM 采用
ACC_SYNCHRONIZED
标记符来实现同步 - 同步代码快:JVM 采用
monitorenter
、monitorexit
两个指令来实现同步
在 The Java® Virtual Machine Specification 中有关于同步方法和同步代码块的实现原理的介绍:
方法级的同步是隐式的。同步方法的常量池中会有一个 ACC_SYNCHRONIZED 标志。当某个线程要访问某个方法的时候,会检查是否有 ACC_SYNCHRONIZED,如果有设置,则需要先获得监视器锁,然后开始执行方法,方法执行之后再释放监视器锁。这时如果其他线程来请求执行方法,会因为无法获得监视器锁而被阻断住。值得注意的是,如果在方法执行过程中,发生了异常,并且方法内部并没有处理该异常,那么在异常被抛到方法外面之前监视器锁会被自动释放。
同步代码块使用 monitorenter 和 monitorexit 两个指令实现。可以把执行 monitorenter 指令理解为加锁,执行 monitorexit 理解为释放锁。 每个对象维护着一个记录着被锁次数的计数器。未被锁定的对象的该计数器为 0,当一个线程获得锁(执行 monitorenter )后,该计数器自增变为 1 ,当同一个线程再次获得该对象的锁的时候,计数器再次自增。当同一个线程释放锁(执行 monitorexit 指令)的时候,计数器再自减。当计数器为0的时候。锁将被释放,其他线程便可以获得锁
无论是 ACC_SYNCHRONIZED
还是 monitorenter
、monitorexit
都是基于 Monitor 实现的,在 Java 虚拟机(HotSpot)中,Monitor 是基于 C++ 实现的,由 ObjectMonitor 实现。
ObjectMonitor 类中提供了几个方法,如 enter、exit、wait、notify、notifyAll 等。sychronized 加锁的时候会调用 objectMonitor 的 enter 方法,解锁的时候会调用 exit 方法
4.2、对象内存布局
一个 Java 对象在内存中是如何存储的?
对象的实例保存在堆上,对象的元数据保存在方法区,对象的引用保存在栈上
在内存中,一个 Java 对象包含三部分:对象头、实例数据和对齐填充。其中,对象头是一个很关键的部分,因为对象头中包含锁状态标志、线程持有的锁等标志
对象头分为:对象的运行时数据(Mark Word)、类型指针
- 对象的运行时数据:GC 分代年龄、哈希码、锁状态标识 等。这部分数据在 32 位和 64 位的虚拟机中分别占 32 位和 64 位。但是对象需要存储的运行时数据很多,32 位或者 64 位都不一定能存的下,考虑到虚拟机的空间效率,这个 Mark Word 被设计成一个非固定的数据结构,它会根据对象的状态复用自己的存储空间,对象处于不同状态的时候,对应的 bit 表示的含义可能会不一样
- 类型指针:虚拟机可以通过这个指针来确认该对象是哪个类的实例(不是所有的虚拟机都必须以这种方式来确定对象的元数据信息)。对象的访问定位一般有句柄和直接指针两种,如果使用句柄的话,那么对象的元数据信息可以直接包含在句柄中(当然也包括对象实例数据的地址信息),也就没必要将这些元数据和实例数据存储在一起了
对 markword 的设计方式上,非常像网络协议报文头:将 mark word 划分为多个比特位区间,并在不同的对象状态下赋予比特位不同的含义。下图描述了在32位虚拟机上,在对象不同状态时 mark word各个比特位区间的含义:
4.3、Monitor 对象定义
ObjectMonitor() {
_header = NULL;
_count = 0;
_waiters = 0,//等待线程数
_recursions = 0;//重入次数
_object = NULL;
_owner = NULL;//持有锁的线程(逻辑上,实际上除了THREAD,还可能是Lock Record)
_WaitSet = NULL;//线程wait之后会进入该列表
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;//等待获取锁的线程列表,和_EntryList配合使用
FreeNext = NULL ;
_EntryList = NULL ;//等待获取锁的线程列表,和_cxq配合使用
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;//当前持有者是否为THREAD类型,如果是轻量级锁膨胀而来,还没有enter的话,
//_owner存储的可能会是Lock Record
_previous_owner_tid = 0;
}
当多个线程同时访问一段同步代码时,首先会进入 _EntryList
队列中,当某个线程获取到对象的 monitor 后进入 _Owner
区域并把 monitor 中的 _owner
变量设置为当前线程,同时 monitor
中的计数器 _count
加 1。即获得对象锁。
若持有 monitor
的线程调用 wait()
方法,将释放当前持有的 monitor
,_owner
变量恢复为 null,_count
自减 1,同时该线程进入 _WaitSet
集合中等待被唤醒。若当前线程执行完毕也将释放monitor
( 锁)并复位变量的值,以便其他线程进入获取 monitor
(锁)
5、synchronized 与原子性
原子性:一个操作是不可中断的,要全部执行完成,要不就都不执行
线程是 CPU 调度的基本单位。CPU 有时间片的概念,会根据不同的调度算法进行线程调度。当一个线程获得时间片之后开始执行,在时间片耗尽之后,就会失去 CPU 使用权。所以在多线程场景下,由于时间片在线程间轮换,就会发生原子性问题。
在 Java 中,为了保证原子性,提供了两个高级的字节码指令 monitorenter
和 monitorexit
,可以保证被 synchronized 修饰的代码在同一时间只能被一个线程访问,在锁未释放之前,无法被其他线程访问到
线程1在执行
monitorenter
指令的时候,会对 Monitor 进行加锁,加锁后其他线程无法获得锁,除非线程 1 主动解锁。即使在执行过程中,由于某种原因,比如 CPU 时间片用完,线程 1 放弃了 CPU,但是,他并没有进行解锁。而由于 synchronized 的锁是可重入的,下一个时间片还是只能被他自己获取到,还是会继续执行代码。直到所有代码执行完。这就保证了原子性
6、synchronized 与可见性
可见性:当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值
被 synchronized 修饰的代码,在开始执行时会加锁,执行完成后会进行解锁。而为了保证可见性,有一条规则是这样的:对一个变量解锁之前,必须先把此变量同步回主存中。这样解锁后,后续线程就可以访问到被修改后的值
7、synchronized 与有序性
有序性:程序执行的顺序按照代码的先后顺序执行
除了引入了时间片以外,由于处理器优化和指令重排等,CPU 还可能对输入代码进行乱序执行,可能存在有序性问题。
synchronized
是无法禁止指令重排和处理器优化的。也就是说,synchronized
无法避免上述提到的问题。
那为什么还说
synchronized
也提供了有序性保证呢?
Java 程序中天然的有序性可以总结为一句话:如果在本线程内观察,所有操作都是天然有序的。如果在一个线程中观察另一个线程,所有操作都是无序的。这其实和 as-if-serial
语义有关
as-if-serial
语义:不管怎么重排序(编译器和处理器为了提高并行度),单线程程序的执行结果都不能被改变。编译器和处理器无论如何优化,都必须遵守 as-if-serial
语义
as-if-serial
语义保证了单线程中,指令重排是有一定的限制的,而只要编译器和处理器都遵守了这个语义,那么就可以认为单线程程序是按照顺序执行的。当然,实际上还是有重排的,只不过我们无须关心这种重排的干扰
由于 synchronized
修饰的代码,同一时间只能被同一线程访问。那么也就是单线程执行的。所以,可以保证其有序性
8、synchronized 锁升级
8.1、概述
事实上,只有在 JDK1.6 之前,synchronized 的实现才会直接调用 ObjectMonitor
的 enter
和 exit
,这种锁被称之为重量级锁
为什么说这种方式操作锁很重呢?
Java 的线程是映射到操作系统原生线程之上的,如果要阻塞或唤醒一个线程就需要操作系统的帮忙,这就要从用户态转换到核心态,因此状态转换需要花费很多的处理器时间,对于代码简单的同步块(如被 synchronized
修饰的 get
或 set
方法)状态转换消耗的时间有可能比用户代码执行的时间还要长,所以说 synchronized
是 java 语言中一个重量级的操作
所以,在 JDK1.6 中出现对锁进行了很多的优化,进而出现轻量级锁,偏向锁,锁消除,适应性自旋锁,锁粗化(自旋锁在 1.4 就有 只不过默认的是关闭的,jdk1.6 是默认开启的),这些操作都是为了在线程之间更高效的共享数据 ,解决竞争问题【这些其实对于使用它的开发者来说是屏蔽掉了的,也就是说,作为一个 Java 开发,你只需要知道你想在加锁的时候使用 synchronized
就可以了,具体的锁的优化是虚拟机根据竞争情况自行决定的】
锁升级: JVM 优化 synchronized
运行的机制,当 JVM 检测到不同的竞争状况时,会自动切换到适合的锁实现,这种切换就是锁的升级。
当没有竞争出现时,默认会使用偏向锁。JVM 会利用 CAS 操作(compare and swap),在对象头上的 Mark Word 部分设置线程 ID,以表示这个对象偏向于当前线程,所以并不涉及真正的互斥锁。这样做的假设是基于在很多应用场景中,大部分对象生命周期中最多会被一个线程锁定,使用偏斜锁可以降低无竞争开销。
如果有另外的线程试图锁定某个已经被偏向过的对象,JVM 就需要撤销(revoke)偏向锁,并切换到轻量级锁实现。轻量级锁依赖 CAS 操作 Mark Word 来试图获取锁,如果重试成功,就使用普通的轻量级锁;否则,进一步升级为重量级锁。
8.2、偏向锁
8.2.1、概念
多线程的情况下,锁不仅不存在多线程竞争,还存在锁由同一线程多次获得的情况(偏向锁:为了解决只有在一个线程执行同步时提高性能)。而且,在实际应用运行过程中发现:“锁总是同一个线程持有,很少发生竞争”,也就是说锁总是被第一个占用他的线程拥有,这个线程就是锁的偏向线程。那么,只需要在锁第一次被拥有的时候,记录下偏向线程 ID。
这样,偏向线程就一直持有着锁。后续这个线程进入和退出这段加了同步锁的代码时,不需要再次加锁和释放锁。而是直接比较对象头里面是否存储了指向当前线程的偏向锁,如果相等,表示偏向锁是偏向于当前线的,就不需要再尝试获得锁了,直到竞争发生才释放锁。
以后每次同步,检查锁的偏向线程 ID 与当前线程 ID 是否一致:如果一致,直接进入同步。无需每次加锁解锁都去 CAS 更新对象头。如果自始至终使用锁的线程只有一个,很明显偏向锁几乎没有额外开销,性能极高;如果不一致,意味着发生了竞争,锁已经不是总是偏向于同一个线程了,这时候可能需要升级变为轻量级锁,才能保证线程间公平竞争锁。
偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程是不会主动释放偏向锁的
8.2.2、偏向锁的实现
一个 synchronized
方法被一个线程抢到了锁时,那这个方法所在的对象就会在其所在的 Mark Word
中将偏向锁修改状态位,同时还会有占用前 54 位来存储线程指针作为标识。若该线程再次访问同一个synchronized
方法时,该线程只需去对象头的 Mark Word
中去判断一下是否有偏向锁指向本身的 ID,无需再进入 Monitor
去竞争对象了
偏向锁的操作不用直接捅到操作系统,不涉及用户到内核转换,不必要直接升级为最高级
8.2.3、案例
以一个 account 对象的“对象头”为例
假如有一个线程执行到 synchronized
代码块的时候,JVM 使用 CAS 操作把线程指针 ID 记录到 Mark Word 当中,并修改标偏向标示,标示当前线程就获得该锁。锁对象变成偏向锁(通过 CAS 修改对象头里的锁标志位)。执行完同步代码块后,线程并不会主动释放偏向锁
这时线程获得了锁,可以执行同步代码块。当该线程第二次到达同步代码块时会判断此时持有锁的线程是否还是自己(持有锁的线程 ID 也在对象头里),JVM 通过 account 对象的 Mark Word 判断:当前线程 ID 还在,说明还持有着这个对象的锁,就可以继续进入临界区工作。由于之前没有释放锁,这里也就不需要重新加锁。
如果自始至终使用锁的线程只有一个,很明显偏向锁几乎没有额外开销,性能极高
结论:JVM 不用和操作系统协商设置 Mutex(争取内核),它只需要记录下线程 ID 就标示自己获得了当前锁,不用操作系统接入。上述就是偏向锁:在没有其他线程竞争的时候,一直偏向偏心当前线程,当前线程可以一直执行
8.2.4、偏向锁的撤销
偏向锁的撤销:偏向锁使用一种等到竞争出现才释放锁的机制,只有当其他线程竞争锁时,持有偏向锁的原来线程才会被撤销。撤销需要等待全局安全点(该时间点上没有字节码正在执行),同时检查持有偏向锁的线程是否还在执行:
- 第一个线程正在执行 synchronized 方法(处于同步块),它还没有执行完,其它线程来抢夺,该偏向锁会被取消掉并出现锁升级为轻量级锁。此时,轻量级锁由原持有偏向锁的线程持有,继续执行其同步代码,而正在竞争的线程会进入自旋等待获得该轻量级锁
- 第一个线程执行完成 synchronized 方法(退出同步块),则将对象头设置成无锁状态并撤销偏向锁,重新偏向
8.3、轻量级锁
轻量级锁是为了在线程近乎交替执行同步块时提高性能(竞争不激烈),在没有多线程竞争的前提下,通过 CAS 减少重量级锁使用操作系统互斥量产生的性能消耗【先自旋再阻塞】
升级时机:当关闭偏向锁功能或多线程竞争偏向锁会导致偏向锁升级为轻量级锁
假如线程 A 已经拿到锁,这时线程 B 又来抢该对象的锁,由于该对象的锁已经被线程 A 拿到,当前该锁已是偏向锁了。而线程 B 在争抢时发现对象头 Mark Word 中的线程 ID 不是线程 B 自己的线程 ID(而是线程 A),那线程 B 就会进行 CAS 操作希望能获得锁。
此时线程 B 操作中有两种情况:如果锁获取成功,直接替换 Mark Word 中的线程 ID 为 B 自己的 ID (A → B),重新偏向于其他线程(即将偏向锁交给其他线程,相当于当前线程"被"释放了锁),该锁会保持偏向锁状态,A 线程 Over,B 线程上位;如果锁获取失败,则偏向锁升级为轻量级锁,此时轻量级锁由原持有偏向锁的线程持有,继续执行其同步代码,而正在竞争的线程 B 会进入自旋等待获得该轻量级锁
8.4、重量级锁
有大量的线程参与锁的竞争,冲突性很高,自旋到达一定次数还是没有获取锁成功,这时候轻量级锁就会膨胀为重量级锁,当锁膨胀为重量锁时,就不能再退回到轻量级锁
9、自旋锁
自旋锁在 JDK 1.4 中已经引入,在 JDK 1.6 中默认开启。
在程序中,Java 虚拟机的开发工程师们在分析过大量数据后发现:共享数据的锁定状态一般只会持续很短的一段时间,为了这段时间去挂起和恢复线程其实并不值得。
如果物理机上有多个处理器,可以让多个线程同时执行的话。我们就可以让后面来的线程“稍微等一下”,但是并不放弃处理器的执行时间,看看持有锁的线程会不会很快释放锁。这个“稍微等一下”的过程就是自旋
自旋锁和阻塞锁最大的区别就是:到底要不要放弃处理器的执行时间?
阻塞锁和自旋锁来都是要等待获得共享资源。但是阻塞锁是放弃了 CPU 时间,进入了等待区,等待被唤醒;自旋锁是一直“自旋”在那里,时刻的检查共享资源是否可以被访问
自旋锁只是将当前线程不停地执行循环体,不进行线程状态的改变,所以响应速度更快。但当线程数不停增加时,性能下降明显,因为每个线程都需要执行,占用 CPU 时间。如果线程竞争不激烈,并且保持锁的时间段。适合使用自旋锁。
如果锁被占用很长时间的话,自旋的线程等待的时间也会变长,白白浪费掉处理器资源。因此在JDK中,自旋操作默认 10 次,我们可以通过参数 “-XX:PreBlockSpin”
来设置,当超过来此参数的值,则会使用传统的线程挂起方式来等待锁释放
10、自适应自旋锁
JDK 1.6 的时候,又出现了一个“自适应自旋锁”。它的出现使得自旋操作变得聪明起来,不再跟之前一样死板。所谓的“自适应”意味着对于同一个锁对象,线程的自旋时间是根据上一个持有该锁的线程的自旋时间以及状态来确定的。
例如对于 A 锁对象来说,如果一个线程刚刚通过自旋获得到了锁,并且该线程也在运行中,那么 JVM 会认为此次自旋操作也是有很大的机会可以拿到锁,因此它会让自旋的时间相对延长。但是如果对于 B 锁对象自旋操作很少成功的话,JVM 甚至可能直接忽略自旋操作。因此,自适应自旋锁是一个更加智能,对我们的业务性能更加友好的一个锁
11、锁消除
除了自旋锁之后,JDK 中还有一种锁的优化被称之为锁消除 (JIT编译器对内部锁的具体实现所做的一种优化)
在动态编译同步块的时候,JIT 编译器可以借助一种被称为逃逸分析(Escape Analysis)的技术来判断同步块所使用的锁对象是否只能够被一个线程访问而没有被发布到其他线程
如果同步块所使用的锁对象通过这种分析被证实只能够被一个线程访问,那么 JIT 编译器在编译这个同步块的时候就会取消对这部分代码的同步
如:
public void f() {
Object obj = new Object();
synchronized(obj) {
System.out.println(obj);
}
}
代码中对 obj 这个对象进行加锁,但是 obj 对象的生命周期只在 f() 方法中,并不会被其他线程所访问到,所以在 JIT 编译阶段就会被优化掉。优化成:
public void f() {
Object obj = new Object();
System.out.println(obj);
}
其实,一般有经验的程序员是有能力判断是否需要加锁的,像这段代码完全没必要加锁。但是还是有可能会疏忽。如:经常在代码中使用StringBuffer
作为局部变量,而 StringBuffer
中的 append()
是线程安全的,有 synchronized
修饰的,这种情况开发者可能会忽略。这时候,JIT 就可以帮忙优化,进行锁消除
总之,在使用 synchronized 的时候,如果 JIT 经过逃逸分析之后发现并无线程安全问题的话,就会做锁消除
12、锁粗化
在代码中,需要加锁的时候,我们提倡尽量减小锁的粒度,这样可以避免不必要的阻塞。这也是很多人原因是用同步代码块来代替同步方法的原因,因为往往它的粒度会更小一些。
如果在一段代码中连续的对同一个对象反复加锁解锁,其实是相对耗费资源的,这种情况可以适当放宽加锁的范围,减少性能消耗
当 JIT 发现一系列连续的操作都对同一个对象反复加锁和解锁,甚至加锁操作出现在循环体中的时候,会将加锁同步的范围扩散(粗化)到整个操作序列的外部
如:
for (int i = 0;i < 100000; i++) {
synchronized(this) {
// TODO
do();
}
}
会被粗化成:
synchronized(this) {
for (int i = 0;i < 100000; i++) {
// TODO
do();
}
}
【参考资料】
synchronized
轻松搞懂Java中的自旋锁