Java并发之synchronized实现原理深入理解
概述
Java中,synchronized关键字是实现多线程同步的一种重要机制,可以让代码块以原子性、独占性执行。在并发编程中,对synchronized的理解非常重要。本文将深入讲解synchronized的实现原理,包括synchronized的底层实现、锁升级机制等方面。
synchronized的实现原理
Java语言规范通过“锁对象”的概念实现了synchronized关键字。在每个Java对象上都存在一个锁(也称为监视器),线程在执行synchronized块之前,必须先得到锁;在执行完synchronized块之后,会将锁释放。锁的实现通过JVM底层实现,具体步骤如下:
- 查找对象头:访问对象头(Object Header)中的Mark Word(标记字段)。
- 确定锁状态:根据Mark Word判断对象的锁状态。当对象锁处于初始状态时,表示锁是自由的;当对象被某一个线程锁定时,锁就处于"busy"状态,其他请求锁动作的线程则会被阻塞挂起等待
- 转换锁状态:如果当前线程没有获取到对象锁,则JVM需要让线程进入阻塞状态,等待抢占锁。如果当前线程获取到了锁,则JVM会将锁的状态改成“locked,并将请求锁的线程记为ownerThread”
锁升级机制
JVM采用的锁升级方式是轻量级锁 -> 重量级锁,这种方式被称为锁的膨胀,也称为锁的升级。锁的升级是为了在多并发场景下,保证线程安全,避免死锁,提高程序性能。具体步骤如下:
- 轻量级锁:首先,JVM会将对象的Mark Word复制到线程线程的栈帧中,自旋尝试去获取锁,如果最终自旋成功,则代表此段同步代码块是处于线程共享的,锁状态会转化为:locked,ownerThread记录的就是这个自旋成功的线程。
- 重量级锁:在自旋尝试获取锁失败的情况下,线程就会进入到重量级锁, 将该线程睡眠相应时间(该时间内不占据锁对象)。在‘睡眠后,该线程重新进入阻塞队列,争夺锁。
示例说明
下面通过两个代码示例来说明synchronized的使用及实现原理:
示例1
public synchronized void doSth() {
for (int i = 0; i < 10; i++) {
System.out.println("doSth");
// 睡眠100毫秒,方便线程切换
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
在上述示例中,我们定义了一个方法doSth(),在方法级别上使用了synchronized关键字修饰该方法。这样的话,当线程执行doSth()方法的时候,会先尝试去获取this这个对象的锁,如果获取到了锁,则该线程会持有锁并执行方法体中的代码;否则,此时处于阻塞状态,等待其它线程释放锁,进而争夺锁。如果多个线程同时访问doSth()方法,会出现互斥现象,即同一时间只有一个线程可以执行doSth()方法。
示例2
public void doSth() {
synchronized (this) {
for (int i = 0; i < 10; i++) {
System.out.println("doSth");
// 睡眠100毫秒,方便线程切换
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
在上述示例中,我们使用synchronized关键字加锁的形式来修饰代码块,而不再是方法级别。synchronized (this)表示该代码块锁住的同步对象是当前对象,即this,这样该代码块的锁对象就是this,相比于在方法级别上加锁,可以更为细粒度地控制锁对象,避免其他的线程过度等待。当线程访问该代码块的时候,也是先去获取this对象的锁。相比于在方法上直接使用synchronized关键字,代码块的使用更为灵活,但是需要自己维护好锁的释放和锁的获取,避免死锁等问题的发生。
总结
本文对synchronized的底层实现原理、锁升级机制做了详细的介绍,并结合实例进行了说明。对于Java并发编程的理解以及代码实现具有一定的借鉴作用。在实际开发中,应该根据实际需求来选择锁的实现方式并细致地处理好锁的获取和释放,提高程序的健壮性和性能。
本站文章如无特殊说明,均为本站原创,如若转载,请注明出处:Java并发之synchronized实现原理深入理解 - Python技术站