理解JVM对synchronized进行的优化

文章目录

  • 一、monitor具体的实现的原理
    • 1.系统调用产生的性能损耗
    • 2.偏向锁
    • 3.轻量级锁
    • 4. 升级是不可逆的
    • 5.锁削除
    • 6.锁粗化
    • 补充:

从 synchronized关键字初步理解中可以知道 synchronized的作用和实现原理是通过 monitor对象的获取和释放。这里来讲讲我对synchronized的优化的理解,那要理解优化,首先得知道问题在哪,那么先了解monitor对象是如何实现同步的呢。

一、monitor具体的实现的原理

monitor对象实际上很复杂。存储了,所有在等待获取该monitor的线程对象,等等。这里说明时需要用到线程的两种状态,也就是运行态和阻塞态。运行态就是线程当前在运行,阻塞态也就是当前线程处于sleep(休眠)状态,一般是在等待需要的资源可用,那么java这里的同步也是同样的。如下面的代码

private static Pen instance;
public static synchronized void draw() {...};

假设现在有一支笔(也就是单例模式),所有的实例对象共用一支笔去画画(调用draw方法)。

  • (1)假设现在A线程调用了draw()方法,获得了这个类的锁,也就是该类对应的Monitor对象。
  • (2)随后B线程也调用了draw()方法,但是获取不到笔(因为A在用)。

这里的(2)中,获取不到笔(也就是获取不到类锁)要干嘛呢。总不能一直尝试去获取类锁吧(一直尝试会消耗CPU周期,浪费运算资源,学过单片机的应该能很容易理解,中断方式和while方式实现的sleep有什么区别)。所以需要进入阻塞态等到A用完了,来通知他恢复到运行态

也就是说B获取不到B后,要进入阻塞态。线程转入阻塞态需要调用系统函数SuspendThread()(假设是windows系统)。A执行完之后,释放该类对应的Monitor的使用权。然后A就调用ResumeThread()唤醒线程B。B被唤醒后,获取到了A释放了的Monitor对象,继续运行,直到完成。

1.系统调用产生的性能损耗

从这里可以知道,monitor对象实际上很复杂,因为复杂,就算只有一个线程重复的执行draw,获取锁也是会特别消耗性能的。而且线程的状态的变化是要通过系统函数的调用来操控的。实际上系统调用是非常消耗资源的,对性能影响非常大。[至于为什么,后面的补充部分的第二个部分进行了说明]。如果A能快速的用完笔呢,但是,B又一下都没等,就直接进入阻塞态了。如果B稍微等等,那么就不需要进入阻塞态,也就不需要进行系统调用,减少了性能损耗,提高了执行效率。所以JVM针对这样的情况进行了优化,除了我们刚刚说的Monitor这种锁(重量级锁)加入了两种新的锁:偏向锁轻量级锁,还加入了一些其他的优化:自旋锁削除锁粗化

2.偏向锁

因为monitor对象的复杂,就算只有一个线程重复的执行draw,获取锁也是会特别消耗性能的。偏向锁就是用来优化这个情况的。

  1. 如果是只有一个线程A一直重复执行的情况。
  • A拿到画笔之后。通过CAS操作把自己的线程ID写入到该类头的一个字段中。获得了该锁的使用权
  • A用完了画笔,随后通过CAS操作把当时写入的自己的线程ID清空,释放了该锁的使用权
    [至于CAS操作是什么,看下面补充的第三部分]

这样的处理,就能优化A能很快的处理完的情况,而不用进行系统调用。

2.如果有B也要获取偏向锁呢,那就是这样的情况了。

  • A拿到画笔之后。通过CAS操作把自己的线程ID写入到该类头的一个字段中。获得了该锁的使用权
  • B通过CAS操作试着写入自己的线程ID,但是发现那个地方不是空的而是A的线程ID。
  • 所以,B通知线程A取消掉偏向锁。
  • A接受到消息之后,暂停当前的任务(实际上就是虚拟机暂停了A),先将偏向锁取消,也就是CAS操作,把类头的那个字段清空。并且把类头上面,的锁的标志位,改为,我们接下来要说的轻量级锁

所以这三个锁,是会进行升级,转化的,会按照下面的顺序升级。
偏向锁 —>轻量级锁 —>重量级锁

3.轻量级锁

经过了上面的升级后,该类的锁已经由偏向锁变成了轻量级锁

  • A和B线程都把对象的类头重的相关字段复制到自己的线程栈中。
  • A线程通过CAS操作,把共享对象的类头重的相关字段的内容修改为自己新建的记录空间的地址。
  • 这个时候B就会尝试去获取轻量级锁
  • 然后B获取不到,会进入自旋状态也就是会进行多次CAS操作尝试。下面就会有两种情况。
  • 如果A线程通过,CAS操作,释放了锁,B线程随后获取到了,那么就会正常执行下去。
  • 如果B线程尝试了多次之后,还没得到,虚拟机就暂停A,然后会把锁的标志位改成重量级锁,并把线程B的状态信息写入到Monitor的阻塞队列中,线程B进入到阻塞状态,以及A的信息也写入monitor,然后恢复A的执行。
  • 这个时候锁就从轻量级锁,升级成了重量级锁。

4. 升级是不可逆的

从上面的情况可以看到,升级的过程,总是要暂停那个正在执行的线程,然后进行锁升级,升级完成后,恢复执行。那么如果锁等级可逆。那么可能会存在锁升级过多次出现的情况,(可能对大部分情况来说)对性能消耗大。所以JVM实现的这个锁升级是不可逆的。也就是,例如不可以从重量级锁,回到之前的锁。

5.锁削除

这个其实很好理解,也就是通过分析,发现并不需要加锁,也能保证执行正确。也就是只可能会有一个线程的情况。那么这个锁,完全可以去除。

6.锁粗化

这个也很好理解,比如下面的代码

{
	draw();
	draw();
	draw();
}

那么这里的三个过程都要获取锁,如果分开,获取。那么可能会使得锁的获取和释放次数增加,为了减少次数,提高性能。就会把三个锁合成一把锁。那么这个时候,就只有一次的获取和释放,性能会有提升。当然,如果每个draw()处理时间很久,这时可能就不太好了。

补充:

  1. 这个存储着线程对象的列表是用来干嘛的呢?
    答:在A释放了类对应的Monitor的使用权后,那这个时候B还在睡觉呢,得叫醒B进入运行态。那A怎么知道有B在等呢。所以这就是为什么Monitor对象中存储了所有在等待获取该monitor的线程对象。这样A就知道要叫醒谁了。

  2. 为什么系统调用非常消耗资源,对性能影响非常大?
    答:如果学过逆向或者看过《Linux二进制》这本书的同学应该知道。系统调用的方式,汇编来实现的话,可以看看我的这篇文章:GUN C内联汇编,就是首先把系统调用需要传递的参数和调用的函数对应的系统调用编号写入对应的寄存器中。然后通过特定的指令发起调用请求。这时,你的应用程序会从用户态转入内核态,因为有些特权指令只有内核态可以调用,特殊内存区域只有内核态可以访问。比如负责线程调度的内存区域。从用户态转入内核态执行,就需要将你的用户态的寄存器信息保存到内存中,然后内核态运行完了之后,将保存的信息恢复到寄存器中,进入用户态继续执行。可想而知,内存和寄存器直接数据的换入换出,是非常消耗资源,对性能影响非常大的。

  3. CAS操作是什么?
    答:CAS全称为:Compare and Swap 比较和替换。这个操作有一个特点就是原子性。这个操作如果用JAVA代码表示如下。至于具体怎么实现原子性,下面这个代码的来源处的文章也有说明。

/**
* 假设这段代码是原子性的,那么CAS其实就是这样一个过程
* 版权声明:该段代码为为CSDN博主「CringKong」的原创文章,遵循 CC 4.0 BY-SA 版* 权协议,转载请附上原文出处链接及本声明。
* 原文链接:https://blog.csdn.net/cringkong/article/details/80533917
*/
public boolean compareAndSwap(int v,int a,int b) {
	if (v == a) {
		v = b;
		return true;
	}else {
		return false;
	}
}

参考:

  • Java synchronized 详解 : http://www.sohu.com/a/273749069_505779
  • Java并发–Java中的CAS操作和实现原理 : https://blog.csdn.net/cringkong/article/details/80533917

你可能感兴趣的:(Java,jvm,synchronized,java)