1 JAVA高级部分
1.1 JUC
- Synchronized用过吗,其原理是什么?
- 你刚才提到获取对象的锁,这个“锁”到底是什么?如何确定对象的锁?
- 什么是可重入性,为什么说Synchronized是可重入锁?
- JVM对Java的原生锁做了哪些优化?
- 为什么说Synchronized是非公平锁?
- 什么是锁清除和锁粗化?
- 为什么说Synchronzed是一个悲观锁?乐观锁的实例原理又是什么?什么是CAS?
- 跟Synchronzed相比,可重入锁ReentrantLock其实现原理有什么不同?
- 谈谈AQS框架是怎么回事?
- 请尽可能详尽的对比下Synchronized和ReentrantLock的异同?
- ReentrantLock是如何实现可重入性的?
1.2 可重入锁
可重入锁又名递归锁
是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提,锁对象得是同一个对象),
不会因为之前已经获取过还没释放而阻塞。
Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。
“可重入锁”这四个字分开来解释:
可:可以。
重:再次。
入:进入 进入同步域(即同步代码块/方法或显式锁锁定的代码)
锁:同步锁
一个线程中的多个流程可以获取同一把锁,持有这把同步锁可以再次进入。自己可以获取自己的内部锁
1.2.1 可重入锁种类
隐式锁(即synchronized关键字使用的锁)默认是可重入锁
同步块
/**
* 可重入锁:可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁,这样的锁就叫做可重入锁。
*
* 在一个synchronized修饰的方法或代码块的内部
* 调用本类的其他synchronized修饰的方法或代码块时,是永远可以得到锁的
*/
public class ReEnterLockDemo {
static final Object objectLockA = new Object();
public static void m1() {
new Thread(() -> {
synchronized (objectLockA) {
System.out.println(Thread.currentThread().getName() + "\t" + "------外层调用");
synchronized (objectLockA) {
System.out.println(Thread.currentThread().getName() + "\t" + "------中层调用");
synchronized (objectLockA) {
System.out.println(Thread.currentThread().getName() + "\t" + "------内层调用");
}
}
}
}, "t1").start();
}
public static void main(String[] args) {
m1();
}
}
同步方法
/**
* 可重入锁:可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁,这样的锁就叫做可重入锁。
*
* 在一个synchronized修饰的方法或代码块的内部
* 调用本类的其他synchronized修饰的方法或代码块时,是永远可以得到锁的
*/
public class ReEnterLockDemo {
public synchronized void m1(){
System.out.println("=====外层");
m2();
}
public synchronized void m2() {
System.out.println("=====中层");
m3();
}
public synchronized void m3(){
System.out.println("=====内层");
}
public static void main(String[] args) {
new ReEnterLockDemo().m1();
}
}
1.2.2 Synchronized的重入的实现机理
每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针。
当执行monitorenter时,如果目标锋对象的计数器为零,那么说明它没有被其他线程所持有,Java虚拟机会将该锁对象的持有线程设置为当前线程,并且将其计数器加i。
在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程是当前线程,那么Java虚拟机可以将其计数器加1,否则需要等待,直至持有线程释放该锁。
当执行monitorexit时,Java虚拟机则需将锁对象的计数器减1。计数器为零代表锁已被释放。
1.2.3 显式锁(即Lock)也有ReentrantLock这样的可重入锁。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* 可重入锁:可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁,这样的锁就叫做可重入锁。
*
* 在一个synchronized修饰的方法或代码块的内部
* 调用本类的其他synchronized修饰的方法或代码块时,是永远可以得到锁的
*/
public class ReEnterLockDemo {
static Lock lock = new ReentrantLock();
public static void main(String[] args) {
new Thread(() -> {
lock.lock();
//lock.lock();
try {
System.out.println("=======外层");
lock.lock();
try {
System.out.println("=======内层");
} finally {
lock.unlock();
}
} finally {
//实现加锁次数和释放次数不一样
//由于加锁次数和释放次数不一样,第二个线程始终无法获取到锁,导致一直在等待。
lock.unlock();
//lock.unlock(); //正在情况,加锁几次就要解锁几次
}
}, "t1").start();
new Thread(() -> {
lock.lock();
try {
System.out.println("b thread----外层调用lock");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}, "b").start();
}
}
1.3 LockSupport
1.3.1 LockSupport是什么
https://www.matools.com/api/java8
LockSupport是用来创建锁和其他同步类的基本线程阻塞原语。
下面这句话,后面详细说
LockSupport中的park()和unpark()的作用分别是阻塞线程和解除阻塞线程
1.3.2 从阿里蚂蚁金服面试题讲起
InterruptedException你说说
1.3.3 线程等待唤醒机制(wait/notify)
3种让线程等待和唤醒的方法
方式1: 使用Object中的wait()方法让线程等待, 使用Object中的notify()方法唤醒线程
方式2: 使用JUC包中Condition的await()方法让线程等待,使用signal()方法唤醒线程
方式3: LockSupport类可以阻塞当前线程以及唤醒指定被阻塞的线程
Object类中的wait和notify方法实现线程等待和唤醒
private static void synchronizedWaitNotify() {
new Thread(() -> {
synchronized (objectLock){
System.out.println(Thread.currentThread().getName()+"\t"+"------come in");
try {
objectLock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"\t"+"------被唤醒");
}
},"A").start();
new Thread(() -> {
synchronized (objectLock)
{
objectLock.notify();
System.out.println(Thread.currentThread().getName()+"\t"+"------通知");
}
},"B").start();
}
异常1
/**
* 要求: t1线程等待3秒钟,3秒钟后t2线程唤醒t1线程继续工作
* 以下异常情况:
* 2 wait方法和notify方法,两个都去掉同步代码块后看运行效果
* 2.1 异常惰况
* Exception in thread "t1" java.Lang.ILlegalLNonitorStateException at java.lang.Object.wait(Native Method)
* Exception in thread "t2" java.lang.ILlegalWonitorStateException at java.lang.Object.notify(Native Method)
*
* 2.2 结论
* Object类中的wait、notify、notifyALlL用于线程等待和唤醒的方法,都必须在synchronized内部执行(必须用到关键字synchronized)
*
*/
public class LockSupportDemo {
static final Object objectLock = new Object();
public static void main(String[] args) {
new Thread(() -> {
synchronized (objectLock) {
System.out.println(Thread.currentThread().getName() + "\t" + "------come in");
try {
objectLock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "\t" + "------被唤醒");
}
}, "A").start();
new Thread(() -> {
synchronized (objectLock) {
objectLock.notify();
System.out.println(Thread.currentThread().getName() + "\t" + "------通知");
}
}, "B").start();
}
}
wait方法和notify方法,两个都去掉同步代码块,异常情况
异常2
/**
* 要求: t1线程等待3秒钟,3秒钟后t2线程唤醒t1线程继续工作
*
* 3 将notify放在wait方法前先执行,t1先notify 了,3秒钟后t2线程再执行wait方法
* 3.1程序一直无法结柬
* 3.2结论
* 先wait后notify、notifyall方法,等待中的线程才会被唤醒,否则无法唤醒
*
*/
public class LockSupportDemo {
static Object objectLock = new Object();
public static void main(String[] args) {
new Thread(() -> {
synchronized (objectLock){
System.out.println(Thread.currentThread().getName()+"\t"+"------come in");
try {
objectLock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"\t"+"------被唤醒");
}
},"A").start();
new Thread(() -> {
synchronized (objectLock)
{
objectLock.notify();
System.out.println(Thread.currentThread().getName()+"\t"+"------通知");
}
},"B").start();
}
}
将notify放在wait方法前面,程序无法执行,无法唤醒
总结
wait和notify方法必须要在同步块或者方法里面且成对出现使用
先wait后notify才OK
Condition接口中的await后signal方法实现线程的等待和唤醒
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* 要求: t1线程等待3秒钟,3秒钟后t2线程唤醒t1线程继续工作
*
* 3 将notify放在wait方法前先执行,t1先notify 了,3秒钟后t2线程再执行wait方法
* 3.1程序一直无法结柬
* 3.2结论
* 先wait后notify、notifyall方法,等待中的线程才会被唤醒,否则无法唤醒
*
*/
public class LockSupportDemo {
static Lock lock = new ReentrantLock();
static Condition condition = lock.newCondition();
public static void main(String[] args) {
new Thread(() -> {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "\t" + "------come in");
try {
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "\t" + "------被唤醒");
} finally {
lock.unlock();
}
}, "A").start();
new Thread(() -> {
lock.lock();
try {
condition.signal();
System.out.println(Thread.currentThread().getName() + "\t" + "------通知");
} finally {
lock.unlock();
}
}, "B").start();
}
}
传统的synchronized和Lock实现等待唤醒通知的约束
线程先要获得并持有锁,必须在锁块(synchronized或lock)中
必须要先等待后唤醒,线程才能够被唤醒
LockSupport类中的park等待和unpark唤醒
通过park()和unpark(thread)方法来实现阻塞和唤醒线程的操作
compact1, compact2, compact3
java.util.concurrent.locks
Class LockSupport
java.lang.Object
java.util.concurrent.locks.LockSupport
public class LockSupport
extends Object
用于创建锁和其他同步类的基本线程阻塞原语。
这个类与每个使用它的线程相关联,一个许可证(在Semaphore类的意义上)。 如果许可证可用,则呼叫park将park返回,在此过程中消耗它; 否则可能会阻止。 致电unpark使许可证可用,如果尚不可用。 (与信号量不同,许可证不能累积,最多只有一个。)
方法park和unpark提供了阻止和解除阻塞线程的有效手段,该方法不会遇到导致不推荐使用的方法Thread.suspend和Thread.resume目的不能使用的问题:一个线程调用park和另一个线程之间的尝试unpark线程将保持活跃性,由于许可证。 另外,如果调用者的线程被中断, park将返回,并且支持超时版本。 park方法也可以在任何其他时间返回,因为“无理由”,因此一般必须在返回之前重新检查条件的循环中被调用。 在这个意义上, park作为一个“忙碌等待”的优化,不浪费时间旋转,但必须与unpark配对才能有效。
park的三种形式也支持blocker对象参数。 线程被阻止时记录此对象,以允许监视和诊断工具识别线程被阻止的原因。 (此类工具可以使用方法getBlocker(Thread)访问阻止程序 。)强烈鼓励使用这些形式而不是没有此参数的原始形式。 在锁实现中作为blocker提供的正常参数是this 。
这些方法被设计为用作创建更高级同步实用程序的工具,并且本身对于大多数并发控制应用程序本身并不有用。 park方法仅用于形式的构造:
while (!canProceed()) { ... LockSupport.park(this); }
其中既不canProceed也没有任何其他动作之前的呼叫park需要锁定或阻止。 因为只有一个许可证与每个线程相关联, park任何中介使用可能会干扰其预期效果。
LockSupport类使用了一种名为Permit(许可)的概念来做到阻塞和唤醒线程的功能,每个线程都有一个许可(permit),permit只有两个值1和零,默认是零。
可以把许可看成是一种(0,1)信号量(Semaphore),但与Semaphore不同的是,许可的累加上限是1。
主要方法
Modifier and Type Method and Description
static Object getBlocker(Thread t)
返回提供给最近调用尚未解除阻塞的park方法的阻止程序对象,如果不阻止则返回null。
static void park()
禁止当前线程进行线程调度,除非许可证可用。
static void park(Object blocker)
禁止当前线程进行线程调度,除非许可证可用。
static void parkNanos(long nanos)
禁用当前线程进行线程调度,直到指定的等待时间,除非许可证可用。
static void parkNanos(Object blocker, long nanos)
禁用当前线程进行线程调度,直到指定的等待时间,除非许可证可用。
static void parkUntil(long deadline)
禁用当前线程进行线程调度,直到指定的截止日期,除非许可证可用。
static void parkUntil(Object blocker, long deadline)
禁用当前线程进行线程调度,直到指定的截止日期,除非许可证可用。
static void unpark(Thread thread)
为给定的线程提供许可证(如果尚未提供)。
阻塞
park()/park(Object blocker)
permit默认是O,所以一开始调用park()方法,当前线程就会阻塞,直到别的线程将当前线程的permit设置为1时,park方法会被唤醒,然后会将permit再次设置为O并返回。
阻塞当前线程/阻塞传入的具体线程
唤醒
unpark(Thread thread)
调用unpark(thread)方法后,就会将thread线程的许可permit设置成1(注意多次调用unpark方法,不会累加,permit值还是1)会自动唤醒thread线程,
即之前阻塞中的LockSupport.park()方法会立即返回。
唤醒处于阻断状态的指定线程
code
正常+无锁块要求
/**
* LockSupport:俗称 锁中断
* 以前的两种方式:
* 1.以前的等待唤醒通知机制必须synchronized里面有一个wait和notify
* 2.lock里面有await和signal
* 这上面这两个都必须要持有锁才能干,
* LockSupport它的解决的痛点
* 1。LockSupport不用持有锁块,不用加锁,程序性能好,
* 2。先后顺序,不容易导致卡死
*/
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "\t ----begi" + System.currentTimeMillis());
//阻塞当前线程
LockSupport.park();
System.out.println(Thread.currentThread().getName() + "\t ----被唤醒" + System.currentTimeMillis());
}, "t1");
t1.start();
LockSupport.unpark(t1);
System.out.println(Thread.currentThread().getName() + "\t 通知t1...");
}
之前错误的先唤醒后等待,LockSupport照样支持
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(5L);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "\t ----begi" + System.currentTimeMillis());
//阻塞当前线程
LockSupport.park();
System.out.println(Thread.currentThread().getName() + "\t ----被唤醒" + System.currentTimeMillis());
}, "t1");
t1.start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (Exception e) {
e.printStackTrace();
}
LockSupport.unpark(t1);
System.out.println(Thread.currentThread().getName() + "\t 通知t1...");
}
main 通知t1...
//sleep方法3s后醒来,执行park无效,但是没有阻塞效果
t1 ----begi1611556466865
//先执行了unpark(t1)导致上面的park方法形同虚设无效,时间一样
t1 ----被唤醒1611556466865
重点说明(重要)
LockSupport是用来创建锁和其他同步类的基本线程阻塞原语
LockSupport是一个线程阻塞工具类,所有的方法都是静态方法,可以让线程在任意位置阻塞,阻塞之后也有对应的唤醒方法。归根结底,LockSupport调用的Unsafe中的native代码。
LockSupport提供park()和unpark()方法实现阻塞线程和解除线程阻塞的过程
LockSupport和每个使用它的线程都有一个许可(permit)关联。permit相当于1,0的开关,默认是0,
调用一次unpark就加1变成1,
调用一次park会消费permit,也就是将1变成0,同时park立即返回。
如再次调用park会变成阻塞(因为permit为零了会阻塞在这里,一直到permit变为1),这时调用unpark会把permit置为1。
每个线程都有一个相关的permit, permit最多只有一个,重复调用unpark也不会积累凭证。
形象的理解
线程阻塞需要消耗凭证(permit),这个凭证最多只有1个。
当调用park方法时
*如果有凭证,则会直接消耗掉这个凭证然后正常退出;
*如果无凭证,就必须阻塞等待凭证可用;
而unpark则相反,它会增加一个凭证,但凭证最多只能有1个,累加无效。
面试题
为什么可以先唤醒线程后阻塞线程?
因为unpark获得了一个凭证,之后再调用park方法,就可以名正言顺的凭证消费,故不会阻塞。
为什么唤醒两次后阻塞两次,但最终结果还会阻塞线程?
因为凭证的数量最多为1,连续调用两次unpark和调用一次unpark效果一样,只会增加一个凭证;
而调用两次park却需要消费两个凭证,证不够,不能放行。