synchronized
1. 基本介绍
synchronized
块是Java提供的一种原子性内置锁,也叫监视器锁。线程的执行代码在进入synchronized
代码块前会自动获取内部锁,这个时候其他线程访问该同步代码块时,会被阻塞挂起。
拿到内部锁的线程会在正常退出同步块 或者 抛出异常后 或者在同步块内调用了 wait系列方法时 释放该内置锁
JVM
基于进入和退出Monitor
对象来实现方法同步和代码块同步。代码块同步是使用monitorenter
和monitorexit
指令实现的,monitorenter
指令是在编译后插入到同步代码块的开始位置,而monitorexit
是插入到方法结束处和异常处。任何对象都有一个monitor
与之关联,当且一个monitor
被持有后,它将处于锁定状态。
monitor
对象分为三个部分。waitSet
,EntryList
,Owner
。当执行到Synchronized
代码时,每个obj
对象可以拥有一个Monitor
对象,Owner(只能拥有一个值)
属性记录当前线程Thread2
,代表当前线程Thread2
拥有这个锁对象,obj
锁的其他线程会被记录到EntryList
阻塞队列中去,当当前线程Thread2
释放掉锁后,obj
的Monitor
会唤醒EntryList
中记录的线程,不公平的再次争夺这个锁对象。
1.1 synchronized 保证线程安全
public class SafeDemo {
static int count = 0;
public static void main(String[] args) {
for (int i = 0; i < 20; i++) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 10000; j++) {
count++;
}
// System.out.println(Thread.currentThread().getName()+" already end");
}
},"thread:"+i);
thread.start();
}
//等待所有线程执行完毕
while (Thread.activeCount()>2){
}
System.out.println("count:"+count);
}
}
如同上面的案例,创建了20个线程,每个线程累加count一万次。理想状态下,得到的count值应该为200000。但是我们可以看到每次程序执行得到的结果永远都是小于200000。这种现象造成的原因是因为多个线程竞争同一个共享变量引发的线程安全问题。通过synchronized加锁的方式可以解决这个问题,如下。
public class SafeDemo { static int count = 0; public static void main(String[] args) { for (int i = 0; i < 20; i++) { Thread thread = new Thread(new Runnable() { @Override public void run() { //保证线程安全 synchronized (SafeDemo.class){ for (int j = 0; j < 10000; j++) { count++; } } // System.out.println(Thread.currentThread().getName()+" already end"); } },"thread:"+i); thread.start(); } //等待所有线程执行完毕 while (Thread.activeCount()>2){ } System.out.println("count:"+count); } }
仅仅只需要改变
run()
,使用synchronized
关键字。保证同一时间内只有一个线程操作共享变量;
1.2 JVM中保证线程安全的原理
package com.wddong.testmoduleone.demoResp.synchronizedPackage;
public class SyncDemo {
public static void main(String[] args) {
synchronized (Object.class){
System.out.println("synchronized jvm");
}
}
}
cd D:\ideaworkspace\wddongProject\testmoduleone\target\classes\com\wddong\testmoduleone\de
moResp\synchronizedPackage
javap -v SyncDemo.class
通过javap
命令得到源文件的字节码,我们可以看到synchronized
关键字,在JVM中是通过monitorenter
和monitorexit
指令来保证线程同步的;
2. 锁对象分析
synchronized
锁对象的分析:
- 静态方法: 静态方法
synchronized
锁的是Class
- 实例方法:
synchronized
锁的是 方法的调用对象 - 代码块:可以任意的去指定是锁
Class
模板还是具体对象
public class SynchronizedDemo {
public static void main(String[] args) {
SynchronizedCase synchronizedCase = new SynchronizedCase();
// 可以任意的去指定是锁Class模板还是具体对象
// synchronized (SynchronizedCase.class){
synchronized (synchronizedCase){
}
}
}
public class SynchronizedCase{
//synchronized修饰静态方法,锁的是Class
public synchronized static void method1(){
}
//synchronized修饰实例方法,锁的是方法的调用对象
public synchronized void method2(){
}
}
3. 锁升级
减少了获取锁和释放锁带来的性能损耗,Java 1.6
对synchronized
进行了各种优化;引入偏向锁和轻量级锁Java 1.6
一共有四种状态,从低到高依次是无锁态==>偏向锁==>轻量级锁==>重量级锁
。锁只可以升级不能降级;
4. 对象头
一个Java对象的组成分为对象头
,实例数据
,对齐填充
;对象头
又分为Mark word
,类型指针
两部分;
Mark Word
的组成:对象自身的运行时数据,HashCode
,GC分代年龄
,锁状态标志,线程持有的锁,偏向线程ID等;
32bit
和64bit
虚拟机下的Mark Word
的存储结构差异:
5. 偏向锁
大多数情况下,锁不仅不存在多线程竞争,且总是由同一线程多次获取。为了让线程获取锁的代价更低而引入了偏向锁;
1. 偏向锁的加锁
当一个线程访问同步块并获取锁时,会在对象头
和栈帧中的锁记录(Lock Record)
中存储锁偏向的线程ID,以后该线程在进入和退出同步代码块时不需要进行CAS操作来加锁解锁;
2. 偏向锁的撤销
偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有执行的字节码)。它会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着,如果线程不处于活动状态,则将对象头设置为无锁状态。如果线程仍然存活,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的mark word 要么重新偏向其他线程,要么恢复到无锁或者标记对象不适合作为偏向锁,最后唤醒暂停的线程;
3. 偏向锁的取消
偏向锁在Java6和Java7
里是默认启用的。但是他在应用程序启动几秒钟之后才激活,如有必要可以使用JVM
参数来关闭延迟:--XX:BiasedLockingStartupDelay=0
;
JVM
参数关闭偏向锁:-XX:-UseBiasedLocking=false
,程序默认会进入轻量级锁状态;
6. 轻量级锁
轻量级锁加锁
线程在执行同步块之前,JVM 会先在当前线程的栈帧中创建用于存储锁记录(Lock record)
的空间,将对象头中的Mark word
复制到锁记录(Lock record)
中,官方称为displaced Mark word
,然后线程尝试使用CAS将对象头中的Mark word
替换成指向锁记录的指针;如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁;
轻量级锁解锁
轻量级锁解锁时,会使用原子的CAS操作将Displaced Mark word
替换回对象头,如果成功,则表示没有竞争发生,如果失败,则表示当前锁存在竞争,锁就会膨胀成重量级锁;
如果锁竞争情况严重,某个达到最大自旋次数的线程,会将轻量级锁升级为
重量级锁
。
7. 重量级锁
重量级锁:当后续线程尝试获取锁时,发现被占用的锁是重量级锁,则直接将自己挂起,等待将来被唤醒。
8. 锁升级过程
阶段1:
线程1
进入同步代码块,此时没有其他线程竞争。成功通过CAS
获取到锁,此时Mark Word
和栈的锁记录(Lock Record)
记录下线程1的线程ID
,同时Mark Word
中 是否偏向锁和锁标志位值为1|01
;阶段2:
线程1
再次进入到同步代码块,此时锁对象中的Mark word
中记录的还是线程1的线程ID
没有被其他线程修改,所以此时为偏向锁,无需CAS
操作直接成功加锁。阶段3:
线程2
尝试通过CAS
获取对象锁,此时对象的Mark Word
存储的还是线程1的线程ID
。所以此处要先执行偏向锁的撤销(上面有讲到)
。,最终栈中的锁记录和对象头的mark word
偏向线程2的线程ID
阶段4:
线程2
再次尝试获取锁对象,但是此时刚好也有线程3
尝试来获取锁对象。此时就会存在锁竞争。偏向锁会升级称为轻量级锁(上面我们也有介绍到轻量级锁的加锁过程)。此时栈的锁记录(Lock Record)
会将对象头中Mark Word
复制过来,然后线程尝试CAS
将对象头中的Mark Word
替换成锁记录的指针,成功代表获取锁成功,失败则自旋再次获取;阶段5:上面
阶段4
有提到失败自旋再次获取,当自旋超过一定的次数后,轻量级锁就会升级称为重量级锁;
阶段5
中长时间的自旋操作是非常消耗资源的,一个线程持有锁,其他线程就只能在原地空耗CPU,执行不了任何有效的任务,这种现象叫做忙等(busy-waiting
)。synchronized
允许短时间的盲等。忙等是有限度的(有个计数器记录自旋次数,默认允许循环10次,可以通过虚拟机参数更改)。当自旋数超过限制,就会升级为重量级锁;