JUC笔记
JUC 笔记
一.基础
并发和并行
并发:是在同一个实体上的多个事件,是在一台处理器上“同时”处理多个任务,同一时刻只有一个事件发生
并行:是在不同实体上的多个事件,是在多台处理器上同时处理多个任务,同一时刻,大家真的都在做事情
管程
管程(Monitor)Monitor 就是一种同步机制,他的义务是保证同一时间只有一个线程可以访问被保护的数据和代码
JVM 中同步是基于进入和退出监视器对象(管程对象)来实现的,每个对象实例都会有一个 Monitor 对象,底层是 C++ 实现的
用户线程和守护线程
用户线程:是系统的工作线程,它会完成这个程序需要完成的业务操作
守护线程:是一种特殊的线程,为其他线程服务的,在后台默默地完成一些系统性的服务(垃圾回收线程就是最好的例子)。没有服务对象就没有必要继续运行了。(当系统只剩下守护线城时,JVM 就会自动退出)
线程的 daemon 属性:true 表示是守护线程,false 表示不是的
小总结:
- 如果用户线程全部结束意味着程序要完成的业务操作已经结束,守护进程随着 JVM 一起结束工作
- setDaemon(true) 方法必须在 start 前设置,否则抛出 llegalThreadStateException
Future 接口
Future 接口(FutureTask 实现类)定义了操作执行异步任务的一些方法,如获取异步任务的执行结果、取消任务的执行、判断任务是否被取消、判断任务执行是否完毕等
作用:为主线程开一个分支任务,专门为主线程处理耗时和费力的复杂业务。提供了一种异步并行计算的功能。
如果主线程需要执行一个耗时很长的任务,可以通过 Future 接口把这个任务放到异步线程中执行,主线程继续处理其他任务或先结束,再通过 Future 获取计算结果。
目的:异步多线程任务执行且返回有结果 三个特点:多线程/有返回/异步任务
FutureTask 实现 RunnableFuture -> Future、Runnable
优点:FutureTask + 线程池能显著提高程序的执行效率
多线程实战
1 | public class FutureThreadPoolDemo { |
缺点:
get 方法容易阻塞(一旦调用必须等到结果才会离开,不管是否计算完成,容易程序堵塞)
1
2
3
4
5
6
7
8
9
10
11
12
public static void main(String[] args) {
FutureTask<String> futureTask = new FutureTask<>(() -> {
System.out.println(Thread.currentThread().getName() + "\t ----come in");
TimeUnit.SECONDS.sleep(5);
return "task over";
});
Thread t1 = new Thread(futureTask, "t1");
t1.start();
System.out.println(futureTask.get());
System.out.println(Thread.currentThread().getName() + "\t ----忙其他任务了");
}输出结果(先执行子线程,执行完才执行 main 线程 -> 阻塞了主线程的执行):
1
2
3t1 ----come in
task over
main ----忙其他任务了解决方法:通过指定等待时间来避免,过时不候
System.out.println(futureTask.get(3, TimeUnit.SECONDS)); 这样子就会抛出超时异常,因为子线程执行时间为 5 秒
isDone 轮询:轮询会耗费无谓的 CPU 的资源,也不能及时获取计算结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static void main(String[] args) {
FutureTask<String> futureTask = new FutureTask<>(() -> {
System.out.println(Thread.currentThread().getName() + "\t ----come in");
TimeUnit.SECONDS.sleep(5);
return "task over";
});
Thread t1 = new Thread(futureTask, "t1");
t1.start();
System.out.println(Thread.currentThread().getName() + "\t ----忙其他任务了");
while(true) {
if (futureTask.isDone()) {
System.out.println(futureTask.get());
break;
} else {
TimeUnit.MILLISECONDS.sleep(500);
System.out.println("正在处理中,不要在催了,越催越慢,再催熄火");
}
}
}
结论:Future 对于结果的获取不是很友好,只能通过阻塞方式或轮询的方式获取任务的结果
CompletableFuture
作用:
- 可将多个异步任务的计算结果组合起来,后一个异步任务的计算结果依赖前一个结果
- 将两个或多个异步计算合成一个计算,这个几个异步计算相互独立
CompletableFuture 提供了一种类似观察者模式的机制,当任务执行完成会通知监听方
类架构说明:
CompletionStage 代表异步计算过程中的某一阶段,一个阶段完成后会触发另一个阶段
获取结果和触发计算
创建异步任务的 2 种方式:
runAsync 无返回值
1
2
3
4
5
6
7
8
9
10
11
12
13
public static void main(String[] args) {
CompletableFuture<Void> completableFuture = CompletableFuture.runAsync(() -> {
System.out.println(Thread.currentThread().getName() + "\t -----come in");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("task is over");
});
System.out.println(completableFuture.get());
}输出内容:
1
2
3ForkJoinPool.commonPool-worker-9 -----come in
task is over
nullsupplyAsync 有返回值
- get 方法阻塞主线程
- get(timeout, TimeUnit) 过时不候,未在规定时间内执行完就抛异常
- getNow 立即获取结果,计算完就获取计算完的结果,否则就获取设定的值。立即获取结果不阻塞
- complet 方法返回的是布尔值,true 代表打断了 get 方法,反之代表未打断
CompletableFuture 的优点:
- 异步任务结束后会自动回调某个对象的方法
- 主线程设置好回调后,不再关心异步任务的执行,异步任务之间可顺序执行
- 异步任务出错时,会自动回调某个对象的方法
CompletableFuture get 方法和 join 方法的区别:
异常处理:
get()
方法声明了throws InterruptedException, ExecutionException
,因此必须处理这两个异常,否则会编译错误。join()
方法没有声明抛出任何受检异常,因此在处理结果时更加方便,不需要显式捕获异常。
返回值:
get()
方法返回T
类型的结果或抛出异常(ExecutionException
包装实际异常)。join()
方法直接返回T
类型的结果,或者如果有异常,会抛出CompletionException
,该异常会包装实际异常。
使用场景:
- 如果你希望在获取结果时能够处理异常,可以使用
join()
方法,因为它简化了异常处理的过程。 - 如果你需要对
InterruptedException
和ExecutionException
进行精细的处理或转换,你可能更倾向于使用get()
方法。
- 如果你希望在获取结果时能够处理异常,可以使用
对计算结果进行处理
thenApply
- 计算结果存在依赖关系,这两个线程串行化
- 异常相关,当前出现异常,不走下一步,并且后面都不走
handle
- 计算结果存在依赖关系,这两个线程串行化
- 异常相关,当前出现异常,不走下一步,但是后面的继续走,根据带的异常参数进一步处理
总结:简单一点理解就是一个是并行的一个是串行的,串行的A步骤G了,就直接去处理异常的步骤了,并行的调用步骤A G了,就当没发生过直接去调用步骤B
任务之间顺序执行
对计算结果进行消费
对计算结果进行选用
那个线程执行快,applyToEither 方法就返回谁
1 | public class CompletableFutureFastDemo { |
对计算结果合并
1 | public class CompletableFutureCombineDemo { |
说说那些”锁“事
乐观锁和悲观锁
乐观锁:认为使用数据时不会有别的线程修改数据或资源,因此不会加锁,而是通过版本号或者 CAS 算法来判断有无被修改(适合读操作多的场景)
悲观锁:认为使用数据时有别的线程会修改数据,因此会加锁来保证数据不被其他线程修改(适合写操作多的场景)
Synchronized 关键字
1 | /** |
公平锁非公平锁
公平锁:是指多个线程按照申请锁的顺序来获取锁,这里类似排队买票,先来的人先买后来的人在队尾排着,这是公平的 Lock lock=new ReentrantLock(true); //true表示公平锁,先来先得
1 | ReentrantLock lock = new ReentrantLock(true); |
非公平锁:是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁,在高并发环境下,有可能造成优先级翻转或者饥饿的状态(某个线程一直得不到锁)
1 | ReentrantLock lock = new ReentrantLock(false); |
如何选择?
如果为了更高的吞吐量,显然是非公平锁合适,因为节省了很多线程切换时间。反之选择公平锁。
1 | public class SaleTicketDemo { |
可重入锁种类
是指同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提,锁对象得是同一个对象),不会因为之前己经获取过还没释放而阻塞。(一个线程中的多个流程可以获取同一把锁,持有这把同步锁可以再次进入。自己可以获取自己的内部锁)
隐式锁
隐式锁:即 synchronized 关键字使用的锁默认是可重入锁
同步代码块:
1 | public class ReEntryLockDemo { |
同步方法:
1 | public class ReEntryLockDemo { |
synchronized 的可重入实现机制
显示锁
1 | public class ReEntryLockDemo { |
以上 t2 线程拿不到锁,出现死锁。加锁几次就要释放锁几次。
死锁及排查
死锁案例:
1 | public class DeadLockDemo { |
结果:
1 | A 自己持有 A 锁,希望获得 B 锁 |
如何排查死锁?
通过图形化界面查找死锁:
二、中级
什么是中断机制?
- 一个线程不该由其他线程来强制中断或停止,而是由自己自行停止。
- 在 Java 中没发立即停止一个线程,但停止线程十分重要,因此 Java 提供了一种用于停止线程的协商机制——中断
- 中断是一种协商机制,中断的过程需要自己实现
- 想要中断一个线程,需要手动调用该线程的 interrupt 方法,这也仅仅将线程对象的中断标识改为 true。
中断的相关 API 方法之三大方法说明
1 | interrupt() // 设置线程的中断状态为 true,发起中断协商而非立即中断 (实例方法) |
大厂面试题中断机制考点
如何停止中断运行中的线程?
通过 volatile 变量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27public class InterruptDemo {
static volatile boolean isStop = false;
public static void main(String[] args) {
new Thread(() -> {
while (true) {
if (isStop) {
System.out.println(Thread.currentThread().getName() + "\t isStop 被修改为 true,程序停止");
break;
}
System.out.println("------hello volatile");
}
}, "t1").start();
try {
TimeUnit.MILLISECONDS.sleep(20);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
new Thread(() -> {
isStop = true;
}, "t2").start();
}
}通过 AtomicBoolean
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27public class InterruptDemo {
static AtomicBoolean atomicBoolean = new AtomicBoolean(false);
public static void main(String[] args) {
new Thread(() -> {
while (true) {
if (atomicBoolean.get()) {
System.out.println(Thread.currentThread().getName() + "\t atomicBoolean 被修改为 true,程序停止");
break;
}
System.out.println("------hello atomicBoolean");
}
}, "t1").start();
try {
TimeUnit.MILLISECONDS.sleep(20);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
new Thread(() -> {
atomicBoolean.set(true);
}, "t2").start();
}
}通过 Thread 类自带的 isInterrupted 方法
总结:
当前线程的中断标识为 true,是不是线程就立刻停止了?
不是
谈谈你对 Thread.interrupted() 的理解
返回当前线程的中断状态,将中断状态改为 false
LockSupport 是什么
LockSupport 中的 park 和 unpark 作用分别是阻塞线程和解除阻塞线程
线程等待和唤醒的方法
Object 的 wait 和 notify 方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23public class LockSupportDemo {
public static void main(String[] args) {
Object objectLock = new Object();
new Thread(() -> {
synchronized (objectLock) {
System.out.println(Thread.currentThread().getName() + "\t ----come in");
try {
objectLock.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + "\t ----被唤醒");
}
}, "t1").start();
new Thread(() -> {
synchronized (objectLock) {
objectLock.notify();
System.out.println(Thread.currentThread().getName() + "\t ----发出通知");
}
}, "t2").start();
}
}结果:
1
2
3t1 ----come in
t2 ----发出通知
t1 ----被唤醒注意:notify 要放在 wait 方法之后,这两个方法必须要在锁块中才能正常使用
Condition 的 await 和 signal 方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33public class LockSupportDemo {
public static void main(String[] args) {
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
new Thread(() -> {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "\t ----come in");
condition.await();
System.out.println(Thread.currentThread().getName() + "\t ----被唤醒");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}, "t1").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
new Thread(() -> {
lock.lock();
try {
condition.signal();
} finally {
lock.unlock();
}
}, "t2").start();
}
}注意:await 要放在 signal 方法之后,这两个方法必须要在锁块中才能正常使用