synchronized想必你们都不陌生,用来解决线程安全问题的利器。同时也是Java高级程序员面试比较常见的面试题。这篇文正会带你们完全了解synchronized的实现。java
想必你们对synchronized都不陌生,主要做用是在多个线程操做共享数据的时候,保证对共享数据访问的线程安全性。c++
好比在下面这个图片中,两个线程对于i这个共享变量同时作i++递增操做,那么这个时候对于i这个值来讲就存在一个不肯定性,也就是说理论上i的值应该是2,可是也多是1。而致使这个问题的缘由是线程并行执行i++操做并非原子的,存在线程安全问题。因此一般来讲解决办法是经过加锁来实现线程的串行执行,而synchronized就是java中锁的实现的关键字。程序员
synchronized在并发编程中是一个很是重要的角色,在JDK1.6以前,它是一个重量级锁的角色,可是在JDK1.6以后对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实现的锁是存储在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) < nonstatic_field_size * heapOopSize); } }; #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表示压缩类指针
在前面咱们提到过,普通对象的对象头由两部分组成,分别是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中状况
锁标志位的表示意义
到目前为止,咱们再总结一下前面的内容,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来分析一下偏向锁的获取逻辑
CAS:表示自旋锁,因为线程的阻塞和唤醒须要CPU从用户态转为核心态,频繁的阻塞和唤醒对CPU来讲性能开销很大。同时,不少对象锁的锁定状态指会持续很短的时间,所以引入了自旋锁,所谓自旋就是一个无心义的死循环,在循环体内不断的重行竞争锁。固然,自旋的次数会有限制,超出指定的限制会升级到阻塞锁。
当其余线程尝试竞争偏向锁时,持有偏向锁的线程才会释放偏向锁,撤销偏向锁的过程须要等待一个全局安全点(全部工做线程都中止字节码的执行)。
前面咱们知道,当存在超过一个线程在竞争同一个同步代码块时,会发生偏向锁的撤销。偏向锁撤销之后对象会可能会处于两种状态
那么升级到轻量级锁之后的加锁过程和解锁过程是怎么样的呢?
一旦锁升级成重量级锁,就不会再恢复到轻量级锁状态。当锁处于重量级锁状态,其余线程尝试获取锁时,都会被阻塞,也就是 BLOCKED状态。当持有锁的线程释放锁以后会唤醒这些现场,被唤醒以后的线程会进行新一轮的竞争
![]()
重量级锁依赖对象内部的monitor锁来实现,而monitor又依赖操做系统的MutexLock(互斥锁)
你们若是对MutexLock有兴趣,能够抽时间去了解,假设Mutex变量的值为1,表示互斥锁空闲,这个时候某个线程调用lock能够得到锁,而Mutex的值为0表示互斥锁已经被其余线程得到,其余线程调用lock只能挂起等待
为何重量级锁的开销比较大呢?
缘由是当系统检查到是重量级锁以后,会把等待想要获取锁的线程阻塞,被阻塞的线程不会消耗CPU,可是阻塞或者唤醒一个线程,都须要经过操做系统来实现,也就是至关于从用户态转化到内核态,而转化状态是须要消耗时间的
到目前为止,咱们分析了synchronized的使用方法、以及锁的存储、对象头、锁升级的原理。若是有问题,能够扫描二维码留言