总览
Java中锁机制可以分成以下几种:
- Lock
- Synchronized
- Automatic (CAS)
今天就我目前总结的知识来简单的聊一聊Synchronized。私以为学习需要有一个全局的概览,才可以更好的进行理解和记忆。如图是我理解的在锁升级过程中的知识概览。
Synchronized 的作用
- 原子性:所谓原子性就是指一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。被synchronized修饰的类或对象的所有操作都是原子的,因为在执行操作之前必须先获得类或对象的锁,直到执行完才能释放。
- 可见性:可见性是指多个线程访问一个资源时,该资源的状态、值信息等对于其他线程都是可见的。 synchronized和volatile都具有可见性,其中synchronized对一个类或对象加锁时,一个线程如果要访问该类或对象必须先获得它的锁,而这个锁的状态对于其他任何线程都是可见的,并且在释放锁之前会将对变量的修改刷新到共享内存当中,保证资源变量的可见性。
- 有序性:有序性值程序执行的顺序按照代码先后执行。 synchronized和volatile都具有有序性,Java允许编译器和处理器对指令进行重排,但是指令重排并不会影响单线程的顺序,它影响的是多线程并发执行的顺序性。synchronized保证了每个时刻都只有一个线程访问同步代码块,也就确定了线程执行同步代码块是分先后顺序的,保证了有序性。
Synchronized 的使用
Synchronized主要有三种用法:
- 修饰实例方法
synchronized void method(){
...
}
- 修饰静态方法
synchronized static void method(){
...
}
- 修饰代码块
synchronized(this){
...
}
本质上synchronized
锁定的都是对象。
synchronized
关键字加到 static 静态方法和 synchronized(class) 代码块上都是是给 Class 类上锁。
synchronized
关键字加到实例方法上是给对象实例上锁。
锁优化和同步原理
java 1.6 版本之后为了提高效率,对Synchronized做了锁优化。其中主要包括了以下三个方面:
锁粗化
通常情况下,为了保证多线程间的有效并发,会要求每个线程持有锁的时间尽可能短,但是大某些情况下,一个程序对同一个锁不间断、高频地请求、同步与释放,会消耗掉一定的系统资源,因为锁的讲求、同步与释放本身会带来性能损耗,这样高频的锁请求就反而不利于系统性能的优化了,虽然单次同步操作的时间可能很短。锁粗化就是告诉我们任何事情都有个度,有些情况下我们反而希望把很多次锁的请求合并成一个请求,以降低短时间内大量锁请求、同步、释放带来的性能损耗。
举个特殊点的例子,如循环中的同步块就进行粗化,以为多次重复加锁解锁。
for(int i=0;i<size;i++){
synchronized(lock){
}
}
锁消除
锁消除是发生在编译器级别的一种锁优化方式。
有时候我们写的代码完全不需要加锁,却执行了加锁操作。
比如,StringBuffer类的append操作。
编译器会将不需要用到的同步操作去除。
锁升级
锁升级过程中,会改变对象头中的标志位信息
我自己有个简单的记忆Tips:
首先,无锁和偏向锁 都是01 但是偏向锁有一个偏向锁标志位 会 置为1
第二、升级成轻量级锁是不连续,所以标志位为00
第三、重量级锁为10
第四、剩下的为GC标记 11
偏向锁
偏向锁是JDK6中的重要引进,因为HotSpot作者经过研究实践发现,在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低,引进了偏向锁。
当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。
如果测试成功,表示线程已经获得了锁。如果测试失败,则需要再测试一下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁):如果没有设置,则使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。
轻量级锁
当有2个线程尝试获取偏向锁时,就会发生锁升级过程,两个线程会在自己的栈中生成一个锁记录,通过CAS操作使头像头中的锁指针指向自己的Lock Record,成功的那个则获得锁。
重量级锁
当未获取到对象锁的线程自旋超过一定的次数或者有更多的线程开始竞争这个锁的适合就会升级到重量级锁。
在java代码中,通过javap
查看字节码信息,能看到monitorenter
和monitorexit
两个字节码指令。其中monitorenter
指令指向同步块的开始位置,monitorexit
指令指明同步块的结束位置。
当执行 monitorenter
指令时,线程试图获取锁也就是获取 对象监视器 monitor
的持有权。
在 Java 虚拟机(HotSpot)中,Monitor 是基于 C++实现的,由ObjectMonitor实现的。每个对象中都内置了一个 ObjectMonitor对象。
另外,wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。
Monitor
每个Java对象都跟Monitor关联
在C++中Monitor的数据结构如下:
oopDesc–继承–>markOopDesc–方法monitor()–>ObjectMonitor–>enter、exit 获取、释放锁
oopDesc类是JVM对象的顶级基类,故每个object都包含markOop
具体更详细的内容可以查看参考资料。
我这里只提几个比较重要的参数:
1. _owner 当前锁对象线程拥有这
2. _EntryList 未获取锁被阻塞或者被wait的线程重新进入被放入entryList中
3. _waitSet 处于wait状态的线程,被加入到这个linkedList
这里我被阿里的面试官问到过Synronized和notify的关系,实际上wait后的线程会被放入到_waitSet,此时如果调用了notify方法,就会把一个wait状态的线程唤醒,但唤醒不意味着like获得锁,仍需要进入EntryList重新竞争。
引用参考
https://www.cnblogs.com/qingshan-tang/p/12698705.html
https://blog.csdn.net/qq_26222859/article/details/80546917
https://www.cnblogs.com/three-fighter/p/14396208.html
https://www.cnblogs.com/dennyzhangdd/p/6734638.html
https://blog.csdn.net/weixin_43968383/article/details/116934595