Java提供的常用同步手段之一就是sychronized关键字,synchronized 是利用锁的机制来实现同步的。
下文我们从3个角度深入剖析synchronized的应用原理:
- synchronized的四个特点(原子性/有序性/互斥性/可重入)
- synchronized的两种锁分类(类锁/对象锁)
- synchronized与ReentrantLock的区别
winter
必须先提及一个重要的基础概念:Monitor监听机制。
Monitor是什么?
Monitor 是Java中实现 synchronized关键字的基础,可以将它理解为一个监听器,是用来实现同步的工具,monitor与每一个Java对象与class字节码相关联。monitor对象存在于每个Java对象的对象头中,synchronized锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因。(参考:《JVM锁优化》)
Monitor的本质?
Monitor 在JVM中是基于C 的实现的,ObjectMonitor中有几个关键属性,见下图:
_owner:指向持有ObjectMonitor对象的线程
_WaitSet:存放处于wait状态的线程队列
_EntryList:存放处于等待锁block状态的线程队列
_recursions:锁的重入次数
_count:用来记录该线程获取锁的次数
加锁过程:
当多个线程(A/B/C)同时访问一段同步代码时,首先会进入_EntryList队列中,当某个线程A获取到对象的monitor后进入_Owner区域并把monitor中的_owner变量设置为当前线程,同时monitor中的计数器_count加1,即(该线程A)获得锁。线程B/C都进入了_EntryList里面,线程A进入了_Owner。
释放锁过程:
若持有monitor的线程A调用wait()方法,将释放它当前持有的monitor,_owner变量恢复为null,_count自减1,同时线程A进入_WaitSet集合中等待被唤醒。
此时在_EntryList的线程B/C会竞争获取monitor,假设结果是B线程竞争成功并进入了_Owner。线程C留在了_EntryList里面,线程A进入了_WaitSet。
若当前线程B执行完毕也将释放monitor(锁)并复位变量的值,以便其他线程进入获取monitor(锁)。线程B可以通过notify/notifyAll 来唤醒 _WaitSet 的线程A,此时_WaitSet 的线程A 与 _EntryList 的线程C会同时进行锁资源竞争。
注意:
1、由于notify唤醒线程具有随机性,甚至导致死锁发生;因此一般建议使用notifyAll。
2、不管唤醒一个线程,还是唤醒多个线程,最终获得对象锁的,只有一个线程。如果_EntryList同时存在竞争锁资源的线程,那么被唤醒的线程还需要和_EntryList中的线程一起竞争锁资源。但是JVM保证最终只会让一个线程获取到锁。
synchronized 的四个特征
基于 monitor 机制,引出了 synchronized 的四个特征:
1.原子性
基于monitor监视器,被synchronized修饰的类或对象的所有操作都是原子的,因为在执行操作之前必须先获得类或对象的锁,直到执行完才能释放,这中间的过程无法被中断(除了已经废弃的stop()方法),即保证了原子性。
2.可见性
基于monitor监视器,synchronized 对一个类或对象加锁时,一个线程如果要访问该类或对象必须先获得它的锁,而这个锁的状态对于其他任何线程都是可见的。在释放锁之前会将对变量的修改刷新到主内存当中,从而保证资源变量的可见性。
3.有序性
基于monitor监视器,有效解决重排序问题:指令重排并不会影响单线程的顺序和结果,它影响的是多线程并发执行的顺序性。而 synchronized 保证了每个时刻都只有一个线程访问同步代码块,也就确定了线程执行同步代码块是分先后顺序的,保证了有序性。
4.可重入性
synchronized和ReentrantLock都是可重入锁。
当一个线程试图操作一个由其他线程持有的对象锁的临界资源时,将会处于阻塞状态;
当一个线程再次请求自己持有对象锁的临界资源时,这种情况属于重入锁。通俗一点讲就是说一个线程拥有了锁仍然还可以重复申请锁。
synchronized 的效果:可以具体体现为 “monitorenter”与“monitorexit”两条指令(一个monitor exit指令之前都必须有一个monitor enter),下面是编译文件的例子:
对 synchronized 的优化,参考《JVM的锁优化》可知,锁升级的过程是:偏向锁 -> 轻量级锁 -> 重量级锁。
synchronized支持类锁与对象锁
例子1:类锁
对于类锁,我们必须理解两种使用场景:
- 修饰一个静态的方法:其作用的范围是整个方法,作用的对象是这个类的所有对象
- 修饰一个类:其作用的范围是synchronized后面括号括起来的部分,作用的对象是这个类的所有对象
例子1.1:修饰一个静态的方法
代码语言:javascript复制//类锁:静态方法 - 修饰一个静态的方法
public static synchronized void lock() throws InterruptedException {
//延时1s执行日志输出
TimeUnit.SECONDS.sleep(1);
System.out.println("lock1 executeTime = " System.currentTimeMillis());
}
例子1.2:修饰一个类
代码语言:javascript复制//类锁:类名 - 修饰一个类
public static void lock2() throws InterruptedException {
synchronized (ClassLock.class){
//延时1s执行日志输出
TimeUnit.SECONDS.sleep(1);
System.out.println("lock2 executeTime = " System.currentTimeMillis());
}
}
测试用例:
代码语言:javascript复制/**
* <p>
* 类锁资源竞争例子:
* 1、修饰一个静态的方法
* 2、修饰一个类
* </p>
*/
public class ClassLock {
public static void main(String[] args) throws InterruptedException{
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
ClassLock classLock = new ClassLock();
try {
// classLock.lock();
classLock.lock2();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
ClassLock classLock = new ClassLock();
try {
// classLock.lock();
classLock.lock2();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t1.start();
t2.start();
//由于t1 和 t2 存在类锁资源竞争,所以两个线程真正执行时间是不一样的
}
}
输出结果:类锁的两种锁都存在竞争互斥,因此代码段都不是同时被执行。
代码语言:javascript复制lock1 executeTime = 1616499560230
lock2 executeTime = 1616499561230
lock1 executeTime = 1616499562231
lock2 executeTime = 1616499563231
例子2:对象锁
对于对象锁,我们必须理解两种使用场景:
- 修饰一个方法:被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;
- 修饰一个代码块:被修饰的代码块称为同步语句块,其作用范围是大括号{}括起来的代码块,作用的对象是调用这个代码块的对象;
例子2.1:修饰一个方法
代码语言:javascript复制//对象锁:普通方法
public synchronized void lock() throws InterruptedException {
//延时1s执行日志输出
TimeUnit.SECONDS.sleep(1);
System.out.println("lock1 executeTime = " System.currentTimeMillis());
}
例子2.2:修饰一个代码块
代码语言:javascript复制 //对象锁:普通方法代码块
public void lock2() throws InterruptedException {
synchronized (this){
//延时1s执行日志输出
TimeUnit.SECONDS.sleep(1);
System.out.println("lock2 executeTime = " System.currentTimeMillis());
}
}
测试用例:
代码语言:javascript复制public class ObjectLock {
public static void main(String[] args) throws InterruptedException{
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
ObjectLock classLock = new ObjectLock();
try {
classLock.lock();
// classLock.lock2();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
ObjectLock classLock = new ObjectLock();
try {
classLock.lock();
// classLock.lock2();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t1.start();
t2.start();
//由于t1 和 t2 不存在对象锁资源竞争,所以两个线程真正执行时间一样
}
}
输出结果:多线程使用的对象锁不存在互斥竞争,因此都是同时被执行了。
代码语言:javascript复制lock1 executeTime = 1616499668823
lock1 executeTime = 1616499668823
lock2 executeTime = 1616499669823
lock2 executeTime = 1616499669823
与ReentrantLock的区别
下面分别从六个角度阐述两者(synchronized 与 ReentrantLock)的区别:
底层实现/可中断机制支持/释放锁方式(手动/非手动)/锁类型(公平锁/非公平锁)/等待线程的精确唤醒/锁对象。
1、底层实现
synchronized 是JVM层面的锁,是Java关键字,通过monitor对象来完成(monitorenter与monitorexit),对象只有在同步块或同步方法中才能调用wait/notify方法;同时涉及到锁的升级,具体为无锁、偏向锁、自旋锁、向OS申请重量级锁(参考另一篇文章:JVM锁的升级)
ReentrantLock 是从jdk1.5以来(java.util.concurrent.locks.Lock)提供的API层面的锁,是通过利用CAS(CompareAndSwap)自旋机制保证线程操作的原子性和volatile保证数据可见性以实现锁的功能。 2、不可中断执行
synchronized是不可中断类型的锁,除非加锁的代码中出现异常或正常执行完成;
ReentrantLock则可以中断,可通过trylock(long timeout,TimeUnit unit)设置超时方法或者将lockInterruptibly()放到代码块中,调用interrupt方法进行中断。 3、jvm底层释放资源
synchronized 不需要用户去手动释放锁,synchronized 代码执行完后系统会自动让线程释放对锁的占用;
ReentrantLock则需要用户去手动释放锁,如果没有手动释放锁,就可能导致死锁现象。一般通过lock()和unlock()方法配合try/finally语句块来完成,使用释放更加灵活。 4、是否公平锁
synchronized为非公平锁(参考开头的Monitor锁资源竞争策略);
ReentrantLock则即可以选公平锁也可以选非公平锁,通过构造方法new ReentrantLock时传入boolean值进行选择,为空默认false非公平锁,true为公平锁。 5、锁是否可绑定条件Condition进行准确的线程唤醒
synchronized不能绑定并精确唤醒某一个线程资源,通过Object类的wait()/notify()/notifyAll()方法要么随机唤醒一个线程要么唤醒全部线程;
ReentrantLock通过绑定Condition结合await()/singal()方法实现线程的精确唤醒,而不是像synchronized。 6、锁的对象
synchronzied锁的是对象,锁是保存在对象头里面的,根据对象头数据来标识是否有线程获得锁/争抢锁;
ReentrantLock锁的是线程,根据进入的线程,和int类型的state标识锁的获得/争抢。
总结
上文结合synchronized的底层原理 -- Monitor机制,分别从3个角度(synchronized的特点/锁分类/与ReentrantLock的区别)剖析了 synchronized 的原理与应用。
后续我们会继续探讨 volatile 重排序 与 ReentranLock 源码和底层原理,希望对大家有所帮助。