目录
java1.5之前Synchronized是一个重量级锁
java1.6有了各种优化,锁的四种分类:无锁-》偏向锁-》轻量级锁-》重量级锁
1.对象头以及markword
对象包括:
- 对象头:markword(哈希码,GC分代年龄,锁状态标识,线程持有锁,偏向线程ID,偏向时间戳),类型指针,如果是数组,还包括数组长度
- 实例数据
- 对齐填充(非必须)
三个用法:
- 普通同步方法:锁的是当前实例对象
- 静态同步方法:锁的是当前类的class对象
- 同步代码块:锁的是Synchronized括号里的对象
无锁-》偏向锁-》轻量级锁-》重量级锁
具体使用哪种锁,要看当前竞争激烈程度
无竞争:会使用偏向锁
轻量竞争:会由偏向锁升级成轻量级锁
重度竞争:会由轻量级锁升级成重量级锁
1.1.普通同步方法
线程栈通过指令去调用在堆里的实例对象,去检查在方法区中的访问标志,也就是大名鼎鼎的ACC_SYNCHRONIZED是否被设置,怎么检查的呢?
1.其实就是检查mark word的锁标志位是不是01
2.把该对象的对象头中的mark word拷贝到自己的执行线程栈中(displaced mark word)
3.通过displaced mark word中指向的重量级锁的指针,找到Monitor对象的起始地址
之后大家就熟悉了,获取Monitor对象,获取成功之后才能执行方法体,其他线程无法获得这个monitor对象了。
1.2.同步代码块
利用了monitorEnter和monitorExit这两个字节码指令,它们分别位于同步代码块的开始和结束位置。
当jvm执行到monitorEnter指令的时候,当前线程试图去获取这个对象的monitor所有权,如果成功,锁计数器+1;当执行到monitorExit指令的时候,锁计数器-1,锁就释放了。如果获取monitor对象失败,线程进入到阻塞状态,直到其他线程释放锁。
java -verbose java文件
就是可以分别看到ACC_SYNCHRONIZED,monitorEnter和monitorExit这是三个字节码指令了。
2.重量级锁
获取对象的同步监视器Monitor,这是JVM层的操作,底层使用了操作系统的互斥量
(Mutex Lock),而互斥量的本身就存在用户态到核心态的切换,这种行为本身代价就很高,之后的改进其目的都是为了减少这种情况的发生。
3.偏向锁
大多数情况下,锁不存在多线程竞争,总是由同一个线程多次获得,偏向锁的目的就是降低多次加锁解锁的开销。
偏向锁会偏向第一个获得它的线程,如果在接下来的执行过程中,该锁没有被其他线程获取,那么持有该锁的线程永远不需要同步。
适用场景:只适用于一个线程访问同步块的场景,当有另一个线程去尝试获得锁,偏向锁就结束了,该对象被恢复到无锁或者轻量级锁状态
优缺点:
加锁和解锁不需要额外消耗;
多线程竞争下,会带来额外锁撤销的消耗,这种情况下建议关闭偏向锁
(--XX:-UseBiaseLocking来禁用偏向锁)。
4.轻量级锁
当出现多个线程进行竞争的时候,或出现锁升级,由原来的的无锁或者偏向锁升级成轻量级锁。
轻量级锁所适应的场景:线程交替执行同步方法或者同步块的时候,如果存在同一时间访问同一锁的情况下,会导致锁进一步升级成重量级锁。
前面流程跟偏向锁一样,
- 该对象的对象头中的mark word拷贝到事先在执行线程栈中建立好的锁记录空间里(Lock Record),这种拷贝记录被称为displaced mark word。
- CAS操作尝试将对象的Mark Word更新成指向Lock Record,并修改Lock Record里的owner指针指向object mark word
- 如果更新成功,则获取对象锁成功,锁标志被设置成“00”,表示该对象处于轻量级锁状态
- 如果更新失败,检查当前对象的mark word是否指向当前线程,如果是就表示已经获取了锁,可以直接进入同步块执行,否则就说明竞争严重,锁要升级成重量级锁,锁标志更新成“10”,Mark word中存储就是互斥量的指针,后续等待锁的线程都得进入线程阻塞状态。当前线程便尝试使用自旋锁来获取锁,自旋就是为了不让线程阻塞,而采取的去获得锁的过程。
轻量级解锁过程:通过CAS把复制过来的displaced mark word对象替换成当前的mark word,替换成功,则整个同步过程就完成了;如果替换失败,说明有其他线程去尝试获取锁,就要在释放锁的同时,唤起被挂起的线程,线程的挂起和阻塞都会造成线程切换,增加付出时间代价的成本。
5.锁优化
适应性自旋:获取轻量级锁失败的过程中执行CAS操作失败,需要自旋获取重量级锁,自旋需要消耗CPU,白白浪费CPU资源,需要指定自旋次数,JDK更聪明采用适应性自旋,如果线程自旋成功了,自旋次数就会增加,否则就减少自旋次数。
锁合并(粗化):将多次加锁解锁的操作合并,例如stringbuffer中的append方法,只需要第一次加锁,最后一次解锁即可。
锁消除:删除不必要的加锁操作,根据代码逃逸技术,认为这段代码是线程安全的,就不需要加锁操。