1、基本概念
1、共享资源
多个线程对同一份资源进行访问(读写操作),该资源被称为共享资源。如何保证多个线程访问到的数据是一致的,则被称为数据同步或资源同步。
2、线程通信
线程通信,又叫进程内通信,和网络通信等进程间通信不同;多个线程实现互斥资源访问的时候,会互相发送信号。
2、同步、异步、阻塞、非阻塞
同步和异步是 获取结果的方式,阻塞和非阻塞是 等待结果中是否能够完成其他事情。
同步阻塞(BIO),需要等待读取完客户端的数据,同时需要阻塞的判断客户端是否有数据。
同步非阻塞(NIO),需要等待读取完客户端的数据,但是轮询的方式判断客户端是否有数据,可以去做其他事情。
异步阻塞,等待结果回调通知,cpu处于等待的休眠中。
异步非阻塞(AIO),操作系统来完成读取的操作,读取完毕通知回调,使用线程池的方式去轮询客户端是否有数据,可以去做其他事情。
并发,单个 cpu 可以处理多个任务,但是同一时刻只有一个任务在运行。
并行,多个任务在多个 cpu 上运行,实现真正的同一时刻运行。
2、生命周期
//当new一个Thread的时候,此时只是一个简单的java对象
NEW
//调用start方法之后,进入该状态,此时只是处于可执行状态,但还未执行
RUNNABLE
//处于执行状态,可由于调用sleep/join和wait、阻塞IO操作、尝试获取锁,进入 BLOCKED 状态
//可由于cpu调度、调用yield,进入 RUNNABLE 状态
RUNNING
//在sleep完毕和wait唤醒、阻塞IO操作完成、获得了锁、阻塞过程被interrupt,进入 RUNNABLE 状态
BLOCKED
3、守护线程
一般使用 new Thread 创建的线程都是非守护线程,也称用户线程。设置为守护进行的方式就是,在 run 之前,调用setDaemon(true)。守护线程,仅仅是为用户线程提供服务,那么一旦所有的用户线程都运行完毕,守护进行也会结束。相反,只要有非守护线程存在,那么守护线程就不会终止。
守护线程的应用场景:垃圾回收、心跳检测、拼音检查线程
package com.vim;
import java.util.concurrent.TimeUnit;
public class App {
public static void main( String[] args ) throws Exception{
Thread t = new Thread(()->{
while (true){
try {
TimeUnit.SECONDS.sleep(1);
System.out.println("......");
}catch (Exception e){
e.printStackTrace();
}
}
});
//只有t设置了此处,才会随着父进程的退出而退出
t.setDaemon(true);
t.start();
TimeUnit.SECONDS.sleep(5);
System.out.println("main is over!");
}
}
4、线程 yield、sleep
yield,称为线程让步,从 RUNNING 状态转为 RUNNABLE 状态,有可能切换之后,再次抢到执行权,进入 RUNNING 状态。
sleep,会阻塞该线程,进入 BLOCKED 状态,此时是挂起,让出cpu;只有阻塞时间到了,才会进入 RUNNABLE 状态,并不一定马上获取到 cpu 的执行权。
sleep(0) 的作用是,触发操作系统立刻重新进行一次CPU竞争,竞争的结果也许是当前线程仍然获得CPU控制权,也许会换成别的线程获得CPU控制权。
package com.vim;
public class App {
public static void main( String[] args ) throws Exception {
new Thread(()->{
try {
while (true){
Thread.sleep(0);
}
}catch (Exception e){
e.printStackTrace();
}
}).start();
}
}
5、线程 interrupt
wait、sleep、join 使当前线程进入阻塞状态,而 interrupt 可以打断阻塞,不过仅仅是打算当前的阻塞状态。当前线程会抛出一个 InterruptException 异常,就像一个信号通知。此通知会将 interrupt flag 置为 true,不过针对阻塞状态下的中断,该状态位会被重置为 false。通过 thread.isInterrupted() 来判断,当然,阻塞状态下的,会被清除,从而影响该方法的结果。
package com.sample.modules.test;
import java.util.concurrent.TimeUnit;
public class Test {
//中断一个线程,中断位 true
public static void test1() throws Exception{
Thread t = new Thread(()->{
//2.获取当前的 interrupt flag 状态为 true
System.out.println(Thread.currentThread().isInterrupted());
});
t.start();
//1.中断线程
t.interrupt();
TimeUnit.MINUTES.sleep(4);
}
//中断一个线程,中断位 false
public static void test2() throws Exception{
Thread t = new Thread(()->{
//2.清除中断位
Thread.interrupted();
//3.获取当前的 interrupt flag 状态为 false
System.out.println(Thread.currentThread().isInterrupted());
});
t.start();
//1.中断线程
t.interrupt();
TimeUnit.MINUTES.sleep(4);
}
//中断一个线程,中断位 false
public static void test3() throws Exception{
Thread t = new Thread(()->{
//2.中断wait、sleep、join导致的阻塞
try {
TimeUnit.SECONDS.sleep(5);
}catch (InterruptedException e){
System.out.println("i am interrupted");
}
//3.获取当前的 interrupt flag 状态为 false
System.out.println(Thread.currentThread().isInterrupted());
});
t.start();
//1.中断线程
t.interrupt();
TimeUnit.MINUTES.sleep(4);
}
//阻塞之前中断结果:false、true、i am interrupted、false
public static void test4(){
Thread.interrupted();
System.out.println("interrupt flag: "+Thread.currentThread().isInterrupted());
Thread.currentThread().interrupt();
System.out.println("interrupt flag: "+Thread.currentThread().isInterrupted());
try {
TimeUnit.SECONDS.sleep(1);
}catch (InterruptedException e){
System.out.println("i am interrupted");
}
System.out.println("interrupt flag: "+Thread.currentThread().isInterrupted());
}
public static void main(String[] args) throws Exception{
test3();
}
}
6、线程 join
parent 线程调用 child 线程的 join 方法,实际上调用的是 join(0) ,该方法加了锁,循环判断 child 线程的存活状态,当然,每次循环中调用 wait(0) 方法,这样的话可以让其他线程也进入 join(0) 方法。
//当前方法没有上锁
public final void join() throws InterruptedException {
join(0);
}
//此方法上锁,此时其他线程可以进入 join(),但不可以进入 join(0)
//不断的检查线程是否 alive,调用 wait(0),这样就释放了锁,其他的线程就可以进入 join(0),也就是可以有多个线程等待某个线程执行完毕
//一旦线程不在 alive,那么就会返回到 join() 方法,调用的线程就可以继续执行下去
public final synchronized void join(long millis){
if (millis == 0) {
while (isAlive()) {
wait(0);
}
}
}
7、线程通知 notify、wait
这两个方法,来源于 Object 类,两者配合使用。wait 方法属于对象方法,在调用之前必须先获取该对象的 monitor 锁,调用之后,就会释放该对象的 monitor 锁,从而进入该对象关联的 waitset 中,等待其他线程使用 notify 唤醒。
典型的生产消费场景:
生产者,在生产产品时,对仓库(同步资源)进行上锁,如果当前仓库没有满,则放入产品,使用 notifyAll 通知消费者;如果当前仓库满了,则使用 wait 释放锁,进入 waitset 阻塞等待 notifyAll 通知。
消费者,来到仓库消费,对仓库(同步资源)进行上锁,如果当前仓库有产品,则拿走产品,使用 notifyAll 通知生产者;如果当前仓库是空的,则使用 wait 释放锁,进入 waitset 阻塞等待 notifyAll 通知。
当 notifyAll 来临的时候,针对所有的生产者和消费者来说,都有拿到仓库钥匙的机会,就会再去竞争,再次进入以上判断逻辑。
8、wait 和 sleep 的区别
相同之处:
使线程进入到阻塞状态;可以被 interrupt 中断;
不同之处:
wait 是 Object 共有,sleep 是 Thread 特有。
wait 必须运行在同步方法中,sleep不需要。
wait 会释放锁,如果sleep放在同步方法中,并不会释放锁。
9、线程异常处理
package com.sample.modules.test;
public class Test {
public static void main(String[] args) throws Exception{
Thread t = new Thread(()->{
int i = 1/0;
});
t.setUncaughtExceptionHandler((thread, e)->{
System.out.println("exception...");
});
t.start();
}
}
10、计算机内存模型 和 Java 内存模型
原理追溯:cpu 在执行指令的时候,数据来源于主内存(RAM),由于两者速度的严重不对等,之间出现了缓存 cache;一般分为 L1、L2、L3 缓存,每个 CPU 核心包含一套 L1,共享 L2 和 L3 缓存。
缓存一致性问题:当多个处理器的运算任务都涉及同一块主内存区域时,每个 cpu 从主内存中取出变量,放到本地 cache 中,进行计算之后,写入到 cache 中,再由 cache 刷新到主内存中。
- 读取主内存 i 到 cache 中
- 对 i 进行 ++
- 将结果写回 cache
- 将 cache 刷新回主内存。
缓存一致性协议:cpu 在操作 cache 中的数据时,如果发现是一个共享变量,那么在写入的时候,会发出信号通知其他的 cpu 将该变量的 cache line 置为无效状态,其他 cpu 在进行该变量读取的时候,就需要去主内存中读取。
相比计算机内存模型,Java 内存模型: 线程 == CPU, 工作内存 == CPU cache,主内存 == 主内存。
12、并发编程三大特性
- 原子性,多次操作中,要么全部得到执行,要么全部不执行。
- 可见性,一个线程对共享变量,作了修改,那么其他线程立即可以看到修改后的值。
- 有序性,程序代码在执行过程中的先后顺序。
13、synchronized 关键字
synchronized 关键字提供了一种锁的机制,能够保证共享变量的互斥访问,即同一时刻,只能有一个线程访问同步资源。
内存方面,monitor enter 和 monitor exit 两个 JVM 指令,保证了任何线程在 monitor enter 之前必须从主内存中获取数据,在 monitor exit 之后,必须把更新的值刷新到主内存中。这两个 JVM 指令,严格的遵守 happends-before 原则,即一个 monitor exit 指令之前必须有一个 monitor enter 指令存在。
该关键字对于同步资源的排他性访问,很有效,但是其他没有获取到 monitor 的线程,到底阻塞多久,能不能提前解除阻塞,这些都是未知的。为此,java 为我们提供了其他的解决方案,显式锁。如 ReentrantLock。