1.Synchronized的性能变化
2.synchronized锁种类及升级步骤
3.JIT编译器对锁的优化
4.总结
1.Synchronized的性能变化
我们都知道synchronized关键字能够让程序串行化执行,保证数据的安全性,但是性能会下降。
所以java对synchronized进行了一系列的优化:
java5之前:
synchronized仅仅只是synchronized,这个操作是重量级别的操作,cpu在进入加锁的程序后,会进行用户态和内核态之间的切换。
用户态:用户态运行用户程序,级别较低
内核态:内核态运行操作系统程序,操作硬件,级别较高。
java如果要阻塞或者唤醒一个线程需要操作系统的接入,需要在用户态和核心态之间切换,因为synchronized属于重量级锁,是需要依赖底层操作系统的Mutex Lock来实现的,挂起线程和恢复线程都需要进入内核态去完成,这种切换会消耗大量的系统资源,如果同步代码块中的内容过于简单,这种切换的时间可能比用户代码的执行时间还长,时间成本太高,这也是为什么早起synchronized效率低的原因。
java6开始:
优化Synchronized,为了减少获得锁和释放锁所带来的性能消耗,引入了偏向锁,轻量级锁和重量级锁(减少了线程的阻塞和唤醒)。
2.synchronized锁种类及升级步骤
在说synchronized锁升级之前,我们要先搞清楚,线程访问一个synchronized修饰的方法,有三种类型:
1)只有一个线程来访问,有且唯一。
2)有两个线程A,B来交替访问
3)竞争激烈,多个线程来访问
还记得我们上一篇博客 JAVA并发编程——Java对象内存布局和对象头中提到了对象头,我们先来看看这张图:
我们可以看出synchronized用的锁是存在java对象头的Mark Word中,锁升级功能主要依赖Mark Word中锁标志位和释放偏向锁标志位。
java的锁升级按照
无锁->偏向锁->轻量级锁->重量级锁
我们挨个进行讲解。
无锁:
我们先看一段无锁的代码
public static void main(String[] args) {
//-XX:-UseCompressedClassPointers -XX:BiasedLockingStartupDelay=0
Object o = new Object();
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
这个对象没有使用锁,我们看一下运行的结果。
这个输出的结果,对象头是倒着输出的,标红的地方便是锁标志位,现在是001,对应的对象头的图就是无锁状态。
偏向锁:
当一段同步代码一直被同一个线程多次访问,由于只有一个线程,那么该线程在后续访问的时候,就会自动获得锁。(这个锁偏向了经常访问的线程。)
在测试偏向锁之前,记得先输入jvm参数:-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0 因为偏向锁默认在jdk1.6之后默认是开启的,但是启动时间有延迟,所以需要手动添加参数,让偏向锁的延时时间为0,在程序启动时立刻开启。
Object o = new Object();
new Thread(() -> {
synchronized (o){
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
},"t1").start();
轻量级锁:
刚刚上面讲述的只是有一个线程正在争抢一个资源类,但是现在有另外线程来逐步竞争锁的时候,就不能使用偏向锁了,要升级为轻量级锁。
在第一个线程正在执行synchronized方法(处于同步块),当它还没有执行完的时候,其它线程来抢夺,竞争线程使用cas更新对象头失败,该偏向锁会被取消并出现锁升级。
此时轻量级锁由原持有偏向锁的线程持有,继续执行其同步代码,而正在竞争的线程会进入自旋等待获取该轻量级锁。
//关闭延时参数,启用该功能
Object o = new Object();
new Thread(() -> {
synchronized (o){
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
},"t1").start();
那么竞争到底自旋多少次会进行锁升级呢?
java6之前:
默认情况下自旋的次数为10次:-XX:PreBlockSpin=10
或者自旋次数超过cpu核数一半
java6之后:
自适应:意味着自旋次数是不固定的。
是根据:同一个锁上次自旋的时间和拥有锁线程的状态来确定。
偏向锁和自旋锁的区别:
争夺轻量级锁失败的时候,自旋尝试抢占锁
轻量级锁每次退出同步代码块都需要释放锁,而偏向锁是在竞争发生时才释放锁。
重锁:
有大量线程正在抢占同一个资源类,冲突性很高,会升级成重量级锁。
new Thread(() -> {
synchronized (o){
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
},"t1").start();
new Thread(() -> {
synchronized (o){
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
},"t2").start();
new Thread(() -> {
synchronized (o){
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
},"t3").start();
3.JIT编译器对锁的优化
1)锁消除
当只有一个线程运行synchronized代码的时候,默认会把锁消除,节省资源。
/**
* 锁消除
* 从JIT角度看相当于无视它,synchronized (o)不存在了,这个锁对象并没有被共用扩散到其它线程使用,
* 极端的说就是根本没有加这个锁对象的底层机器码,消除了锁的使用
*/
public class LockClearUPDemo
{
static Object objectLock = new Object();//正常的
public void m1()
{
//锁消除,JIT会无视它,synchronized(对象锁)不存在了。不正常的
Object o = new Object();
synchronized (o)
{
System.out.println("-----hello LockClearUPDemo"+"\t"+o.hashCode()+"\t"+objectLock.hashCode());
}
}
public static void main(String[] args)
{
LockClearUPDemo demo = new LockClearUPDemo();
for (int i = 1; i 10; i++) {
new Thread(() -> {
demo.m1();
},String.valueOf(i)).start();
}
}
}
2)锁粗化
加入一个锁在同一个方法中,头尾相接,前后相邻的都是一个锁对象,那么编译器就会把这几个synchronized合并成一大块,加粗了范围,节省了资源。
/**
* 锁粗化
* 假如方法中首尾相接,前后相邻的都是同一个锁对象,那JIT编译器就会把这几个synchronized块合并成一个大块,
* 加粗加大范围,一次申请锁使用即可,避免次次的申请和释放锁,提升了性能
*/
public class LockBigDemo
{
static Object objectLock = new Object();
public static void main(String[] args)
{
new Thread(() -> {
synchronized (objectLock) {
System.out.println("11111");
}
synchronized (objectLock) {
System.out.println("22222");
}
synchronized (objectLock) {
System.out.println("33333");
}
},"a").start();
new Thread(() -> {
synchronized (objectLock) {
System.out.println("44444");
}
synchronized (objectLock) {
System.out.println("55555");
}
synchronized (objectLock) {
System.out.println("66666");
}
},"b").start();
}
}
4.总结
锁 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
偏向锁 | 加锁和解锁不需要额外的消耗,和执行非同步方法只有纳秒的差距 | 如果线程间存在锁竞争,会带来额外的锁撤销的消耗 | 只有一个线程访问同步块的场景 |
轻量级锁 | 竞争的线程不会阻塞,提高了线程的相应速度 | 始终得不到cpu的话,会空转,浪费cpu | 追求相应时间,同步块执行速度非常快 |
重量级锁 | 线程竞争不适用自旋,不消耗cpu | 线程阻塞,造成用户态和内核态的切换,响应时间慢 | 追求数据一致性,同步执行块执行速度较长 |
锁升级用一句话概括:
先自旋,不行再阻塞。
就是把之前的悲观锁(重量级锁)在变成一定条件下使用偏向锁以及使用轻量级锁。
synchronized在修饰方法和代码块在字节码上实现方式有很大差异,但是内部实现还是基于对象头的MarkWord来实现的。
JDK1.6之前synchronized使用的是重量级锁,JDK1.6之后进行了优化,拥有了无锁->偏向锁->轻量级锁->重量级锁的升级过程,而不是无论什么情况都使用重量级锁。