若是某一个资源被多个线程共享,为了不由于资源抢占致使资源数据错乱,咱们须要对线程进行同步,那么synchronized就是实现线程同步的关键字,能够说在并发控制中是必不可少的部分,今天就来看一下synchronized的使用和底层原理。javascript
所谓原子性就是指一个操做或者多个操做,要么所有执行而且执行的过程不会被任何因素打断,要么就都不执行。java
在Java中,对基本数据类型的变量的读取和赋值操做是原子性操做,即这些操做是不可被中断的,要么执行,要么不执行。可是像i++、i+=1等操做字符就不是原子性的,它们是分红读取、计算、赋值几步操做,原值在这些步骤还没完成时就可能已经被赋值了,那么最后赋值写入的数据就是脏数据,没法保证原子性。面试
被synchronized修饰的类或对象的全部操做都是原子的,由于在执行操做以前必须先得到类或对象的锁,直到执行完才能释放,这中间的过程没法被中断(除了已经废弃的stop()方法),即保证了原子性。数组
注意!面试时常常会问比较synchronized和volatile,它们俩特性上最大的区别就在于原子性,volatile不具有原子性。安全
可见性是指多个线程访问一个资源时,该资源的状态、值信息等对于其余线程都是可见的。多线程
synchronized和volatile都具备可见性,其中synchronized对一个类或对象加锁时,一个线程若是要访问该类或对象必须先得到它的锁,而这个锁的状态对于其余任何线程都是可见的,而且在释放锁以前会将对变量的修改刷新到主存当中,保证资源变量的可见性,若是某个线程占用了该锁,其余线程就必须在锁池中等待锁的释放。并发
而volatile的实现相似,被volatile修饰的变量,每当值须要修改时都会当即更新主存,主存是共享的,全部线程可见,因此确保了其余线程读取到的变量永远是最新值,保证可见性。函数
有序性值程序执行的顺序按照代码前后执行。性能
synchronized和volatile都具备有序性,Java容许编译器和处理器对指令进行重排,可是指令重排并不会影响单线程的顺序,它影响的是多线程并发执行的顺序性。synchronized保证了每一个时刻都只有一个线程访问同步代码块,也就肯定了线程执行同步代码块是分前后顺序的,保证了有序性。学习
synchronized和ReentrantLock都是可重入锁。当一个线程试图操做一个由其余线程持有的对象锁的临界资源时,将会处于阻塞状态,但当一个线程再次请求本身持有对象锁的临界资源时,这种状况属于重入锁。通俗一点讲就是说一个线程拥有了锁仍然还能够重复申请锁。
synchronized能够修饰静态方法、成员函数,同时还能够直接定义代码块,可是归根结底它上锁的资源只有两类:一个是对象,一个是类。
先看看下面的代码(初学者看到先不要晕,后面慢慢讲解):
首先咱们知道被static
修饰的静态方法、静态属性都是归类全部,同时该类的全部实例对象均可以访问。可是普通成员属性、成员方法是归实例化的对象全部,必须实例化以后才能访问,这也是为何静态方法不能访问非静态属性的缘由。咱们明确了这些属性、方法归哪些全部以后就能够理解上面几个synchronized的锁究竟是加给谁的了。
首先看第一个synchronized所加的方法是add1()
,该方法没有被static
修饰,也就是说该方法是归实例化的对象全部,那么这个锁就是加给Test1类所实例化的对象。
而后是add2()
方法,该方法是静态方法,归Test1类全部,因此这个锁是加给Test1类的。
最后是method()
方法中两个同步代码块,第一个代码块所锁定的是Test1.class
,经过字面意思便知道该锁是加给Test1类的,而下面那个锁定的是instance
,这个instance是Test1类的一个实例化对象,天然它所上的锁是给instance实例化对象的。
弄清楚这些锁是上给谁的就应该很容易懂synchronized的使用啦,只要记住要进入同步方法或同步块必须先得到相应的锁才行。那么我下面再列举出一个很是容易进入误区的代码,看看你是否真的理解了上面的解释。
上面的简单意思就是用两个线程分别对i加100万次,理论结果应该是200万,并且我还加了synchronized锁住了add方法,保证了其线程安全性。但是!!!我不管运行多少次都是小于200万的,为何呢?
缘由就在于synchronized加锁的函数,这个方法是普通成员方法,那么锁就是加给对象的,可是在建立线程时却new了两个Test2实例,也就是说这个锁是给这两个实例加的锁,并无达到同步的效果,因此才会出现错误。至于为何小于200万,要理解i++
的过程就明白了,我以前写了一篇文章讲解过这个过程,请阅读:详谈Java中的CAS操做
synchronized有两种形式上锁,一个是对方法上锁,一个是构造同步代码块。他们的底层实现其实都同样,在进入同步代码以前先获取锁,获取到锁以后锁的计数器+1,同步代码执行完锁的计数器-1,若是获取失败就阻塞式等待锁的释放。只是他们在同步块识别方式上有所不同,从class字节码文件能够表现出来,一个是经过方法flags标志,一个是monitorenter和monitorexit指令操做。
首先来看在方法上上锁,咱们就新定义一个同步方法而后进行反编译,查看其字节码:
能够看到在add方法的flags里面多了一个ACC_SYNCHRONIZED
标志,这标志用来告诉JVM这是一个同步方法,在进入该方法以前先获取相应的锁,锁的计数器加1,方法结束后计数器-1,若是获取失败就阻塞住,知道该锁被释放。
若是看不懂字节码指令的朋友能够先阅读我以前写的两篇文章,了解一下class的结构:
咱们新定义一个同步代码块,编译出class字节码,而后找到method方法所在的指令块,能够清楚的看到其实现上锁和释放锁的过程,截图以下:
从反编译的同步代码块能够看到同步块是由monitorenter指令进入,而后monitorexit释放锁,在执行monitorenter以前须要尝试获取锁,若是这个对象没有被锁定,或者当前线程已经拥有了这个对象的锁,那么就把锁的计数器加1。当执行monitorexit指令时,锁的计数器也会减1。当获取锁失败时会被阻塞,一直等待锁被释放。
可是为何会有两个monitorexit呢?其实第二个monitorexit是来处理异常的,仔细看反编译的字节码,正常状况下第一个monitorexit以后会执行goto
指令,而该指令转向的就是23行的return
,也就是说正常状况下只会执行第一个monitorexit释放锁,而后返回。而若是在执行中发生了异常,第二个monitorexit就起做用了,它是由编译器自动生成的,在发生异常时处理异常而后释放掉锁。
在理解锁实现原理以前先了解一下Java的对象头和Monitor,在JVM中,对象是分红三部分存在的:对象头、实例数据、对其填充。
实例数据和对其填充与synchronized无关,这里简单说一下(我也是阅读《深刻理解Java虚拟机》学到的,读者可仔细阅读该书相关章节学习)。实例数据存放类的属性数据信息,包括父类的属性信息,若是是数组的实例部分还包括数组的长度,这部份内存按4字节对齐;对其填充不是必须部分,因为虚拟机要求对象起始地址必须是8字节的整数倍,对齐填充仅仅是为了使字节对齐。
对象头是咱们须要关注的重点,它是synchronized实现锁的基础,由于synchronized申请锁、上锁、释放锁都与对象头有关。对象头主要结构是由Mark Word
和 Class Metadata Address
组成,其中Mark Word
存储对象的hashCode、锁信息或分代年龄或GC标志等信息,Class Metadata Address
是类型指针指向对象的类元数据,JVM经过该指针肯定该对象是哪一个类的实例。
锁也分不一样状态,JDK6以前只有两个状态:无锁、有锁(重量级锁),而在JDK6以后对synchronized进行了优化,新增了两种状态,总共就是四个状态:无锁状态、偏向锁、轻量级锁、重量级锁,其中无锁就是一种状态了。锁的类型和状态在对象头Mark Word
中都有记录,在申请锁、锁升级等过程当中JVM都须要读取对象的Mark Word
数据。
每个锁都对应一个monitor对象,在HotSpot虚拟机中它是由ObjectMonitor实现的(C++实现)。每一个对象都存在着一个monitor与之关联,对象与其monitor之间的关系有存在多种实现方式,如monitor能够与对象一块儿建立销毁或当线程试图获取对象锁时自动生成,但当一个monitor被某个线程持有后,它便处于锁定状态。
ObjectMonitor() { _header = NULL; _count = 0; //锁计数器 _waiters = 0, _recursions = 0; _object = NULL; _owner = NULL; _WaitSet = NULL; //处于wait状态的线程,会被加入到_WaitSet _WaitSetLock = 0 ; _Responsible = NULL ; _succ = NULL ; _cxq = NULL ; FreeNext = NULL ; _EntryList = NULL ; //处于等待锁block状态的线程,会被加入到该列表 _SpinFreq = 0 ; _SpinClock = 0 ; OwnerIsThread = 0 ; }
该段摘自:https://blog.csdn.net/javazejian/article/details/72828483 ObjectMonitor中有两个队列_WaitSet和_EntryList,用来保存ObjectWaiter对象列表(每一个等待锁的线程都会被封装ObjectWaiter对象),_owner指向持有ObjectMonitor对象的线程,当多个线程同时访问一段同步代码时,首先会进入_EntryList 集合,当线程获取到对象的monitor 后进入 _Owner 区域并把monitor中的owner变量设置为当前线程同时monitor中的计数器count加1,若线程调用 wait() 方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 WaitSe t集合中等待被唤醒。若当前线程执行完毕也将释放monitor(锁)并复位变量的值,以便其余线程进入获取monitor(锁)。 monitor对象存在于每一个Java对象的对象头中(存储的指针的指向),synchronized锁即是经过这种方式获取锁的,也是为何Java中任意对象能够做为锁的缘由,同时也是notify/notifyAll/wait等方法存在于顶级对象Object中的缘由(关于这点稍后还会进行分析)
从最近几个jdk版本中能够看出,Java的开发团队一直在对synchronized优化,其中最大的一次优化就是在jdk6的时候,新增了两个锁状态,经过锁消除、锁粗化、自旋锁等方法使用各类场景,给synchronized性能带来了很大的提高。
上面讲到锁有四种状态,而且会因实际状况进行膨胀升级,其膨胀方向是:无锁——>偏向锁——>轻量级锁——>重量级锁,而且膨胀方向不可逆。
一句话总结它的做用:减小统一线程获取锁的代价。在大多数状况下,锁不存在多线程竞争,老是由同一线程屡次得到,那么此时就是偏向锁。
核心思想:
若是一个线程得到了锁,那么锁就进入偏向模式,此时Mark Word
的结构也就变为偏向锁结构,当该线程再次请求锁时,无需再作任何同步操做,即获取锁的过程只须要检查Mark Word
的锁标记位为偏向锁以及当前线程ID等于Mark Word
的ThreadID便可,这样就省去了大量有关锁申请的操做。
轻量级锁是由偏向锁升级而来,当存在第二个线程申请同一个锁对象时,偏向锁就会当即升级为轻量级锁。注意这里的第二个线程只是申请锁,不存在两个线程同时竞争锁,能够是一前一后地交替执行同步块。
重量级锁是由轻量级锁升级而来,当同一时间有多个线程竞争锁时,锁就会被升级成重量级锁,此时其申请锁带来的开销也就变大。
重量级锁通常使用场景会在追求吞吐量,同步块或者同步方法执行时间较长的场景。
消除锁是虚拟机另一种锁的优化,这种优化更完全,在JIT编译时,对运行上下文进行扫描,去除不可能存在竞争的锁。好比下面代码的method1和method2的执行效率是同样的,由于object锁是私有变量,不存在所得竞争关系。
锁粗化是虚拟机对另外一种极端状况的优化处理,经过扩大锁的范围,避免反复加锁和释放锁。好比下面method3通过锁粗化优化以后就和method4执行效率同样了。
轻量级锁失败后,虚拟机为了不线程真实地在操做系统层面挂起,还会进行一项称为自旋锁的优化手段。
自旋锁:许多状况下,共享数据的锁定状态持续时间较短,切换线程不值得,经过让线程执行循环等待锁的释放,不让出CPU。若是获得锁,就顺利进入临界区。若是还不能得到锁,那就会将线程在操做系统层面挂起,这就是自旋锁的优化方式。可是它也存在缺点:若是锁被其余线程长时间占用,一直不释放CPU,会带来许多的性能开销。
自适应自旋锁:这种至关因而对上面自旋锁优化方式的进一步优化,它的自旋的次数再也不固定,其自旋的次数由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定,这就解决了自旋锁带来的缺点。