同一个时间只容许一个线程拥有一个对象锁,这样在同一时间只有一个线程对须要同步的代码块进行访问java
必须确保在某个线程的某个对象锁在释放以前,对某个共享变量所作的改变,对于下一个拥有在这个对象锁的线程是可见的,不然另外线程读取的是本地的副本从而进行操做,致使结果不一致。安全
从互斥锁的设计上来讲,一个线程试图操做一个由其余线程持有的临界资源的时候,这个线程会处于堵塞状态。数据结构
若是一个线程再次请求本身持有对象锁的临界资源的时候,这就属于重入锁。多线程
所以在一个线程调用synchronized方法的同时在其方法体内部调用该对象另外一个synchronized方法,也就是说一个线程获得一个对象锁后再次请求该对象锁,是容许的,这就是synchronized的可重入性。性能
锁对象存储在Java对象头里面测试
位数 | 头对象结构 | |
---|---|---|
32 | Mark word | 存储对象的HashCode,GC分代年龄,锁类型,锁标记 |
32 | Class MeteDataAddress | 类型指针:指向实例对象所属的类 |
MarkWord被设定为一个非固定的数据结构,用来存储更多的数据,结构以下(这里不是很懂)操作系统
Monitor(内部锁,Monitor锁,管程,监视器锁,也就是和对象锁对应的对象).net
每一个对象都存在这一个Monitor与之关联线程
每一个Java对象天生带有这把看不见的锁,在MarkWord的结构中,重量级锁的标记为是10,也就是指针就是指向Monitor对象的起始地址,在这里也就说明了Synchronized的默认锁是重量级锁。monitor能够与对象一块儿建立销毁或当线程试图获取对象锁时自动生成,但当一个 monitor 被某个线程持有后,它便处于锁定状态。设计
在Java虚拟机中,Monitor是有MonitorObject所实现的,部分结构以下
_owner:指向持有ObjectMonitor对象的线程
_WaitSet:存放处于wait状态的线程队列
_EntryList:存放处于等待锁block状态的线程队列
_count:用来记录该线程获取锁的次数
ObjectMonitor中有两个队列,_WaitSet 和 _EntryList,用来保存ObjectWaiter对象列表( 每一个等待锁的线程都会被封装成ObjectWaiter对象),_owner指向持有ObjectMonitor对象的线程,当有多个线程访问同一块同步代码块的时候,线程会线程会进入_EntryList,当线程获取到对象的monitor 后进入 _Owner 区域并把monitor中的owner变量设置为当前线程,同时monitor中的计数器count加1,若线程调用 wait() 方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 WaitSet集合中等待被唤醒。若当前线程执行完毕也将释放monitor(锁)并复位变量的值,以便其余线程进入获取monitor(锁)。
Monitorenter和Monitorexit
Synchronized代码块执行原理
字节码中可知同步语句块的实现使用的是monitorenter 和 monitorexit 指令,其中monitorenter指令指向同步代码块的开始位置,monitorexit指令则指明同步代码块的结束位置 。当执行monitorenter指令时,若是当前线程获取对象锁所对应的monitor的特权的时候
1 会去检查monitor的对象的count是否为0
2 若是为0的话就获取成功,而且将count置为1
3 假若其余线程已经拥有 objectref 的 monitor 的全部权,那当前线程将被阻塞,直到正在执行线程执行完毕,即monitorexit指令被执行,执行线程将释放 monitor(锁)并设置计数器值为0 ,其余线程将有机会持有 monitor 。
编译器将会确保不管方法经过何种方式完成,方法中调用过的每条 monitorenter 指令都有执行其对应 monitorexit 指令,而不管这个方法是正常结束仍是异常结束。为了保证在方法异常完成时 monitorenter 和 monitorexit 指令依然能够正确配对执行,编译器会自动产生一个异常处理器,这个异常处理器声明可处理全部的异常,它的目的就是用来执行 monitorexit 指令。通常字节码文件中都会多出一条monitorexit指令。
Synchronized方法执行原理
方法级的同步是隐式,即无需经过字节码指令来控制的,它实如今方法调用和返回操做之中。JVM能够从ACC_SYNCHRONIZED 访问标志区分一个方法是否同步方法。当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,若是设置了,执行线程将先持有monitor,而后再执行方法,最后再方法完成(不管是正常完成仍是非正常完成)时释放monitor。
若是一个同步方法执行期间抛出了异常,而且在方法内部没法处理此异常,那这个同步方法所持有的monitor将在异常抛到同步方法以外时自动释放
自旋锁
synchronized在jdk1.6以前的锁是重量级锁,对于互斥同步的性能来讲,阻塞挂起的是影响最大的。由于挂起线程和恢复线程都是要让操做系统从用户态转化到内核态中完成,而这两个状态的转换是比较影响性能的。
大多数状况下,线程拥有锁的时间不会太长,若是直接挂起的话,会影响系统的性能。由于前面说过,线程切换是须要在操做系统的用户态和内核态之间转换的。因此为了解决这个问题,引进了自旋锁。
自旋锁假设在不久,当前线程能够得到这个锁,所以JVM就让这个想要得到锁的线程,先作几个空循环先,让这个线程先不要放弃占有CPU资源的机会,通过若干次空循环以后,若是得到锁,那么就顺利的进入临界区。不然,你也不能让这个线程一直占有CPU资源呀,因此通过大概10次空循环以后,就只能老老实实地挂起了。
自旋适应锁
自旋适应锁就是从自旋锁改进而来的。在自旋锁的基础上,假如A线程经过自旋必定的时间以后得到了锁,而后释放锁。这时B线程也得到了这个锁,若是此时A线程再次想获得这个锁,那么JVM就会根据以前A线程曾经得到过这个锁,那么我就给你适当地增长一点空循环的次数,好比说从10次空循环到100次。假若有个C线程,他也想得到这个锁,也得自旋等待,但是不多轮到他或者没获得过这个锁(多是被A抢了机会或者其余的),那么JVM就会认为C线程之后可能没什么机会得到了,就适当地减小C线程的空循坏次数甚至不让他作空循环。
偏向锁
若是A线程第一次得到锁,那么锁就进入偏向模式(虚拟机把对象头中的标志位设为“01”),MarkWord的结构也变成偏向锁结构,若是没有其余线程和A线程竞争,A线程再次请求该锁时,无需任何同步操做
只须要检查MarkWord的锁标记位是否为偏向锁和当前线程的Id是否为ThreadId便可。
也就是说当一个线程访问同步块而且获取锁的时候,会经过CAS操做在对象头的偏向锁结构里记录线程的ID,若是记录成功,线程在进入和退出同步块时,不须要进行CAS操做来加锁和解锁,从而提升程序的性能。
TIPS:偏向锁只能被第一个获取它的线程进行 CAS 操做,一旦出现线程竞争锁对象,其它线程不管什么时候进行 CAS 操做都会失败。
加锁具体步骤以下
先检查Mark Word是否为可偏向状态,也就是说是否 是偏向锁1,锁标识位为01
若是是可偏向状态,那么就测试Mark Word结构的线程ID是否是和当前线程的ID一致,
若是是就直接执行同步代码块。
若是不是就经过CAS操做竞争锁,
若是操做成功,就把Mark Word的线程ID设置为线程的ID
若是操做失败,那么就说明此时有多线程竞争的状态,等到安全点,得到偏向锁的线程就挂起,进行解锁操做。偏向锁升级为轻量锁,被阻塞在安全点的线程继续往下执行同步代码块。
解锁
当得到偏向锁的线程挂起以后,就会进行解锁操做。
在解锁成功以后,JVM判断此时线程的状态,
若是尚未执行完同步代码,则直接将偏向锁升级为轻量级锁,而后继续执行剩下的代码块。
若是此时已经执行完同步代码,则撤销锁为无锁状态,之后执行同步代码的时候JVM则会直接升级为轻量锁。
轻量锁(加锁解锁操做是须要依赖屡次CAS原子指令的)
偏向锁一旦受到多线程竞争,就会膨胀为轻量锁
获取锁
释放锁
重量级锁
重量级锁经过对象内部的监视器(monitor)实现
其中monitor的本质是依赖于底层操做系统的Mutex Lock实现
操做系统实现线程之间的切换须要从用户态到内核态的切换,切换成本很是高。
锁主要存在四种状态,无状态锁,偏向锁,轻量锁,重量锁,会随着线程竞争的程度逐渐增大。锁只能够单向升级,不能够降级。
主要是为了提升得到锁和解锁的效率。
锁类型 | 特征 | 优势 | 缺点 | 使用场景 |
---|---|---|---|---|
偏向锁 | 只须要比较ThreadId | 加锁和解锁不须要额外的消耗,和执行非同步代码块时间相差无几 | 若是线程之间有竞争,会增长锁撤销的消耗 | 当程序大部分只有一个线程操做的时候 |
轻量锁 | 自旋 | 竞争线程不会阻塞,提升了程序的响应速度 | 始终得不到锁的线程使用自旋会消耗CPU | 追求响应时间,同步执行代码比较快的时候 |
重量锁 | 依赖Mutex(操做系统的互斥) | 线程竞争不使用自旋,不怎么会消耗CPU | 线程阻塞,响应缓慢 | 同步代码执行比较慢的状况 |
这里有一张原理图(盗用别人的图),把上述的文字都进行了一个总结