并发编程的三个理念
- 原子性:一个操作要么全部完成,要么全部失败。
- 可见性:当一个线程对共享变量进行修改后,其他线程也应立刻看到。
- 有序性:程序按照顺序执行
synchronized基本使用
- 修饰静态方法,锁的是类,Class字节码对象
- 修饰实例方法,锁的是当前实例对象
- 修饰代码块,锁的是当前指定的对象
原理
在JDK1.6之前,synchronized是重量级锁,是独占锁,在JDK6中,引入了偏向锁和轻量级锁,同时synchronized支持锁升级,降低了synchronized的性能消耗。
我们以synchronized的重量级锁为例,来讲解原理。
同步代码块
当一个线程访问同步代码块时,首先要获取到锁才能执行同步代码块,当退出或抛出异常时必须要释放锁
我们这里有一个同步代码块
public void add() {
synchronized (this) {
int i = 1;
int b = i + 3;
}
}
利用javap反编译看这段代码的字节码指令
可以看到,synchronized是通过monitorenter和monitorexit这一组字节码指令来完成对临界资源的互斥访问
- monitorenter标志进入同步代码块
- monitorexit标志退出同步代码块。
我们知道,任意一个对象都可以作为锁对象。
每个锁对象都有一个监视器,叫做Monitor,当Monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取Monitor的使用权,过程如下:
- 如果Monitor的进入数是0,则该线程进入Monitor,然后将进入量设置为1,该线程即为Monitor的所有者。
- 如果线程已经占用了该Monitor,只是重新进入,则进入Monitor的进入数加1
- 如果其他线程已经占用了Monitor,则该线程进入阻塞状态,直到Monitor的进入数为0,在重新尝试获取Monitor的所有权。
Monitor是操作系统中管程的一个实现,管程是操作系统中对同步互斥的一种实现方案,建议先去看看管程。
执行monitorexit的线程必须是锁对象所对应的Monitor的所有者,线程执行monitorexit指令的过程:
monitorexit执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出Monitor,不再是这个Monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个monitor的所有权。
这一组指令必须是成对出现的,不能单独出现,这两个指令是通过操作系统互斥原语mutex来实现的,被阻塞的线程会被挂起、等待重新调度,会导致用户态和内核态之间的来回切换,性能损耗严重。
总结,synchronized底层是通过一个Monitor监视器对象来实现的,线程只有抢占到了Monitor对象的所有权,才有权获取到临界资源,线程之间的wait/notify等方法也是依赖于monitor对象,这就是为什么只有在同步代码块中执行wait/notify,否则抛出异常
同步方法
对于同步方法,即在方法上使用synchronized关键字修饰,在反编译后,并没有看到monitorenter和monitorexit这一组指令。
这是一个同步方法
public synchronized void add() {
int i = 1;
int b = i + 3;
}
利用javap 反编译后的结果中,没有monitorenter和monitorexit这一对字节码指令
对于同步方法来说,在常量池中多了一个ACC_SYNCHRONIZED
标志位,用来标记该方法是否是一个同步方法,JVM会检查该标志位来完成方法的同步:
当方法被调用时,首先会检查该方法的ACC_SYNCHRONIZED
标志位是否被设置
- 如果设置了,表明该方法是一个同步方法,会先去持有Monitor,然后执行方法体。
- 在方法执行期间,其他线程都无法获取到同一个Monitor。
- 如果在方法执行期间发生了无法处理的异常,那么在抛出异常时,会自动释放该Monitor。
无论是同步方法还是同步代码块,这两种同步的本质是没有区别的,都是通过Monitor监视器对象来实现的。
ObjectMonitor
在JVM中,Monitor是由ObjectMonitor实现的,
ObjectMonitor整体上分为两部分,一部分是是这个监控对象的基本信息,表示当前锁的实时状态,一部分表示各种情况下需要获取锁的排队信息。如图所示:
具体的工作流程是:
- 每个等待锁的线程会被封装成ObjectWaiter对象,当线程需要获取Object Monitor时,将线程封装成ObjectWaiter对象,放入Entry Set集合中。
- 当线程获取到ObjectMonitor后,就可以获取到临界资源了,同时ObjectMonitor内部的owner属性指向此线程。每个ObjectMonitor同一时刻只有一个线程进入。
- 如果线程获取到了ObjectMonitor之后,在执行过程中调用了wait()或wait(timeout)方法,则当前线程进入到wait Set集合,并释放持有的ObjectMonitor。
- 当其他线程进入ObjectMonitor之后,调用notify()或notifyAll(),则wait Set中的线程会被唤醒,重新进入Entry Set去争夺ObjectMonitor
大致的工作流程就是这样。
synchronized的可重入性
从互斥锁的设计上看,当一个线程试图操作另一个线程持有的锁临界资源时,会进入阻塞状态;
当一个线程再次请求自己持有对象锁的临界资源时,请求就会成功。
在Java中,synchronized是基于原子性的内部锁机制,是可重入的,因此在一个线程调用synchronized方法的同时在其方法体内部调用该对象另一个synchronized方法,是可以的
线程获取到了锁之后,再次请求该锁对象的临界资源,是允许的,这就是synchronized的可重入性。
例如:
class MyRun{
int i = 0;
int j = 100;
public synchronized void add(){
i ++;
increase(); // 再次获取该对象锁,直接允许,因为当前线程已经持有了该锁
}
public synchronized void increase(){
j --;
}
}
当线程执行到了add()方法,说明该线程已经拥有了该锁,在add()方法中,再次请求increase()方法,因为该方法的锁已经被当前线程持有了,所以直接允许,操作成功,这就是可重入性。
synchronized的优化
锁的状态共有四种:无锁、偏向锁、轻量级锁、重量级锁。
随着线程的竞争激励程度增加,锁可以从偏向锁升级到轻量级锁,再升级到重量级锁,锁的升级是单向的,只能从低到高升级,不会出现锁的降级
可以看我的这篇文章synchronized锁膨胀、锁升级、锁优化
等待唤醒与synchronized
notify()、notifyAll()和wait()这三个方法,就是实现多线程中的等待唤醒机制,使用这三个方法时,必须处于synchronized代码块或synchronized方法中,否则抛出异常,这是因为调用这几个方法前必须要拿到当前锁对象的监视器对象,也就是notify()、notifyAll()、wait()方法依赖于Monitor对象
注意:与sleep()方法不同的是,wait()方法调用完成后,线程会被暂停,线程将会释放当前持有的Monitor,直到其他线程调用notify()或notifyAll()方法后才能重新竞争锁;而sleep()方法只让线程休眠但不释放锁。
参考资料
深入理解Java并发之synchronized实现原理_synchronized原理_zejian_的博客-CSDN博客
Java并发编程:Synchronized及其实现原理 - liuxiaopeng - 博客园
☆啃碎并发(七):深入分析Synchronized原理 - 简书
JAVA系列教程:Object Monitor与Synchronized关键字_Mary Ling的博客-CSDN博客