这是Java并发编程学习的第一篇,最先在2013年时便励志要把JAVA的并发编程好好学习一下,那个时候才工做一年。后来因为各类各样的缘由,未能学习起来,5年时间过去,技术止步不前,学到的都是业务领域知识,站在我我的发展角度,我但愿在技术,主要是JAVA后端技术领域再往前走一步,因此在这里记录下我学习的点点滴滴,同时将代码记录在Github上。并发编程的文章主要是记录个人学习过程,应该会有不少错误的地方,也会有不少没有深度的内容,欢迎你们纠正。html
一、为何会用到synchronizedjava
Java语言的一个高级特性就是支持多线程,线程在操做系统的实现上,能够当作是轻量级的进程,同一进程中的线程都将共享进程的内存空间,因此Java的多线程在共享JVM的内存空间。JVM的内存空间主要分为:程序计数器、虚拟机栈、本地方法栈、堆、方法区和运行时常量池。git
在这些内存空间中,咱们重点关注栈和堆,这里的栈包括了虚拟机栈和本地方法栈(实际上不少JVM的实现就是二者合二为一)。在JVM中,每一个线程有本身的栈内存,其余线程没法访问该栈的内存数据,栈中的数据也仅仅限于基本类型和对象引用。在JVM中,全部的线程共享堆内存,而堆上则不保存基本类型和对象引用,只包含对象。除了重点关注的栈和堆,还有一部分数据存放在方法区,好比类的静态变量,方法区和栈相似,只能存放基本类型和对象引用,不一样的是方法区是全部线程共享的。github
如上所述,JVM的堆(对象信息)和方法区(静态变量)是全部线程共享的,那么多线程若是访问同一个对象或者静态变量时,就须要进行管控,不然可能出现没法预测的结果。为了协调多线程对数据的共享访问,JVM给每一个对象和类都分配了一个锁,同一时刻,只有一个线程能够拥有这个对象或者类的锁。JVM中锁是经过监视器(Monitors)来实现的,监视器的主要功能就是监视一段代码,确保在同一时间只有一个线程在执行。每一个监视器都和一个对象关联,当线程执行到监视器的监视代码第一条指令时,线程获取到该对象的锁定,直到代码执行完成,执行完成后,线程释放该对象的锁。编程
synchronized就是Java语言中一种内置的Monitor实现,咱们在多线程的实现上就会用到synchronized来对类和对象进行行为的管控。后端
二、synchronized用法及背后原理数组
主要提供了2种方式来协调多线程的同步访问数据:同步方法和同步代码块。代码以下:安全
public class SynchronizedPrincipleTest { public synchronized void f1() { System.out.println("synchronized void f1()"); } public void f2() { synchronized(this) { System.out.println("synchronized(this)"); } } public static void main(String[] args) { SynchronizedPrincipleTest test = new SynchronizedPrincipleTest(); test.f1(); test.f2(); } }
f1就是同步方法,f2就是同步代码块。这两种实如今背后有什么差别呢?咱们能够先javac编译,而后再经过javap反编译来看下。网络
图一数据结构
从图一能够看出同步方法JVM是经过ACC_SYNCHRONIZED来实现的,同步代码块JVM是经过monitorenter、monitorexit来实现的。在JVM规范中,同步方法经过ACC_SYNCHRONIZED标记对方法隐式加锁,同步代码块则显示的经过monitorenter和monitorexit进行加锁,当线程执行到monitorenter时,先得到锁,而后执行方法,执行到monitorexit再释放锁。
3、JVM Monitor背后实现
查阅网上各类资料及翻阅openJDK代码。
synchronized uses a locking mechanism that is built into the JVM and MONITORENTER / MONITOREXIT bytecode instructions. So the underlying implementation is JVM-specific (that is why it is called intrinsic lock) and AFAIK usually (subject to change) uses a pretty conservative strategy: once lock is "inflated" after threads collision on lock acquiring, synchronized begin to use OS-based locking ("fat locking") instead of fast CAS ("thin locking") and do not "like" to use CAS again soon (even if contention is gone). ………… PS: you're pretty curious and I highly recommend you to look at HotSpot sources to go deeper (and to find out exact implementations for specific platform version). It may really help. Starting point is somewhere here: http://hg.openjdk.java.net/jdk8/jdk8/hotspot/file/87ee5ee27509/src/share/vm/runtime/synchronizer.cpp
上述表达的大体意思是同步在字节码层面就是经过monitorenter和monitorexit来实现的,能够理解为这种实现是JVM规范,一旦线程在锁获取时出现冲突,锁就会膨胀,这种膨胀是基于系统的实现(胖锁)来替代CAS实现(瘦锁)。最后给出JVM底层C++代码的连接。
查看该类代码,有以下注释:
// This is full version of monitor enter and exit. I choose not // to use enter() and exit() in order to make sure user be ware // of the performance and semantics difference. They are normally // used by ObjectLocker etc. The interpreter and compiler use // assembly copies of these routines. Please keep them synchornized.
知道这个类是全部monitor enter and exit的实现,其中方法jni_enter和jni_exit就是heavy weight monitor的实现。再看这个jni_enter方法的实现,调用了inflate方法,返回ObjectMonitor指针。
再看类ObjectMonitor,给出了具体enter和exit方法的实现。
// The ObjectMonitor class is used to implement JavaMonitors which have // transformed from the lightweight structure of the thread stack to a // heavy weight lock due to contention
与wiki.openjdk描述一致:
Synchronization affects multiple parts of the JVM: The structure of the object header is defined in the classes oopDesc and markOopDesc, the code for thin locks is integrated in the interpreter and compilers, and the class ObjectMonitor represents inflated locks.
4、锁优化
从上述中的注释咱们能够看出synchronized是一种heavy weight lock,Brian Goetz在IBM developerworks的论文《Java theory and practice:More flexible, scalable locking in JDK 5.0》也比较了synchronized和ReentrantLock二者的性能,因此在JDK1.6以后对锁作了不少优化,主要有:自旋锁和自适应自旋、锁消除、锁粗化、轻量级锁和偏向锁。
4.1、自旋锁和自适应自旋
线程执行到synchronized同步方法或者代码块时,若是另外的线程已经获取到该对象的锁,该线程就只能等待,被操做系统挂起,直到另外的线程处理完成,该线程才能恢复执行。线程的挂起和恢复都须要从应用的用户态切换到操做系统的内核态才能完成,这种操做给系统也带来了性能上很大的影响。同时虚拟机的研发团队注意到在不少应用上,共享数据上的锁只会持续很短的时间,为了这点时间去挂起和恢复线程是不值得的。若是物理机上有多个CPU,那么就能够同时让多个线程并行执行,就能够在JVM的层面上让请求锁的线程“稍等一下”,但不放弃CPU的执行时间,看下持有锁的线程是否会很快就释放锁。为了让线程等待,就让线程去执行一个忙循环(自旋),这种技术就是所谓的自旋锁。举个例子,我以为有点像,工做中我正在回复邮件,这个动做其实很快就能作完,这个时候另一我的给我打电话,我接通了,可是我告诉他等我一下,我回复完这封邮件,我们再交流。这个过程,回复邮件占用了我这个资源,另一我的要和我通话,若是彻底阻塞,我就不接电话直接完成回复邮件再接通电话,可是其实回复邮件只要一下子时间,因此我接通了电话,而后对方一直在线上等占用着个人时间(他本身也一直在等我,暂时不作别的事情,忙循环),等我回复邮件完成,立马切换过来电话交流。在这个例子里面,其实咱们也能够看出若是对方一直等待,若是我邮件迟迟未回复完成,对方也是一直在耗着等待且不能作其余的工做,这也是性能的浪费,这个就是自旋锁的缺点,因此自旋不能没有限制,要能作到“智能”的判断,这个就是自适应自旋的概念,自适应自旋时间不固定,是由前一次锁的自旋时间和锁的拥有者的状态决定的,若是以前自旋成功获取过锁,则这次自旋等待的时间能够长一点,不然省略自旋,避免资源浪费。一样,拿这个例子来讲,若是这次对方在电话那头就等了我一小段时间,我就和对方沟通了,那么下次碰到一样的状况时,对方会继续在电话耐心的等待,不然对方就直接挂电话了(由于丧失了“信任”)。
4.2、锁消除
锁消除指的是JVM在JIT运行时,对一些代码要求同步而实际该段代码在数据共享上不可能出现竞争的锁而进行消除操做。好比代码(EascapeTest类)以下:
private static String concatString(String s1,String s2) { return s1 + s2; } public static void main(String[] args) { EascapeTest eascapeTest = new EascapeTest(); eascapeTest.concatString("a","b"); }
方法concatString,是咱们在实际开发中常常会用到的一个字符串拼接的实现,从源代码层面上看是没有任何同步操做的。但实际JVM在运行这个方法时会优化为StringBuilder的append()操做,这个咱们能够经过javap反编译来验证,见图二。
图二
JVM采用StringBuilder来实现,是经过同步方法来实现的,可是concatString方法中StringBuilder的对象的做用域是被限制在这个方法内部,只要作到EascapeTest被安全的发布,那么concatString方法中StringBuilder的全部对象都不会发生逸出,也就是线程安全的,对应的锁能够被消除,从而提高性能。
4.3、锁粗化
锁粗化是合并使用相同锁定对象的相邻同步块的过程。看以下代码:
public void addStooges(Vector v) { v.add("Moe"); v.add("Larry"); v.add("Curly"); }
addStooges方法中的一系列操做都是在不断的对同一个对象进行反复的加锁和解锁,即便没有线程竞争,如此频繁的同步操做也是很损耗性能的,JVM若是探测到这样的操做,就会对同步范围进行粗化,把锁放在第一个操做加上,而后在最后一个操做中释放,这样就只加了一次锁可是达到了一样的效果。
4.4、轻量级锁和偏向锁
学习轻量级锁和偏向锁以前,我们得先来学习下Java对象模型和对象头,有了这个基础才好来理解这两个锁。
4.4.1、Java对象模型及对象头
Java虚拟机有不少对应的实现版本,本小节的内容基于HotSpot虚拟机来学习下Java对象模型和对象头。HotSpot的底层是用C++实现的,这个能够经过下载OpenJDK源代码来看便可确认。众所周知,C++和Java都是面向对象的语言,那么Java的对象在虚拟机的表示,最简单的一种实现就是在C++层面上实现一个与之对应的类,然而HotSpot并无这么实现,而是专门设计一套OOP-Klass二分模型。
OOP:ordinary object pointer,普通对象指针,用来描述对象实例信息。
Klass:Java类的C++对等体,用来描述Java类。
之因此会这么设计,其中一个理由就是设计者不想让每个对象都有一个C++虚函数指针(取自klass.hpp注释)。
// One reason for the oop/klass dichotomy in the implementation is // that we don't want a C++ vtbl pointer in every object. ……….
对于OOP对象来讲,主要职能是表示对象的实例信息,不必持有任何虚函数;而在描述Java类的Klass对象中含有VTBL(继承自klass_vtbl),那么Klass就能够根据Java对象的实际类型进行C++的分发,这样OOP对象只须要经过相应的Klass即可以找到全部的虚函数,就避免了给每个对象都分配一个C++的虚函数指针。
Klass向JVM提供了2个功能:
实现语言层面的Java类;
实现Java对象的分发功能;
这2个功能在一个C++类中就能实现,前者在基类Klass中已经实现,然后者就由Klass的子类提供虚函数实现(取自klass.hpp注释)。
// A Klass provides: // 1: language level class object (method dictionary etc.) // 2: provide vm dispatch behavior for the object // Both functions are combined into one C++ class.
OOP框架和Klass框架的关系能够在oopsHierarchy.hpp文件中体现,JDK1.7和JDK1.8因为内存空间的变化,因此oopsHierarchy.hpp的实现也不同,这里以OpenJDK1.7来描述OOP-Klass。
typedef class oopDesc* oop;//oops基类 typedef class instanceOopDesc* instanceOop; //Java类实例 typedef class methodOopDesc* methodOop; //Java方法 typedef class constMethodOopDesc* constMethodOop; //Java方法不变信息 typedef class methodDataOopDesc* methodDataOop; //性能信息数据结构 typedef class arrayOopDesc* arrayOop; //数组oops基类 typedef class objArrayOopDesc* objArrayOop; //数组oops对象 typedef class typeArrayOopDesc* typeArrayOop; typedef class constantPoolOopDesc* constantPoolOop; typedef class constantPoolCacheOopDesc* constantPoolCacheOop; typedef class klassOopDesc* klassOop; //与Java类对等的C++类 typedef class markOopDesc* markOop; //Java对象头 typedef class compiledICHolderOopDesc* compiledICHolderOop;
在Java程序运行的过程当中,每建立一个Java对象,在JVM内部就会相应的建立一个OOP对象来表示该Java对象。OOP对象的基类就是oopDesc,它的代码实现以下:
volatile markOop _mark; union _metadata { wideKlassOop _klass; narrowOop _compressed_klass; } _metadata;
在虚拟机内部,经过instanceOopDesc来表示一个Java对象。对象在内部中的布局能够分为两个连续的部分:instanceOopDesc和实例数据。instanceOopDesc又被称为对象头,继承自oopDesc,看instanceOop.hpp的实现,未新增新的数据结构,和oopDesc同样,包含以下2部分信息:
_mark:markOop类型,存储对象运行时记录信息,主要有HashCode、分代年龄、锁状态标记、线程持有的锁、偏向线程ID等,占用内存和虚拟机位长一致,若是是32位虚拟机则为32位,在64位虚拟机则为64位;
_metadata:联合体,指向描述类型的Klass对象的指针,由于Klass对象包含了实例对象所属类型的元数据,故被称为元数据指针。虚拟机运行时将频繁使用这个指针定位到方法区的类信息。
到此基本描述了Java的对象头,可是这只是一部分,还有一部分是Klass,合起来才是完整的对象模型。那么Klass在对象模型中是如何体现的呢?实际上,HotSpot是这样处理的,经过为每个已加载的Java类建立一个instanceKlass对象,用来在JVM层表示Java类。来看看instanceKlass的数据结构。
// Method array.方法列表 objArrayOop _methods; // Int array containing the original order of method in the class file (for // JVMTI).方法顺序 typeArrayOop _method_ordering; // Interface (klassOops) this class declares locally to implement.实现接口 objArrayOop _local_interfaces; // Interface (klassOops) this class implements transitively.继承接口 objArrayOop _transitive_interfaces; ………… typeArrayOop _fields; // Constant pool for this class. constantPoolOop _constants; // Class loader used to load this class, NULL if VM loader used. oop _class_loader; // Protection domain. oop _protection_domain;
能够看到一个类该有的内容,instanceKlass基本都有了。
综上,Java对象在JVM中的表示是这样的,对象的实例(instanceOopDesc)存储在堆上,对象的元数据(instanceKlass)存储在方法区,对象的引用存储在栈上。以下图:
图三 取自参考资料
4.4.2、轻量级锁
轻量级锁并非用来替代重量级锁的,它的本意是在没有多线程竞争的前提下,减小重量级锁使用操做系统互斥量产生的性能消耗。上面咱们已经介绍了Java对象头(instanceOopDesc),数据结构以下:
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, cms_bits = LP64_ONLY(1) NOT_LP64(0), epoch_bits = 2 };
此结构和网络数据包的报文头结构很是像。
图四 取自参考资料
简单介绍完对象头的构造后,回到轻量级锁的的执行过程上。在代码进入同步块的时候,若是此时同步对象未被锁定(Unlocked,01),JVM会在当前线程的栈帧中创建一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝(Displace Mark Word)。而后,JVM将使用CAS操做尝试将该对象的Mark Word指向Lock Record的指针,若是这个操做成功,则线程就拥有了该对象的锁,而且对象的Mark Word状态变成(Light-weight locked,00),表示轻量级锁定。若是这个操做失败的话,JVM会先检查对象的Mark Word是否是已经指向当前栈帧,若是是则直接进入同步代码块执行,不然说明对象已经被其余线程抢占了。若是同时有两个线程以上争用同一个锁,那轻量级锁再也不有效,要膨胀为重量级锁,锁标记状态更新为“10”(Heavy-weight locked),后面等待锁的线程直接进入阻塞状态。
轻量级锁的解锁过程,也是同样的,经过CAS来实现,若是对象的Mark Word仍然指向线程的锁记录,那么就用CAS操做把对象当前的Mark Word和线程复制的Displace Mark Word替换回来。
4.4.3、偏向锁
若是说轻量级锁是在无竞争的状况下使用CAS操做消除同步使用的互斥量,那偏向锁就是在无竞争的状况下将整个同步都消除掉,连CAS都不操做了。偏向的意思就是这个对象的锁会偏向于第一个获取到它的线程,若是再接下来的过程当中,该锁没有被其余线程获取,则持有偏向锁的线程将永远不须要同步。
是否开启偏向锁模式,看的是参数UseBiasedLocking,这个在synchronizer.cpp文件中也能够看到。
if (UseBiasedLocking) { BiasedLocking::revoke_and_rebias(obj, false, THREAD); assert(!obj->mark()->has_bias_pattern(), "biases should be revoked by now"); }
若是启用了偏向锁,那么当锁对象第一次被线程获取的时候,JVM会把对象头的标志位置为“01”,Biased/Biasable,同时使用CAS操做把得到锁的线程ID记录在对象的Mark Word之中,若是CAS操做成功,持有偏向锁的线程之后每次进入到这个锁的相关同步块时JVM均不用再次进行同步操做。当另外有线程去尝试获取这个锁时,偏向模式宣布结束,恢复到Unlocked或者是Light-weight locked,后续的同步操做就和上述轻量级锁那样执行。
提纲:先描述了为何会用到synchronized,再学习了同步的用法及背后的实现,最后到JVM中ObjectMonitor,这里没有分析ObjectMonitor具体是怎么作的,最后学了一下锁的优化技术。
参考资料:
https://github.com/lingjiango/ConcurrentProgramPractice
https://wiki.openjdk.java.net/display/HotSpot/Synchronization
https://stackoverflow.com/questions/26357186/what-is-in-java-object-header
https://stackoverflow.com/questions/36371149/reentrantlock-vs-synchronized-on-cpu-level
https://www.ibm.com/developerworks/java/library/j-jtp10264/
https://stackoverflow.com/questions/47605/string-concatenation-concat-vs-operator
<<HotSpot实战>>