synchronized
的作用和实现原理是通过 monitor
对象的获取和释放。这里来讲讲我对synchronized的优化的理解,那要理解优化,首先得知道问题在哪,那么先了解monitor对象是如何实现同步的呢。
monitor对象实际上很复杂。存储了,所有在等待获取该monitor的线程对象,等等。这里说明时需要用到线程的两种状态,也就是运行态和阻塞态。运行态就是线程当前在运行,阻塞态也就是当前线程处于sleep(休眠)状态,一般是在等待需要的资源可用,那么java这里的同步也是同样的。如下面的代码
private static Pen instance;
public static synchronized void draw() {...};
假设现在有一支笔(也就是单例模式),所有的实例对象共用一支笔去画画(调用draw方法)。
draw()
方法,获得了这个类的锁,也就是该类对应的Monitor对象。draw()
方法,但是获取不到笔(因为A在用)。这里的(2)中,获取不到笔(也就是获取不到类锁)要干嘛呢。总不能一直尝试去获取类锁吧(一直尝试会消耗CPU周期,浪费运算资源,学过单片机的应该能很容易理解,中断方式和while方式实现的sleep有什么区别)。所以需要进入阻塞态
等到A用完了,来通知他恢复到运行态
。
也就是说B获取不到B后,要进入阻塞态。线程转入阻塞态需要调用系统函数SuspendThread()
(假设是windows系统)。A执行完之后,释放该类对应的Monitor的使用权。然后A就调用ResumeThread()
唤醒线程B。B被唤醒后,获取到了A释放了的Monitor对象,继续运行,直到完成。
从这里可以知道,monitor对象实际上很复杂,因为复杂,就算只有一个线程重复的执行draw,获取锁也是会特别消耗性能的。而且线程的状态的变化是要通过系统函数的调用来操控的。实际上系统调用是非常消耗资源的,对性能影响非常大。[至于为什么,后面的补充部分的第二个部分
进行了说明]。如果A能快速的用完笔呢,但是,B又一下都没等,就直接进入阻塞态了。如果B稍微等等,那么就不需要进入阻塞态,也就不需要进行系统调用,减少了性能损耗,提高了执行效率。所以JVM针对这样的情况进行了优化,除了我们刚刚说的Monitor这种锁(重量级锁)加入了两种新的锁:偏向锁
和轻量级锁
,还加入了一些其他的优化:自旋
、锁削除
、锁粗化
。
因为monitor对象的复杂,就算只有一个线程重复的执行draw,获取锁也是会特别消耗性能的。偏向锁
就是用来优化这个情况的。
CAS操作
把自己的线程ID
写入到该类头的一个字段
中。获得了该锁的使用权
。CAS操作
把当时写入的自己的线程ID
清空,释放了该锁的使用权
。CAS操作
是什么,看下面补充的第三部分]这样的处理,就能优化A能很快的处理完的情况,而不用进行系统调用。
2.如果有B也要获取偏向锁呢,那就是这样的情况了。
CAS操作
把自己的线程ID
写入到该类头的一个字段
中。获得了该锁的使用权
。CAS操作
试着写入自己的线程ID,但是发现那个地方不是空的而是A的线程ID。轻量级锁
。所以这三个锁,是会进行升级,转化的,会按照下面的顺序升级。
偏向锁
—>轻量级锁
—>重量级锁
。
经过了上面的升级后,该类的锁已经由偏向锁
变成了轻量级锁
。
轻量级锁
自旋状态
也就是会进行多次CAS操作尝试。下面就会有两种情况。锁的标志位
改成重量级锁,并把线程B的状态信息写入到Monitor的阻塞队列中,线程B进入到阻塞状态,以及A的信息也写入monitor,然后恢复A的执行。从上面的情况可以看到,升级的过程,总是要暂停那个正在执行的线程,然后进行锁升级,升级完成后,恢复执行。那么如果锁等级可逆。那么可能会存在锁升级过多次出现的情况,(可能对大部分情况来说)对性能消耗大。所以JVM实现的这个锁升级是不可逆的。也就是,例如不可以从重量级锁,回到之前的锁。
这个其实很好理解,也就是通过分析,发现并不需要加锁,也能保证执行正确。也就是只可能会有一个线程的情况。那么这个锁,完全可以去除。
这个也很好理解,比如下面的代码
{
draw();
draw();
draw();
}
那么这里的三个过程都要获取锁,如果分开,获取。那么可能会使得锁的获取和释放次数增加,为了减少次数,提高性能。就会把三个锁合成一把锁。那么这个时候,就只有一次的获取和释放,性能会有提升。当然,如果每个draw()处理时间很久,这时可能就不太好了。
这个存储着线程对象的列表是用来干嘛的呢?
答:在A释放了类对应的Monitor的使用权后,那这个时候B还在睡觉呢,得叫醒B进入运行态。那A怎么知道有B在等呢。所以这就是为什么Monitor对象中存储了所有在等待获取该monitor的线程对象
。这样A就知道要叫醒谁了。
为什么系统调用非常消耗资源,对性能影响非常大?
答:如果学过逆向或者看过《Linux二进制》这本书的同学应该知道。系统调用的方式,汇编来实现的话,可以看看我的这篇文章:GUN C内联汇编,就是首先把系统调用需要传递的参数和调用的函数对应的系统调用编号写入对应的寄存器中。然后通过特定的指令发起调用请求。这时,你的应用程序会从用户态
转入内核态
,因为有些特权指令只有内核态可以调用,特殊内存区域只有内核态可以访问。比如负责线程调度的内存区域。从用户态转入内核态执行,就需要将你的用户态的寄存器信息保存到内存中,然后内核态
运行完了之后,将保存的信息恢复到寄存器中,进入用户态
继续执行。可想而知,内存和寄存器直接数据的换入换出,是非常消耗资源,对性能影响非常大的。
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;
}
}
参考: