对于久经沙场的Java老手来说,锁的概念一定如雷贯耳。毕竟,在打赢并发和多线程的战役中,锁可谓举足轻重。伴随技术领域的推进,系统架构逐渐演变为微服务,锁的范围超脱单个JVM的管理,分布式锁开始替代传统JVM锁,崭露头角。凭借自身的灵活和高逼格的技术栈,收到大家的一致追捧。但由浅入深,JVM锁不意味着过时与淘汰,相反,它在众多业务中还是那么不可替代。JVM锁的家族中,Synchronized一骑红尘,广为流传。本文,帝都的雁就来为读者盘点Synchronized的那些花花肠子。(PS:需要有Java对象布局的技术基础)
一、基本概念
Synchronized,即同步的概念,属Java关键字。C++语言编写,方便Java自动调用,常年游际于线程安全问题,自动管理线程访问对象锁的上锁和释放。
二、Synchronized的上锁方式
Synchronized上锁是建立在任意Java对象上的,也就是说,使用者必须明确通知Synchronized,到底要锁什么对象。特别注意:保证线程安全的前提,锁的对象必须是同一个!
1.this锁
这种锁的对象属于隐式,即调用者本身。如下两种:
public synchronized void a(){}
public void b(){
synchronized (this){}
}
2.object锁
将锁定义一个object类型对象,多个线程共享这个对象。
private Object lock = new Object();
public void a(){synchronized (lock){}}
3.类的字节码锁
定义静态的成员变量或者直接使用当前类字节码当做锁。
public class Test02 {
private static Object lock = new Object();
public void a(){synchronized (lock){}}
public void b(){synchronized (Test02.class){}}
}
由上可知,其实多线程上锁,不关注锁的对象是谁,而是关注多个线程是不是抢一把锁。相信大家在网上一定看到过这样的对比,如果把上锁这件事情比喻为开车,那么Synchronized就是自动挡,Lock是手动挡。可以这么理解,但Synchronized自动上锁和撤销的原理可不是那么简单的。
三、Monitor(检察者)
C++为Synchronized提供了一个Monitor对象,Monitor维护Synchronized对Java对象上锁的开销。
Monitor有几个比较好的功能:
1.锁重入次数
了解重入前,先上一段代码。
public synchronized void a(){
b();
}
public synchronized void b(){}
当前线程获取a方法上的对象锁后,再去访问b方法,不会继续开一个对象锁,而通过锁的重入性,进行传递。
如果使用汇编指令查看的话,会发现,在进入a方法时触发MonitorEnter指令,Monitor开始检察锁,Monitor的重入次数会+1;进入b方法,也会触发MonitorEnter指令,使得Monitor的重入次数会继续+1;退出b方法时,触发MonitorExit指令,Monitor的重入次数-1;退出a方法时,触发MonitorExit指令,Monitor的重入次数-1,此时重入数为0,Monitor撤销锁。
2.锁的持有线程
Owner,Monitor就将当前获取Java对象锁的线程ID记录在自身的属性中,用于做后续的判断。
3.等待池
WaitSet,即被当前锁对象使用wait()方法阻塞,释放CPU执行权的线程,会全部放入等待池,等待唤醒。
4.锁池
没有抢到对象锁的线程,会被让如锁池,等对象锁的释放后,锁池中的线程会重新进行锁的竞争。
5.锁竞争
Cxq,也就是多个线程抢夺Java对象锁的过程。
四、锁的升级
JDK6之后,Synchronized做了很大的优化。试想一些场景:如果某个上了锁的业务在一段时间内只被一个线程频繁访问,那么有必要加锁吗?再者,如果两个线程先后访问上锁业务,前者执行时间特别快,后者可能只需要等候忽略不计的时间,那么这时候通过锁的方式让后者进入阻塞,然后再唤醒,是否多余? 结合这些场景,Synchronized提出了锁的一些概念。
1.偏向锁
偏向,即对某个线程有好感,先到先得。
当锁被一个线程获取后,Java锁对象的对象头中一个名为Mark World的空间会记录下来此线程的ID,并且将自身偏向锁的状态由0变为1,并使用Monitor对业务加锁。
该线程再次来访问时,发现对象锁的偏向锁状态为1,就会去比较线程ID是否是自己,如果是自己则没有Monitor什么事,不做拦截,直接访问。
那如果不是呢?轻量锁开始登场。
2.轻量锁
本质上不会使当前线程进入阻塞。
当发现锁中偏向线程ID不是自己时,当前线程会把锁对象的Mark World拷贝至自身独享的栈帧空间中,然后通过CAS(compareAndSwap)尝试修改对象锁中Mark World的引用。修改成功,表明前面的线程执行结束,释放了锁,那么当前线程执行业务,并调整偏向锁的信息。
3.重量锁
轻量锁首次自旋失败后,不会马上让当前线程进入阻塞状态,而且继续自旋。当超过配置的自旋次数后还不超过,就会停止这种消耗CPU的行为,将其阻塞等待,锁也由轻量级变为重量级。
欢迎大家和帝都的雁积极互动,头脑交流会比个人埋头苦学更有效!共勉!
公众号:帝都的雁