阿里面试官让我分析Synchronized原理 我直接让他哑口无言
文章简介
synchronized想必大家都不陌生,用来解决执行绪安全问题的利器。同时也是Java高阶程序员面试比较常见的面试题。这篇文正会带大家彻底了解synchronized的实现。内容导航
什么时候需要用Synchronizedsynchronized的使用synchronized的实现原理分析什么时候需要用Synchronized
想必大家对synchronized都不陌生,主要作用是在多个执行绪操作共享资料的时候,保证对共享资料访问的执行绪安全性。比如在下面这个图片中,两个执行绪对于i这个共享变数同时做i++递增操作,那么这个时候对于i这个值来说就存在一个不确定性,也就是说理论上i的值应该是2,但是也可能是1。而导致这个问题的原因是执行绪并行执行i++操作并不是原子的,存线上程安全问题。所以通常来说解决办法是通过加锁来实现执行绪的序列执行,而synchronized就是java中锁的实现的关键字。

synchronized在并发程式设计中是一个非常重要的角色,在JDK1.6之前,它是一个重量级锁的角色,但是在JDK1.6之后对synchronized做了优化,优化以后效能有了较大的提升(这块会在后面做详细的分析)。
先来看一下synchronized的使用
Synchronized的使用
synchronized有三种使用方法,这三种使用方法分别对应三种不同的作用域,程式码如下修饰普通同步方法
将synchronized修饰在普通同步方法,那么该锁的作用域是在当前例项物件范围内,也就是说对于 SyncDemosd=newSyncDemo();这一个例项物件sd来说,多个执行绪访问access方法会有锁的限制。如果access已经有执行绪持有了锁,那这个执行绪会独占锁,直到锁释放完毕之前,其他执行绪都会被阻塞
public SyncDemo{
Object lock =new Object();
//形式1
public synchronized void access(){
//
}
//形式2,作用域等同于形式1
public void access1(){
synchronized(lock){
//
}
}
//形式3,作用域等同于前面两种
public void access2(){
synchronized(this){
//
}
}
}
修饰静态同步方法
修饰静态同步方法或者静态物件、类,那么这个锁的作用范围是类级别。举个简单的例子,
SyncDemo sd=SyncDemo();
SyncDemo sd2=new SyncDemo();}
两个不同的例项sd和sd2, 如果sd这个例项访问access方法并且成功持有了锁,那么sd2这个物件如果同样来访问access方法,那么它必须要等待sd这个物件的锁释放以后,sd2这个物件的执行绪才能访问该方法,这就是类锁;也就是说类锁就相当于全域性锁的概念,作用范围是类级别。
这里抛一个小问题,大家看看能不能回答,如果不能也没关系,后面会讲解;问题是如果sd先访问access获得了锁,sd2物件的执行绪再访问access1方法,那么它会被阻塞吗?
public SyncDemo{
static Object lock=new Object();
//形式1
public synchronized static void access(){
//
}
//形式2等同于形式1
public void access1(){
synchronized(lock){
//
}
}
//形式3等同于前面两种
public void access2(){
synchronzied(SyncDemo.class){
//
}
}
}
步方法块
public SyncDemo{
Object lock=new Object();
public void access(){
//do something
synchronized(lock){
//
}
}
}
通过演示3种不同锁的使用,让大家对synchronized有了初步的认识。当一个执行绪检视访问带有synchronized修饰的同步程式码块或者方法时,必须要先获得锁。当方法执行完毕退出以后或者出现异常的情况下会自动释放锁。如果大家认真看了上面的三个案例,那么应该知道锁的范围控制是由物件的作用域决定的。物件的作用域越大,那么锁的范围也就越大,因此我们可以得出一个初步的猜想,synchronized和物件有非常大的关系。那么,接下来就去剖析一下锁的原理
Synchronized的实现原理分析
当一个执行绪尝试访问synchronized修饰的程式码块时,它首先要获得锁,那么这个锁到底存在哪里呢?物件在内存中的布局
synchronized实现的锁是储存在Java物件头里,什么是物件头呢?在Hotspot虚拟机器中,物件在内存中的储存布局,可以分为三个区域:物件头(Header)、例项资料(Instance Data)、对齐填充(Padding)

当我们在Java程式码中,使用new建立一个物件例项的时候,(hotspot虚拟机器)JVM层面实际上会建立一个 instanceOopDesc物件。
Hotspot虚拟机器采用OOP-Klass模型来描述Java物件例项,OOP(Ordinary Object Point)指的是普通物件指标,Klass用来描述物件例项的具体型别。Hotspot采用instanceOopDesc和arrayOopDesc来描述物件头,arrayOopDesc物件用来描述阵列型别
instanceOopDesc的定义在Hotspot源代码中的 instanceOop.hpp档案中,另外,arrayOopDesc的定义对应 arrayOop.hpp
class instanceOopDesc : public oopDesc {
public:
// aligned header size.
static int header_size() { return sizeof(instanceOopDesc)/HeapWordSize; }
// If compressed, the offset of the fields of the instance may not be aligned.
static int base_offset_in_bytes() {
// offset computation code breaks if UseCompressedClassPointers
// only is true
return (UseCompressedOops && UseCompressedClassPointers) ?
klass_gap_offset_in_bytes() :
sizeof(instanceOopDesc);
}
static bool contains_field_offset(int offset, int nonstatic_field_size) {
int base_in_bytes = base_offset_in_bytes();
return (offset >= base_in_bytes &&
(offset-base_in_bytes) }
};
#endif // SHARE_VM_OOPS_INSTANCEOOP_HPP
从instanceOopDesc程式码中可以看到 instanceOopDesc继承自oopDesc,oopDesc的定义载Hotspot源代码中的 oop.hpp档案中
class oopDesc {
friend class VMStructs;
private:
volatile markOop _mark;
union _metadata {
Klass* _klass;
narrowKlass _compressed_klass;
} _metadata;
// Fast access to barrier set. Must be initialized.
static BarrierSet* _bs;
...
}
在普通例项物件中,oopDesc的定义包含两个成员,分别是 _mark和 _metadata
_mark表示物件标记、属于markOop型别,也就是接下来要讲解的Mark World,它记录了物件和锁有关的资讯
_metadata表示类元资讯,类元资讯储存的是物件指向它的类元资料(Klass)的首地址,其中Klass表示普通指标、 _compressed_klass表示压缩类指标
Mark Word
在前面我们提到过,普通物件的物件头由两部分组成,分别是markOop以及类元资讯,markOop官方称为Mark Word
在Hotspot中,markOop的定义在 markOop.hpp档案中,程式码如下
class markOopDesc: public oopDesc {
private:
// Conversion
uintptr_t value() const { return (uintptr_t) this; }
public:
// Constants
enum { age_bits = 4, //分代年龄
lock_bits = 2, //锁标识
biased_lock_bits = 1, //是否为偏向锁
max_hash_bits = BitsPerWord - age_bits - lock_bits - biased_lock_bits,
hash_bits = max_hash_bits > 31 ? 31 : max_hash_bits, //物件的hashcode
cms_bits = LP64_ONLY(1) NOT_LP64(0),
epoch_bits = 2 //偏向锁的时间戳
};
...
Mark word记录了物件和锁有关的资讯,当某个物件被synchronized关键字当成同步锁时,那么围绕这个锁的一系列操作都和Mark word有关系。Mark Word在32位虚拟机器的长度是32bit、在64位虚拟机器的长度是64bit。
Mark Word里面储存的资料会随着锁标志位的变化而变化,Mark Word可能变化为储存以下5中情况
32位虚拟机器中的定义
64位虚拟机器中的定义
锁标志位的表示意义
锁标识 lock=00 表示轻量级锁锁标识 lock=10 表示重量级锁偏向锁标识 biased_lock=1表示偏向锁偏向锁标识 biased_lock=0且锁标识=01表示无锁状态到目前为止,我们再总结一下前面的内容,synchronized(lock)中的lock可以用Java中任何一个物件来表示,而锁标识的储存实际上就是在lock这个物件中的物件头内。大家懂了吗?
其实前面只提到了锁标志位的储存,但是为什么任意一个Java物件都能成为锁物件呢?
首先,Java中的每个物件都派生自Object类,而每个Java Object在JVM内部都有一个native的C++物件 oop/oopDesc进行对应。
其次,执行绪在获取锁的时候,实际上就是获得一个监视器物件(monitor) ,monitor可以认为是一个同步物件,所有的Java物件是天生携带monitor.
在hotspot源代码的 markOop.hpp档案中,可以看到下面这段程式码。
ObjectMonitor* monitor() const {
assert(has_monitor(), "check");
// Use xor instead of &~ to provide one extra tag-bit check.
return (ObjectMonitor*) (value() ^ monitor_value);
}
多个执行绪访问同步程式码块时,相当于去争抢物件监视器修改物件中的锁标识,上面的程式码中ObjectMonitor这个物件和执行绪争抢锁的逻辑有密切的关系(后续会详细分析)
锁的升级
前面提到了锁的几个概念,偏向锁、轻量级锁、重量级锁。在JDK1.6之前,synchronized是一个重量级锁,效能比较差。从JDK1.6开始,为了减少获得锁和释放锁带来的效能消耗,synchronized进行了优化,引入了 偏向锁和 轻量级锁的概念。所以从JDK1.6开始,锁一共会有四种状态,锁的状态根据竞争激烈程度从低到高分别是:无锁状态->偏向锁状态->轻量级锁状态->重量级锁状态。这几个状态会随着锁竞争的情况逐步升级。为了提高获得锁和释放锁的效率,锁可以升级但是不能降级。
下面就详细讲解synchronized的三种锁的状态及升级原理
偏向锁
在大多数的情况下,锁不仅不存在多执行绪的竞争,而且总是由同一个执行绪获得。因此为了让执行绪获得锁的代价更低引入了偏向锁的概念。偏向锁的意思是如果一个执行绪获得了一个偏向锁,如果在接下来的一段时间中没有其他执行绪来竞争锁,那么持有偏向锁的执行绪再次进入或者退出同一个同步程式码块,不需要再次进行抢占锁和释放锁的操作。偏向锁可以通过 -XX:+UseBiasedLocking开启或者关闭
偏向锁的获取
偏向锁的获取过程非常简单,当一个执行绪访问同步块获取锁时,会在物件头和栈帧中的锁记录里储存偏向锁的执行绪ID,表示哪个执行绪获得了偏向锁,结合前面分析的Mark Word来分析一下偏向锁的获取逻辑
首先获取目标物件的Mark Word,根据锁的标识为和epoch去判断当前是否处于可偏向的状态如果为可偏向状态,则通过CAS操作将自己的执行绪ID写入到MarkWord,如果CAS操作成功,则表示当前执行绪成功获取到偏向锁,继续执行同步程式码块如果是已偏向状态,先检测MarkWord中储存的threadID和当前访问的执行绪的threadID是否相等,如果相等,表示当前执行绪已经获得了偏向锁,则不需要再获得锁直接执行同步程式码;如果不相等,则证明当前锁偏向于其他执行绪,需要撤销偏向锁。CAS:表示自旋锁,由于执行绪的阻塞和唤醒需要CPU从使用者态转为核心态,频繁的阻塞和唤醒对CPU来说效能开销很大。同时,很多物件锁的锁定状态指会持续很短的时间,因此引入了自旋锁,所谓自旋就是一个无意义的死循环,在循环体内不断的重行竞争锁。当然,自旋的次数会有限制,超出指定的限制会升级到阻塞锁。
偏向锁的撤销
当其他执行绪尝试竞争偏向锁时,持有偏向锁的执行绪才会释放偏向锁,撤销偏向锁的过程需要等待一个全域性安全点(所有工作执行绪都停止字节码的执行)。
首先,暂停拥有偏向锁的执行绪,然后检查偏向锁的执行绪是否为存活状态如果执行绪已经死了,直接把物件头设定为无锁状态如果还活着,当达到全域性安全点时获得偏向锁的执行绪会被挂起,接着偏向锁升级为轻量级锁,然后唤醒被阻塞在全域性安全点的执行绪继续往下执行同步程式码偏向锁的获取流程图
偏向锁的获取流程图
轻量级锁
前面我们知道,当存在超过一个执行绪在竞争同一个同步程式码块时,会发生偏向锁的撤销。偏向锁撤销以后物件会可能会处于两种状态
一种是不可偏向的无锁状态,简单来说就是已经获得偏向锁的执行绪已经退出了同步程式码块,那么这个时候会撤销偏向锁,并升级为轻量级锁一种是不可偏向的已锁状态,简单来说就是已经获得偏向锁的执行绪正在执行同步程式码块,那么这个时候会升级到轻量级锁并且被原持有锁的执行绪获得锁那么升级到轻量级锁以后的加锁过程和解锁过程是怎么样的呢?
轻量级锁加锁
JVM会先在当前执行绪的栈帧中建立用于储存锁记录的空间(LockRecord)将物件头中的Mark Word复制到锁记录中,称为Displaced Mark Word.执行绪尝试使用CAS将物件头中的Mark Word替换为指向锁记录的指标如果替换成功,表示当前执行绪获得轻量级锁,如果失败,表示存在其他执行绪竞争锁,那么当前执行绪会尝试使用CAS来获取锁,当自旋超过指定次数(可以自定义)时仍然无法获得锁,此时锁会膨胀升级为重量级锁
轻量级锁加锁
轻量锁解锁
尝试CAS操作将所记录中的Mark Word替换回到物件头中如果成功,表示没有竞争发生如果失败,表示当前锁存在竞争,锁会膨胀成重量级锁一旦锁升级成重量级锁,就不会再恢复到轻量级锁状态。当锁处于重量级锁状态,其他执行绪尝试获取锁时,都会被阻塞,也就是 BLOCKED状态。当持有锁的执行绪释放锁之后会唤醒这些现场,被唤醒之后的执行绪会进行新一轮的竞争
轻量级锁解锁
重量级锁
重量级锁依赖物件内部的monitor锁来实现,而monitor又依赖操作系统的MutexLock(互斥锁)
大家如果对MutexLock有兴趣,可以抽时间去了解,假设Mutex变数的值为1,表示互斥锁空闲,这个时候某个执行绪呼叫lock可以获得锁,而Mutex的值为0表示互斥锁已经被其他执行绪获得,其他执行绪呼叫lock只能挂起等待
为什么重量级锁的开销比较大呢?
原因是当系统检查到是重量级锁之后,会把等待想要获取锁的执行绪阻塞,被阻塞的执行绪不会消耗CPU,但是阻塞或者唤醒一个执行绪,都需要通过操作系统来实现,也就是相当于从使用者态转化到核心态,而转化状态是需要消耗时间的
粉丝福利
离金9银十还有差不多两个月,在这里小编整理了一些互联网大厂的面试题,分享给大家

关注作者+转发 转发 转发 重要的事说三遍
后台回复(面试)关键词即可免费领取