同一个时间只允许一个线程拥有一个对象锁,这样在同一时间只有一个线程对需要同步的代码块进行访问
必须确保在某个线程的某个对象锁在释放之前,对某个共享变量所做的改变,对于下一个拥有在这个对象锁的线程是可见的,否则另外线程读取的是本地的副本从而进行操作,导致结果不一致。
从互斥锁的设计上来说,一个线程试图操作一个由其他线程持有的临界资源的时候,这个线程会处于堵塞状态。
如果一个线程再次请求自己持有对象锁的临界资源的时候,这就属于重入锁。
因此在一个线程调用synchronized方法的同时在其方法体内部调用该对象另一个synchronized方法,也就是说一个线程得到一个对象锁后再次请求该对象锁,是允许的,这就是synchronized的可重入性。
锁对象存储在Java对象头里面
位数 | 头对象结构 | |
---|---|---|
32 | Mark word | 存储对象的HashCode,GC分代年龄,锁类型,锁标记 |
32 | Class MeteDataAddress | 类型指针:指向实例对象所属的类 |
MarkWord被设定为一个非固定的数据结构,用来存储更多的数据,结构如下(这里不是很懂)
Monitor(内部锁,Monitor锁,管程,监视器锁,也就是和对象锁对应的对象)
每个对象都存在这一个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 指令,其中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指令。
方法级的同步是隐式,即无需通过字节码指令来控制的,它实现在方法调用和返回操作之中。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线程再次请求该锁时,无需任何同步操作
也就是说当一个线程访问同步块并且获取锁的时候,会通过 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实现
操作系统实现线程之间的切换需要从用户态到内核态的切换,切换成本非常高。
锁主要存在四种状态,无状态锁,偏向锁,轻量锁,重量锁,会随着线程竞争的程度逐渐增大。锁只可以单向升级,不可以降级。
主要是为了提高获得锁和解锁的效率。