创建一个线程,直接设置为守护线程即可
t.setDeamon(true) //讲当前t线程设置为守护线程!
例如:垃圾回收线程就是是一个守护线程!
线程的状态
关于状态有两种说法,一种是说五种状态、一种是六种状态,分别说明!
五种状态:
**六种状态:**根据Thread. State枚举划分(更偏向Java)
-
NEW(初始):线程被创建后尚未启动。(new 出来线程,未start())
-
RUNNABLE(运行):包括了操作系统线程状态中的Running和Ready,也就是处于此状态的线程可能正在运行,也可能正在等待系资源,如等待CPU为它分配时间片。(在这种状态分类情况加,获取键盘的读入,被看作为可运行状态)
-
BLOCKED(阻塞):线程阻塞于锁。(等待t1线程结束,但是t1一直不结束)
-
WAITING(等待):线程需要等待其他线程做出一些特定动作(通知或中断)。(线程同步机制,等待另一个线程归还对象锁!)
-
TIME_WAITING(超时等待):该状态不同于WAITING,它可以在指定的时间内自行返回。(sleep、wait等状态,有时间限制的休眠)
-
TERMINATED(终止):该线程已经执行完毕。
管程就是Monitor锁
线程安全问题产生的原因
举个例子:线程t1、t2 对共享的静态变量i,分别执行i ++
, i --
操作,由于并发会产生一下两种情况,导致线程静态变量 I不正确
-
I ++ 的原代码被字节码化后,代码分为4个步骤:获取 i、准备常量1、自增、写入自增后的值
-
I – 的原代码被字节码化后,代码分为4个步骤:获取 i、准备常量1、自减、写入自减后的值
因此当我们两个线程并发执行的时候,如果在写入值之前,发生上下文切换,(指令交错)则会导入如下两种情况:
情况一:
t1、t2并发执行,结果出现负数
情况二:
t1、t2并发执行,结果出现正数
结论:多个线程对某个共享资源进行读操作没问题,写操作则会产生线程安全的问题,
临界区
线程中我们把对共享资源修改的区域,称这块代码块为临界区
public class ThreadTest {
static int count = 0 ;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
//临界区
{
count ++ ;
}
});
Thread t2 = new Thread(() -> {
{ //临界区
count – ;
}
});
t1.start(); t2.start();
t1.join(); t2.join();
}
}
竞态条件
多个线程在临界区内执行,由于执行顺序不同,导致结果无法预测,称之为发生了竞态条件!
synchronized解决方案
1、保证静态变量的安全:
public class ThreadTest {
static int count = 0 ;
public static void main(String[] args) throws InterruptedException {
Object obj = new Object();
Thread t1 = new Thread(() -> {
synchronized(obj){
count ++ ;
}
});
Thread t2 = new Thread(() -> {
synchronized (obj){
count – ;
}
});
t1.start(); t2.start();
t1.join(); t2.join();
}
}
2、保证实例变量的安全:
public class ThreadTest {
public static void main(String[] args) throws InterruptedException {
Room room = new Room();
Thread t1 = new Thread(() -> {
room.a();
}) ;
Thread t2 = new Thread(() -> {
room.b();
}) ;
Thread t3 = new Thread(() -> {
room.c();
}) ;
}
}
class Room{
public synchronized void a(){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(“a.begin…”);
}
public synchronized void b(){
System.out.println(“b.begin…”);
}
public void c(){
System.out.println(“c.begin…”);
}
}
//结果:
/*
-
c b 1s a
-
c 1s a b
-
b c 1s a
-
*/
具体参考“线程八锁”
变量的线程安全问题
1、成员变量(实例变量、静态变量)
-
如果成员变量,没有在线程间去共享,那没是线程安全的!
-
如果成员变量,在线程间被共享:
-
如果只有读操作,安全
-
如果有写操作,不安全
2、局部变量
局部变量并不会产生线程安全问题
每个线程中的栈是私有的!方法入栈后创建的局部变量,其他线程访问不到!
常见的线程安全类
-
String
-
Integer
-
String Buffer
-
Random
-
Vector
-
HashTable
-
java.util.concurrent(JUC) 包下的类
线程安全的类其中的方法是线程安全的!
但是线程安全的方法,组合起来也许会产生线程不安全的情况
例如:Hashtable
Hashtable table = new Hashtable( ) ; //原子性,只能在单一方法中保证
if (table.get(“Key”) == null) {
table.put(“key”) ;
}
我们的get、put方法都是被synchronized修饰的线程安全的方法,但是上述代码依然可能会产生线程不安全的情况
流程:线程 t1 get到key == null ,此时线程t1 执行get方法完毕,此时线程 t2 获得时间片执行 get 同样返回null,然后接着发生上下文切换,t1执行put方法,放入<k1,v1>然后经过上下文切换t2也回到put阶段,线程2中的的此时得到的状态仍然是key == null ,因此重复添加<k1,v2>,发生覆盖!
不可变的线程安全类
例如:String、Integer 他们的值是不能被修改的,实例即使被共享,也不会有线程安全的问题
Java对象头
每一个对象在JVM当中都被分为对象头和内容:(如下是32位的JVM)
例如Integer : 对象头占8个字节,内容为int类型占4个字节 ;
Klass部分保存的是对象的类型
Mark Word 保存的是的如下:(由于对象是否加锁,而不同!)
-
Normal : 不加锁 01
-
Biased:偏向锁 01
-
Lightweight Locked : 轻量锁 00
-
Heavyweight : 重量级锁 10
-
Marked for GC :垃圾回收锁
Monitor锁
Monitor被翻译为监视器或管程
- Monitor的结构图,Monitor是重量级锁!
Monitor的工作原理 *
Monitor和obj的关联流程
详细的流程:
-
当我们的obj对象被加上了synchronized锁之后,我们obj的对象头就会转化为一个指针指向Monitor(关联起来) ;
-
当我们的线程去访问临界区代码的时候,回将线程Thread1设置为Monitor的所有者!(获取对象锁)
-
当新的线程2、线程3 去访问临界区代码时,则会判断obj的对象锁是否有主人,如果有那么就会在EntryList阻塞等待,等待线程1执行完临界区代码块的内容归还锁,接着线程1归还锁后, 线程2、线程3再去争夺对象锁的所有权!
总结 : 我们所说的对象锁,也即是这个Monitor ;
Synchronized优化原理 *
由于Monitor锁每次都需要与操作系统打交道,效率较低,因此我们需要对锁进行优化!如轻量级锁、偏向锁
1、轻量级锁
如果多个线程访问一个一个对象,但是这个访问时间隔开的,不是并发(没有竞争关系),那么就可以使用轻量级锁来优化
轻量级锁对使用者是透明的,依然可以使用synchronized
//测试代码:
static final Object obj = new Object();
public static void method1(){
synchronized(obj){
method2();
}
}
public static void method2(){
synchronized(obj){
}
}
加锁(为对象添加轻量级锁)
- Thread-0调用method1,方法入栈,执行synchronized代码块,然后创建一个锁的记录对象Lock Record,
- 让锁记录中的reference指向我们的对象,并尝试用cas替换Object中的Mark word部分与锁地址交换
- 如果cas(原子性操作)替换成功!对象头中保存了,lock record和状态00,表示该线程为对象加锁!
如果cas失败,有两种情况:
-
如果其他线程已经持有了该Object的轻量级锁,这表明有竞争,进入锁膨胀过程!
-
如果是当前线程自己执行了锁重入(重复加锁),那么再加入一条Lock Record锁记录作为重入的计数!
解锁(为对象接触,轻量级锁)
- 当退出我们的synchronized代码块的时候,解锁时,如果有Lock Record锁记录为null,直接移除,表示锁重入记录-1
当退出我们的synchronized的时候,锁记录的值不为null,此时使用cas将Mark word的值恢复给对象头,
-
成功:解锁成功!
-
失败:说明轻量级锁进行了锁膨胀,或者已经升级为重量级锁,这时进入重量级解锁流程!
2、锁膨胀
当退出我们的synchronized的时候,此时使用cas将Mark word的值恢复给对象头失败 :说明轻量级锁进行了锁膨胀,或者已经升级为重量级锁,这时进入重量级解锁流程!
static final Object obj = new Object();
public static void method1(){
synchronized(obj){
method2();
}
}
- 当我们的的Thread-1为obj加轻量级锁发现,obj已经被Thread-01加上轻量级锁了
这是Thread-1加轻量级锁失败,进入锁膨胀流程
-
即为Object对象申请Monitor锁,让Object指向重量级锁地址 ;
-
然后自己进入重量级锁中的EntryList中,等待!
此时当Thread-0执行完同步代码块,使用cas将Mark word的值恢复给对象头失败,进入重量级锁的解锁流程,即按照Monitor的地址找到Monitor对象,然后将其Owner设置为null,解锁成功,然后唤醒EntryList中的阻塞线程;执行重量级锁的加锁流程!
3、自旋优化
重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。
(多核CPU自旋才有意义)
- 自旋成功:
- 自旋失败:
-
在Java6之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。
-
自旋会占用CPU时间,单核CPU自旋就是浪费,多核CPU自旋才能发挥优势。
-
Java 7之后不能控制是否开启自旋功能
4、偏向锁
当我们的轻量级锁重入时,效率较低,因为每次重入都需要执行一次cas操作,让Lock Record中的地址与Object对象中的Mark Work比较一次,适合:就一个线程访问
Java 6中引入了偏向锁来做进一步优化:只有第一次使用CAS将线程ID设置到对象的 Mark Word头,之后发现这个线程D是自己的就表示没有竞争,不用重新CAS。以后只要不发生竞争,这个对象就归该线程所有
回顾一下64位JVM中的对象
创建对象时:
-
如果开启了偏向锁(默认开启),那么对象创建后,markword值为Ox05即最后3位为101,这时它日thread、epoch、age都为0
-
偏向锁是默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加VM参数-XX : BiasedLockingStartupDelay=e来禁用延迟
-
如果没有开启偏向锁,那么对象创建后,markword值为Ox01即最后3位为001,这时它的hashcode、age都为0,第一次用到hashcode 时才会赋值
-
前54位表示的偏向锁的ThreadID ;
-
偏向锁解锁后,线程ID依然会在Mark word中,直到下一个
-
在上面测试代码运行时在添加VM参数-xx:-UseBiasedLocking 禁用偏向锁
-
当一个可偏向的对象调用其本身hashcode方法,会禁止偏向锁 【 因为我们加了偏向锁的对象没有空间去保存32位的hashcode】
-
其他线程访问,也会撤销偏向锁 【对象加偏向锁就是只适用于这个对象只有某一个线程访问】
批量重偏向
当我们的对象上的偏向锁,被撤销20次后,不会再变为轻量级锁,而是以后的对象上的偏向锁,全部偏向一个新的线程 ;
超过40次,我们的对象上的偏向锁,变为不可偏向 ;
总结 :
重量级锁:适合多个线程访问,支持并发 ;缺点:与操作系统交互,效率变低
轻量级锁 : 适合多个线程访问,但是多个线程时错开访问的 ;缺点:一个线程多次为一个对象加锁,锁重入 ;
偏向锁:适合单一线程访问 ,目的是解决锁重入 ; 缺点:只能单一线程访问
5、锁消除
JVM存在一个JTL即时编译 ,由于这个机制会对代码优化,默认锁消除的这个优化机制是开启的,可以手动取消!
//测试代码 :
static int x ;
public static void method1(){
x ++ ;
}
public static void method2(){
Object o = new Object(); //局部对象,不被共享,这个锁相当于每加! JTL会直接将其优化掉!
synchronized (o){
x ++ ;
}
}
结论:我们的两个方法执行效率是差不多一样的
Wait和Notify *
原理:
-
Owner线程发现条件不满足,调用wait方法,即可进入WaitSet变为WAITING 状态
-
BLOCKED和WAITING的线程都处于阻塞状态,不占用CPU时间片
-
BLOCKED线程会在Owner线程释放锁时唤醒
-
WAITNG线程会在Owner线程调用notify或notifyAll 时唤醒,但唤醒后并不意味者立刻获得锁,仍需入EntryList重新竞争
wait : 线程拿到锁的使用权,但是放弃了,进入waiting区 ;
notify : 唤醒正在waiting中的某个线程,到EntryList中准备与其他阻塞中的线程去争抢时间片!(随机挑一个唤醒)
notifyAll : 唤醒waiting中的所有正在等待唤醒的线程
Wait方法重载:
public final void wait() throws InterruptedException { //无限期等待
wait(0);
}
public final native void wait(long timeout) throws InterruptedException; //设置等待时长,一段时间后会自动唤醒
public final void wait(long timeout, int nanos) throws InterruptedException
Wait和Notify的使用
先了解一下Wait和Sleep的区别
Wait和Sleep的区别
-
Sleep是Thread的方法,Wait是Object的方法 ;
-
Sleep可以在任何时候使用,Wait与Synchronized联用 ;
-
Sleep不会释放对象锁,wait会释放对象锁 ;
总结 :
synchronized(lock){
while(条件不成立){
lock.wait() ;
}
//满足条件,干活
}
synchronized(lock){
lock.notifyAll() ; //使用notify可能会产生虚假唤醒!
}
设计模式—保护性暂停模式
一个结果需要从一个线程传到另外一个线程,让他们关联同一个GuardedObject (保护对象);
-
如果结果不断地从一个线程到另外一个线程 ;那么可以使用消息队列 ;
-
JDK中join得实现、Future的实现都是这种模式 ;
-
因为要等待另一方的结果,所有归结到同步模式
总结:在两个线程之间通过一个共享对象,线程1通过该共享对象保存当前线程中需要保存的结果 ;线程2则可以通过该对象获取到这个结果 ;
-
保护性暂停模式:一个线程等待另外一个线程的返回结果 ;
-
join:一个线程等待另外一个线程执行完毕 ;
扩展:可以在获取结果的时候添加超时 , 一旦超时,即使没有获取的结果依然结束等待 ;
设计模式—生产者消费者模式
异步的原因:生产者产生的结果不会被立刻调用 ;
-
与前面的保护性暂停中的GuardedObject 不同,不需要产生结果和消费结果的线程一一对应
-
消费队列可以用来平衡生产和消费的线程资源
-
生产者仅负责产生结果数据,不关心数据该如何处理,而消费者专心处理结果
-
数据消息队列是有容量限制的,满时不会再加入数据,空时不会再消耗数据
-
JDK中各种阻塞队列,采用的就是这种模式
Park和unpark
类似与wait和notify ,但是park休眠的可以使用unpark提前唤醒
public class ParkandUnpark {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
System.out.println(“t1线程开始”);
System.out.println(“t1线程暂停”);
LockSupport.park();
System.out.println(“t1线程结束”);
},“t1”);
t1.start();
Thread.sleep(2000);
System.out.println(“t1线程恢复”);
LockSupport.unpark(t1);
}
}
与Object的 wait & notify 相比
-
wait,notify和notifyAll必须配合Object Monitor一起使用,而park,unpark不必
-
park & unpark是以线程为单位来【阻塞】和【唤醒】线程,而notify只能随机唤醒一个等待线程,notifyAll是唤醒所有等待线程,就不那么【精确】
-
park & unpark可以先unpark,而wait & notify不能先notify
park和unpark分析图:
总结:我们每个线程都可以比作一个汽车,每个汽车都含有一个park对象,包括counter(油的量),condition(停车、不停车),mutex
当我们的线程执行park
的时候,先判断油量是否充足,如果充足,汽车不会停车 ;如果不充足则停车休息 ;
当我们的unaprk
执行的时候,就是给汽车加油(有没有油都加一下)也就是counter = 1,加过油后,汽车发现有油了,汽车启动 ;
重新理解线程的状态 *
以偏java的6种线程状态理解 Thread. Sate
假设有线程 Thread t ;
情况一:NEW — > RUNNABLE
当调用t .start方法的时候,由NEW 到RUNNABLE ;
情况二: RUNNABLE <—> WAITING *
1、线程t 获得obj的锁后调用wait ,由RUNNABLE 到 WAITING ;
然后再调用notifyALL 、notify 、interrupt ,唤醒线程,放入EntryList中竞争:
-
竞
争成功:WAITING 到RUNNABLE ; -
竞争失败:WAITING 到BLOCK ;
2、当前线程main调用t.join ,则当前线程由RUNNABLE 到 WAITING ;
当t线程结束,或者调用当前线程的interrupt 会由RUNNABLE 到 WAITING ;
情况三:RUNNABLE <—> TIMED_WAITING
1、调用wait(long time),比情况2的方法1多一个超时时间 ;
2、调用t.join(long time) , 比情况2的方法2多一个超时时间 ;
3、调用Thread.sleep(long time)
情况四:RUNNABLE <—> BLOCKED
当线程执行到synchronized(obj)的时候,发现obj的owner已经有线程存在了,那么会由RUNNABLE 到BLOCKED ;
当占有obj锁的线程执行完毕后,当前线程争夺到CPU时间片就会再由BLOCKED 到 RUNNABLE
情况五:RUNNABLE <—>TERMINTED
线程中的代码执行完毕 ;
死锁 *
死锁现象
一个线程需要获得多个对象锁 ,容易发生死锁现象
package com.juc;
public class DeadLockTest {
public static void main(String[] args) {
final Object lock2 = new Object() ;
final Object lock1 = new Object() ;
new Thread(() -> {
synchronized (lock1){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+“获得lock1”);
synchronized (lock2){
System.out.println(Thread.currentThread().getName()+“获得lock2”);
}
}
},“t1”).start();
new Thread(() -> {
synchronized (lock2){
System.out.println(Thread.currentThread().getName()+“获得lock2”);
synchronized (lock1){
System.out.println(Thread.currentThread().getName()+“获得lock1”);
}
}
},“t2”).start();
}
}
//测试结果:
//t2获得lock2
//t1获得lock1 发生死锁现象
可以使用 : 顺序加锁的方式解决! 线程1 线程2获取锁的顺序保持一致!例如:都是现加lock1再加lock2
定位死锁
方法一:jps + jstack + 进程号
E:\JavaSE笔记>jps //查看java所有的进程 ;
11760
7028 Launcher
17832 Jps
18168 RemoteMavenServer
14508 DeadLockTest
E:\JavaSE笔记>jstack 14508 查看某个进程中线程的详细信息;
打印出死锁的信息 :
方法二:jconsole
win + R : 输入jconsole
以上两种方法,及时的定位到死锁的位置 ;
哲学家就餐问题
有五位哲学家,围坐在圆桌旁。
-
他们只做两件事,思考和吃饭,思考一会吃口饭,吃完饭后接着思考。
-
吃饭时要用两根筷子吃,桌上共有5根筷子,每位哲学家左右手边各有一根筷子。
-
·如果筷子被身边的人拿着,自己就得等待
活锁
两个线程互相改变对方条件,都无法执行完成 !
package com.juc;
public class LiveLockTest {
static volatile int count = 10 ;
public static void main(String[] args) {
new Thread(() -> {
while (count > 0){
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
count-- ;
System.out.println("count= "+ count);
}
},“t1”).start();
new Thread(() -> {
while (count < 20){
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
count++ ;
System.out.println("count= "+ count);
}
},“t2”).start();
}
}
//两个线程互相改变对方条件,都无法执行完成 !
//测试结果 :
count= 10
count= 10
count= 11
count= 11
count= 10
饥饿
很多教程中把饥饿定义为,一个线程由于优先级太低,始终得不到CPU调度执行,也不能够结束
饥饿的情况不易演示,讲读写锁时会涉及饥饿问题
例如:我们的哲学家问题中,我们如果顺序加锁去解决死锁,会发现其中一个哲学家一直拿不到筷子,这个现象就是饥饿现象
如何解决 饥饿、死锁、活锁 这些问题 ?
答 : ReentrantLock
ReentrantLock *
可重入锁 , 位于java.util.concurrent 包下
相对synchronized 它具备以下特点 :
-
可以中断 : 线程一拿到对象锁,线程二可以打断
-
可以设置超时时间 : 线程阻塞一段时间仍未获取对象锁,放弃多锁的获取 ;
-
可以设置为公平锁 : 线程先到先得,而非按优先级分配
-
支持多个条件变量 : 可以有多个WaitSet ,不需要全部唤醒
与synchronized一样支持锁重入
1、基本语法
ReentrantLock reentrantLock = new ReentrantLock();
reentrantLock.lock(); //获取锁
try {
//临界区
}finally {
reentrantLock.unlock(); //释放锁
}
2、可重入
当一个线程已经是锁的主人的时候,还可以再次去获取锁的owner,重复获取!
package com.juc;
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockTest {
private static ReentrantLock reentrantLock = new ReentrantLock();
public static void main(String[] args) {
reentrantLock.lock(); //当前线程获取到锁,称为lock的主人 ;
try{
System.out.println(“进入了main方法”);
m1();
}finally {
reentrantLock.unlock();
}
}
public static void m1(){
reentrantLock.lock(); //锁重入
try{
System.out.println(“进入了m1方法”);
m2();
}finally {
reentrantLock.unlock();
}
}
public static void m2(){
reentrantLock.lock(); //锁重入
try{
System.out.println(“进入了m1方法”);
}finally {
reentrantLock.unlock();
}
}
}
3、可打断
必须是加的lockInterruptibly()可被打断锁,其他线程使用Interrupt可以进行打断操作!
package com.juc;
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockTest02 {
private static ReentrantLock reentrantLock = new ReentrantLock() ;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
try {
reentrantLock.lockInterruptibly(); //线程加入可打断锁!
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println(“t1线程被打断”);
}
try{ //临界区
System.out.println(“t1线程没有打断”);
}finally {
reentrantLock.unlock();
}
},“t1”);
reentrantLock.lock();
Thread.sleep(1000);
t1.interrupt();
}
}
4、锁超时
目的还是不让线程长时间陷入阻塞状态,如果等待一段时间t1线程仍然不释放锁,t2则不再等待 ;
package com.juc;
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockTest02 {
private static ReentrantLock reentrantLock = new ReentrantLock() ;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
if (reentrantLock.tryLock()){ //t1线程尝试获取锁
try{
System.out.println(“获取到锁”);
}finally {
reentrantLock.unlock();
}
}
System.out.println(“获取不到锁”);
},“t1”);
reentrantLock.lock(); //主线程占用lock锁
t1.start();
}
} //获取不到锁
5、公平锁
当我们多个线程竞争的时候,会按照先到先得的原则,而非再去争抢 ;
默认我们的ReentrantLock是不开启公平锁的,因为会降低并发 ;
6、条件变量
synchronized 中也有条件变量,就是我们讲原理时那个waitSet休息室,当条件不满足时进入waitSet等待ReentrantLock的条件变量比synchronized 强大之处在于,它是支持多个条件变量的,这就好比
-
synchronized是那些不满足条件的线程都在一间休息室等消息
-
而ReentrantLock支持多间休息室,有专门等烟的休息室、专门等早餐的休息室、唤醒时也是按休息室来唤醒
public class ReentrantLockTest03 {
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
Condition condition01 = lock.newCondition(); //创建两个休息室,类似WaitSet
Condition condition02 = lock.newCondition();
lock.lock();
condition01.await(); //让线程去condition01休息室休息
condition01.signal(); //唤醒condition01休息室中的线程
}
}
ReentrantLock与synchronized的区别:
-
ReentrantLock 是给锁实例加锁!而synchronized则是以关键字的形式给所有对象加锁!
-
可以使用ReentrantLock,解决死锁问题 ;
-
ReentrantLock 需要在finally当中手动释放锁 ,而synchronized则是代码块结束,释放锁 ;
交替输出的实现 *
-
使用wait、notify 实现
-
使用park、unpark 实现