众所周知 Synchronized
关键字是解决并发问题常用解决方案,有以下三种使用方式:
- 同步普通方法,锁的是当前对象。
- 同步静态方法,锁的是当前
Class
对象。 - 同步块,锁的是
{}
中的对象。Synchronizd(this)锁当前实例,Synchronizd(Demo.class)锁Demo.class对象
实现原理:JVM
是通过进入、退出对象监视器( Monitor
)来实现对方法、同步块的同步的。
具体实现是在编译之后在同步方法调用前加入一个 monitor.enter
指令,在退出方法和异常处插入 monitor.exit
的指令。
其本质就是对一个对象监视器( Monitor
)进行获取,而这个获取过程具有排他性从而达到了同一时刻只能一个线程访问的目的。
而对于没有获取到锁的线程将会阻塞到方法入口处,直到获取锁的线程 monitor.exit
之后才能尝试继续获取锁。
流程图如下:
通过一段代码来演示:
public static void main(String[] args) {
synchronized (Synchronize.class){
System.out.println("Synchronize");
}
}
使用 javap -c Synchronize
可以查看编译之后的具体信息。
public class com.crossoverjie.synchronize.Synchronize {
public com.crossoverjie.synchronize.Synchronize();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: ldc #2 // class com/crossoverjie/synchronize/Synchronize
2: dup
3: astore_1
**4: monitorenter**
5: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
8: ldc #4 // String Synchronize
10: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
13: aload_1
**14: monitorexit**
15: goto 23
18: astore_2
19: aload_1
20: monitorexit
21: aload_2
22: athrow
23: return
Exception table:
from to target type
5 15 18 any
18 21 18 any
}
可以看到在同步块的入口和出口分别有 monitorenter,monitorexit
指令。
锁优化
synchronize
很多都称之为重量锁,JDK1.6
中对 synchronize
进行了各种优化,为了能减少获取和释放锁带来的消耗引入了偏向锁
和轻量锁
。锁的优化过程称为锁膨胀:锁最开始为偏向锁,锁竞争触发轻量锁,锁自旋获取长时间获取不到升级为重量级锁;
锁膨胀过程:无锁->偏向锁->轻量级锁->重量级锁
锁膨胀过程是不可逆的;
java对象头
在Hotspot虚拟机中,对象在内存中的布局分为三块区域:对象头(Mark Word、Class Metadata Address)、实例数据和对齐填充;Java对象头是实现synchronized的锁对象的基础。一般而言,synchronized使用的锁对象是存储在Java对象头里。它是轻量级锁和偏向锁的关键。
Mark Word
Mark Word用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的
锁、偏向线程 ID、偏向时间戳等等。Java对象头一般占有两个机器码(在32位虚拟机中,1个机器码等于4字节,
也就是32bit)。Class Metadata Address
类型指针,即是对象指向它的类的元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。Array length
如果对象是一个Java数组,那在对象头中还必须有一块用于记录数组长度的数据。
轻量锁
使用流程:
当代码进入同步块时,如果同步对象为无锁状态时,当前线程会在栈帧中创建一个锁记录(
Lock Record
)区域,同时将锁对象的对象头中Mark Word
拷贝到锁记录中,再尝试使用CAS
将Mark Word
更新为指向锁记录的指针。如果更新成功,当前线程就获得了锁。
如果更新失败
JVM
会先检查锁对象的Mark Word
是否指向当前线程的锁记录。如果是则说明当前线程拥有锁对象的锁,可以直接进入同步块。
锁膨胀:不是则说明有其他线程抢占了锁,这个时候会首先使用轻量级锁进行争抢,即通过自旋的方式实现锁的获取,轻量级锁的好处是在其他线程执行速度很快的情况下可以提升性能;缺点是当挣钱的线程很多的时候大量线程自旋加大了对计算机性能的开销,因此,当自旋次数达到10次轻量锁就会膨胀为重量锁。
解锁
轻量锁的解锁过程也是利用
CAS
来实现的,会尝试锁记录替换回锁对象的Mark Word
。如果替换成功则说明整个同步操作完成,失败则说明有其他线程尝试获取锁,这时就会唤醒被挂起的线程(此时已经膨胀为重量锁
)轻量锁能提升性能的原因是:认为大多数锁在整个同步周期都不存在竞争,所以使用
CAS
比使用互斥开销更少。但如果锁竞争激烈,轻量锁就不但有互斥的开销,还有CAS
的开销,甚至比重量锁更慢。
偏向锁
为了进一步的降低获取锁的代价,
JDK1.6
之后还引入了偏向锁。偏向锁的特征是:锁不存在多线程竞争,并且应由一个线程多次获得锁(仅适用于单线程操作锁的情况,这种很少情况下可以关闭这个提升性能)。
当线程访问同步块时,会使用
CAS
将线程 ID 更新到锁对象的Mark Word
中,如果更新成功则获得偏向锁,并且之后每次进入这个对象锁相关的同步块时都不需要再次获取锁了。释放锁
当有另外一个线程获取这个锁时,持有偏向锁的线程就会释放锁,释放时会等待全局安全点(这一时刻没有字节码运行),接着会暂停拥有偏向锁的线程,根据锁对象目前是否被锁来判定将对象头中的
Mark Word
设置为无锁或者是轻量锁状态。轻量锁可以提高带有同步却没有竞争的程序性能,但如果程序中大多数锁都存在竞争时,那偏向锁就起不到太大作用。可以使用
-XX:-userBiasedLocking=false
来关闭偏向锁,并默认进入轻量锁。
重量级锁
重量级锁通过对象内部的监视器(monitor)实现,其中monitor的本质是依赖于底层操作系统的Mutex Lock实现,操作系统实现线程之间的切换需要从用户态到内核态的切换,切换成本非常高。
自旋锁与自适应自旋
线程的挂起和恢复会极大的影响开销。并且jdk官方人员发现,很多线程在等待锁的时候,在很短的一段时间就获得了锁,所以它们在线程等待的时候,并不需要把线程挂起,而是让他无目的的循环,一般设置10次。这样就避免了线程切换的开销,极大的提升了性能。
而适应性自旋,是赋予了自旋一种学习能力,它并不固定自旋10次一下。他可以根据它前面线程的自旋情况,从而调整它的自旋,甚至是不经过自旋而直接挂起。
wait和notify的原理
调用wait方法,首先会获取监视器锁,获得成功以后,会让当前线程进入等待状态进入等待队列并且释放锁。
当其他线程调用notify后,会选择从等待队列中唤醒任意一个线程,而执行完notify方法以后,并不会立马唤醒线程,原因是当前的线程仍然持有这把锁,处于等待状态的线程无法获得锁。必须要等到当前的线程执行完按monitorexit指令以后,也就是锁被释放以后,处于等待队列中的线程就可以开始竞争锁了。
wait和notify为什么需要在synchronized里面?
wait方法的语义有两个,一个是释放当前的对象锁、另一个是使得当前线程进入阻塞队列,而这些操作都和监视器是相关的,所以wait必须要获得一个监视器锁。而对于notify来说也是一样,它是唤醒一个线程,既然要去唤醒,首先得知道它在哪里,所以就必须要找到这个对象获取到这个对象的锁,然后到这个对象的等待队列中去唤醒一个线程
作者:crossoverJie
链接:https://www.jianshu.com/p/2ba154f275ea
来源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。