底层实现
synchronized底层实现
详细参考:杨晓峰极客时间上的课程《Java核心技术面试精讲》:第16讲 | synchronized底层如何实现?什么是锁的升级、降级
synchronized 代码块是由一对儿 monitorenter/monitorexit 指令实现的,Monitor 对象是同步的基本实现单元。
-
发展历程:
- JDK6之前,Monitor 的实现完全是依靠操作系统内部的互斥锁,因为需要进行用户态到内核态的切换,所以同步操作是一个无差别的重量级操作。(@所以说效率低下嘛)
- 现代的(Oracle)JDK 中,JVM 对此进行了大刀阔斧地改进,提供了三种不同的 Monitor 实现,也就是常说的三种不同的锁:偏斜锁(Biased Locking)、轻量级锁和重量级锁大大改进了其性能。
偏斜锁:JVM 会利用 CAS 操作(compare and swap),在对象头上的 Mark Word 部分设置线程 ID,以表示这个对象偏向于当前线程,所以并不涉及真正的互斥锁
-
轻量级锁:
- 如果有另外的线程试图锁定某个已经被偏斜过的对象,JVM 就需要撤销(revoke)偏斜锁,并切换到轻量级锁实现。轻量级锁依赖 CAS 操作 Mark Word 来试图获取锁,如果重试成功,就使用普通的轻量级锁;否则,进一步升级为重量级锁。
- 因为重量级锁性能差,所以轻量级锁又衍生出了一种锁:自旋锁,其实现就是自循环若干次,通过CAS操作MARK WROD试图获取锁
其他锁模型
详细参考:王宝令极客时间上的课程《Java并发编程实战》- 08 | 管程:并发编程的万能钥匙
- 管程模型:
- Hasen模型:要求 notify() 放在代码的最后,这样 T2 通知完 T1 后,T2 就结束了,然后 T1 再执行,这样就能保证同一时刻只有一个线程执行。
- Hoare模型:T2 通知完 T1 后,T2 阻塞,T1 马上执行;等 T1 执行完,再唤醒 T2,也能保证同一时刻只有一个线程执行。但是相比 Hasen 模型,T2 多了一次阻塞唤醒操作。
- MESA模型(JAVA参考实现):MESA 管程里面,T2 通知完 T1 后,T2 还是会接着执行,T1 并不立即执行,仅仅是从条件变量的等待队列进到入口等待队列里面。这样做的好处是 notify() 不用放到代码的最后,T2 也没有多余的阻塞唤醒操作。但是也有个副作用,就是当 T1 再次执行的时候,可能曾经满足的条件,现在已经不满足了,所以需要以循环方式检验条件变量。—也就是产生假唤醒
锁变化
升级/膨胀
其实就是偏斜锁=》轻量级锁=》重量级锁的过程,见第一节 #底层实现
锁降级
锁降级确实是会发生的,当 JVM 进入安全点(SafePoint)的时候,会检查是否有闲置的 Monitor,然后试图进行降级。
synchronized锁的范围
- 范围
- 代码块
- 方法
- 对象
- 类
- 对象锁和类
- 类锁和对象锁是分开的,(现在只是个概念,用来区分对象锁的,是指静态方法的锁),程序中获得类锁的同时也可以获得对象锁。
- 同一个类锁和同一个类锁是互斥的,同一个对象锁和同一个对象锁互斥。 非静态方法不受类锁的影响
- 对象锁与实例对象相关, 不同的对象的对象锁不一样,可以同时获取两个不同对象的对象锁
package com.keven;
//类锁和对象锁的测试代码
public class SyncTest {
public static void main(String[] args) throws Exception {
runObjectLockTest();
System.out.println("finished runObjectLockTest");
runClassLockTest();
System.out.println("finished runClassLockTest");
runClassObjectLockTest();
System.out.println("finished runClassObjectLockTest");
Thread.sleep(10000);
}
//测试对象锁和类锁是否能够同时获取, 可以看到两个线程打印数据不受影响,说明不是同一个锁
private static void runClassObjectLockTest() {
new Thread(SyncTest::testClassLock1, "thread1").start();
new Thread(() -> {
new SyncTest().testObjectLock();
}, "thread2").start();
}
//测试类锁,显示thread1打印完成,后面thread2才开始打印,从侧面验证获取到的是同一个锁
private static void runClassLockTest() {
new Thread(SyncTest::testClassLock1, "thread1").start();
new Thread(SyncTest::testClassLock2, "thread2").start();
}
//测试对象锁, 可以看到两个线程打印数据不受影响, 且this对象的hash值不一样
private static void runObjectLockTest() {
final SyncTest syncTest = new SyncTest();
final Thread thread1 = new Thread(() -> {
syncTest.testObjectLock();
}, "thread1");
final Thread thread2 = new Thread(() -> {
new SyncTest().testObjectLock();
}, "thread2");
thread1.start();
thread2.start();
}
private static synchronized void testClassLock1() {
int i = 100;
int count = 0;
while ((i-- > 0) && (count++ < 10)) {
System.out.println("method testClassLock1--" + Thread.currentThread().getName() + " : " + i);
try {
Thread.sleep(50);
} catch (InterruptedException ie) {
}
}
}
private static synchronized void testClassLock2() {
int i = 100;
int count = 0;
while ((i-- > 0) && (count++ < 10)) {
System.out.println("method testClassLock2--" + Thread.currentThread().getName() + " : " + i);
try {
Thread.sleep(50);
} catch (InterruptedException ie) {
}
}
}
private void testObjectLock() {
synchronized (this) {
System.out.println(Thread.currentThread().getName() + " : " + this);
int i = 100;
int count = 0;
while ((i-- > 0) && (count++ < 5)) {
System.out.println("method testObjectLock--" + Thread.currentThread().getName() + " : " + i);
try {
Thread.sleep(50);
} catch (InterruptedException ie) {
}
}
}
}
}
synchronized和ReentrantLock有什么区别?
详细可参考:杨晓峰极客时间上的课程《Java核心技术面试精讲》:第15讲 | synchronized和ReentrantLock有什么区别呢?
- synchronized 和 ReentrantLock 的性能不能一概而论:
- 早起版本的synchronize在很多场景下性能相差较大
- 在后续版本进行了较多的改进,在低竞争场景中表现可能由于ReentrantLock
- 这里所谓的公平性是指在竞争场景中,当公平性为真时,会倾向于将锁赋予等待时间最久的线程。公平性是减少线程“饥饿”(个别线程长期等待锁,但始终无法获取)情况发生的一个办法。
- ReentrantLock与Synchronized的区别:
- ReentrantLock
- 更加的灵活,但必须手动释放锁
- 可通过条件控制同步
- 可被中断,并抛出中断异常,释放锁
- 可选择获取锁的超时时间,尝试获取锁
- 可选择是否为公平锁
- 只适合代码块的锁
- 更加的灵活,但必须手动释放锁
- synchronized
- 无需释放锁,自动处理
- 可修饰方法,类,代码块
- 非公平锁,如果阻塞则必须等待cpu调度
- ReentrantLock
- ReentrantLock与Synchronized的共通点:都是独占锁或者说是排它锁
关联关键词
- 在上面的代码中,我用的是 notifyAll() 来实现通知机制,为什么不使用 notify() 呢?
- 这二者是有区别的,notify() 是会随机地通知等待队列中的一个线程,而 notifyAll() 会通知等待队列中的所有线程。
- 从感觉上来讲,应该是 notify() 更好一些,因为即便通知所有线程,也只有一个线程能够进入临界区。但那所谓的感觉往往都蕴藏着风险,实际上使用 notify() 也很有风险,它的风险在于可能导致某些线程永远不会被通知到。@随机的弊病不就是存在永远不被轮到的弊病么?这跟非公平锁的弊病是一个意思
- wait与sleep区别在于:
- wait会释放所有锁而sleep不会释放锁资源.
- wait只能在同步方法和同步块中使用,而sleep任何地方都可以
- wait无需捕捉异常,而sleep需要
- sleep是Thread的方法,而wait是Object类的方法;
- sleep方法调用的时候必须指定时间
两者相同点:都会让渡CPU执行时间,等待再次调度!。补充关于二者的区别还可以看知乎的这篇帖子
- wait()方法与sleep()方法的不同之处在于,wait()方法会释放对象的“锁标志”。当调用某一对象的wait()方法后,会使当前线程暂停执行,并将当前线程放入对象等待池中,直到调用了notify()方法后,将从对象等待池中移出任意一个线程并放入锁标志等待池中,只有锁标志等待池中的线程可以获取锁标志,它们随时准备争夺锁的拥有权。当调用了某个对象的notifyAll()方法,会将对象等待池中的所有线程都移动到该对象的锁标志等待池。
- sleep()方法需要指定等待的时间,它可以让当前正在执行的线程在指定的时间内暂停执行,进入阻塞状态,该方法既可以让其他同优先级或者高优先级的线程得到执行的机会,也可以让低优先级的线程得到执行机会。但是sleep()方法不会释放“锁标志”,也就是说如果有synchronized同步块,其他线程仍然不能访问共享数据
常见面试题
- synchronized和ReentrantLock的区别 @见笔记
- 锁什么时候升级/降级?@见笔记
- 类锁和对象锁的区别? @见笔记
- 为什么JDK8中ConcurrentHashMap的锁实现要用CAS+synchronized来取代Segment+ReentrantLock呢?
- @详细见ConcurrentHashMap 1.8为什么要使用CAS+Synchronized取代Segment+ReentrantLock - 羊飞 - 博客园
- 简单说就是锁的粒度下降到Node级别了,竞争会比较小,这个时候synchronized的性能要优于ReentrantLock.
- 为什么wait必须是在同步块中的呢?@重看了一遍王宝令的课程,发现这是MESA管程模型的设计范式,硬要解释的话可以是这样:
- wait是跟notify, notifyAll配对的, 是和synchronized关键字一起使用的
- wait的工作原理就是wait的时候,会进入同步块(synchronized)所对应的条件等待队列,在其他地方使用这个关键字是不可进入的
- 或者说wait所对应的管程的入口在synchronied处
- wait与sleep区别是什么? @见上面的笔记