1、基本概念
1、共享资源
多个线程对同一份资源进行访问(读写操作),该资源被称为共享资源。如何保证多个线程访问到的数据是一致的,则被称为数据同步或资源同步。
2、线程通信
线程通信,又叫进程内通信,和网络通信等进程间通信不同;多个线程实现互斥资源访问的时候,会互相发送信号。
2、同步、异步、阻塞、非阻塞
同步和异步是 获取结果的方式,阻塞和非阻塞是 等待结果中是否能够完成其他事情。
同步阻塞(BIO),需要等待读取完客户端的数据,同时需要阻塞的判断客户端是否有数据。
同步非阻塞(NIO),需要等待读取完客户端的数据,但是轮询的方式判断客户端是否有数据,可以去做其他事情。
异步阻塞,等待结果回调通知,cpu处于等待的休眠中。
异步非阻塞(AIO),操作系统来完成读取的操作,读取完毕通知回调,使用线程池的方式去轮询客户端是否有数据,可以去做其他事情。
并发,单个 cpu 可以处理多个任务,但是同一时刻只有一个任务在运行。
并行,多个任务在多个 cpu 上运行,实现真正的同一时刻运行。
2、生命周期
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。
14、AQS
- 独占模式
#获取
1、尝试获取资源成功立即返回;失败的话,创建独占节点,利用CAS加入到队列尾部,进入自旋状态
2、如果前一个节点是头节点,再次尝试获取资源,成功设置为头节点;否则挂起,等待被前驱节点唤醒
#针对可中断来说
1、普通的获取,在发生了中断后,会清除中断位,并在获取资源成功后,触发一次中断
2、可中断的获取,在发生了中断后,也会清除中断位,但是直接抛出 interrupted 异常
#释放
1、释放同步状态
2、获取当前节点的下一个节点,唤醒
- 共享模式
#获取
1、获取同步状态,如果返回值>=0,则说明同步状态(state)有剩余,获取锁成功直接返回
2、失败,向队列尾部添加一个共享类型的Node节点,随即该节点进入自旋状态
3、前驱节点如果为头节点,再次判断同步状态是否(state)有剩余
4、如果是,则说明当前节点可执行,同时把当前节点设置为头节点,并且唤醒所有后继节点
- 两者区别
1、独占锁的同步状态值为1,即同一时刻只能有一个线程成功获取同步状态;共享锁的同步状态>1,取值由上层同步组件确定
2、独占锁队列中头节点运行完成后释放它的直接后继节点;共享锁队列中头节点运行完成后释放它后面的所有节点
3、共享锁中会出现多个线程(即同步队列中的节点)同时成功获取同步状态的情况
15、重入锁 ReentrantLock
#获取
1、如果当前有锁,且拥有者是当前线程,再次增加重入
2、如果当前无锁,直接尝试获取锁,公平模式会判断是否有等待的线程,非公平模式下直接尝试独占资源
16、计数器 CountDownLatch
1、使用的是共享模式,初始化时设置 state 一个固定的数量
2、await 方法,调用 sync 的中断 acquireShared 方法,重写 tryAcquireShared 获取方式,当 state 到达0的时候,才表示资源可获取
3、countDown 方法,调用 sync 的 releaseShared 方法,不断的对 state 进行减一
17、读写锁 ReentrantReadWriteLock
- 写锁
#获取
1、当前处于无锁状态,独占模式获取写锁
2、如果设置了 writerShouldBlock,直接返回 false
1、当前处于有锁状态
2、有读锁,直接返回 false
3、有写锁,如果是当前线程,则重入,否则返回 false
- 读锁
1、如果有写锁,并且写锁不是当前线程,返回 -1
2、如果没有写锁,尝试获取读锁,如果设置了 readerShouldBlock,进入再次判断