1、什么是多线程?
1.1、线程和进程
1.1.1、进程
当一个程序被运行,就开启了一个进程, 比如启动了qq,word
程序由指令和数据组成,指令要运行,数据要加载,指令被cpu加载运行,数据被加载到内存,指令运行时可由cpu调度硬盘、网络等设备
1.1.2、线程
一个进程内可分为多个线程
一个线程就是一个指令流,cpu调度的最小单位,由cpu一条一条执行指令
操作系统的设计,因此可以归结为三点:
(1)以多进程形式,允许多个任务同时运行;
(2)以多线程形式,允许单个任务分成不同的部分运行;
(3)提供协调机制,一方面防止进程之间和线程之间产生冲突,另一方面允许进程之间和线程之间共享资源。
外部链接:通俗易懂的进程与线程之间的关系(有图)
1.2、并行和并发
1.2.1、并行
多核cpu运行 多线程时,真正的在同一时刻运行
1.2.2、并发
单核cpu运行多线程时,时间片进行很快的切换。线程轮流执行cpu
2、为什么使用多线程?
多线程的好处:
- 程序运行的更快!快!快!
- 充分利用cpu资源,目前几乎没有线上的cpu是单核的,发挥多核cpu强大的能力
3、多线程难在哪里?
单线程只有一条执行线,过程容易理解,可以在大脑中清晰的勾勒出代码的执行流程
多线程却是多条线,而且一般多条线之间有交互,多条线之间需要通信,一般难点有以下几点:
1. 多线程的执行结果不确定,受到cpu调度的影响
2. 多线程的安全问题
3. 线程资源宝贵,依赖线程池操作线程,线程池的参数设置问题
4. 多线程执行是动态的,同时的,难以追踪过程
5. 多线程的底层是操作系统层面的,源码难度大
4、多线程的基本使用
4.1、定义任务、创建和运行线程
任务: 线程的执行体。也就是我们的核心代码逻辑。
定义线程:
- 继承Thread类(可以说是 将任务和线程合并在一起了)
- 实现Runnable接口(可以说是 将任务和线程分开了)
- 实现Callable接口(利用FutureTask执行任务)
/**
* 1、corePoolSize 核心线程数大小,当线程数 < corePoolSize ,会创建线程执行 runnable
*
* 2、maximumPoolSize 最大线程数, 当线程数 >= corePoolSize的时候,会把 runnable 放入 workQueue中
*
* 3、keepAliveTime 保持存活时间,当线程数大于corePoolSize的空闲线程能保持的最大时间。
*
* 4、unit 时间单位
*
* 5、workQueue 保存任务的阻塞队列
*
* 6、threadFactory 创建线程的工厂
*
* 7、handler 拒绝策略
*/
public class WtyicyThreadPoolExecutor extends ThreadPoolExecutor {
private ThreadLocal<Integer> local = new ThreadLocal<Integer>() {
@Override
protected Integer initialValue() {
return 0;
}
};
public WtyicyThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
}
public WtyicyThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory);
}
public WtyicyThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, handler);
}
public WtyicyThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler);
}
@Override
public void execute(Runnable command) {
int priority = local.get();
try {
this.execute(command, priority);
} finally {
local.set(0);
}
}
public void execute(Runnable command, int priority) {
super.execute(new PriorityRunnable(command, priority));
}
public <T> Future<T> submit(Callable<T> task, int priority) {
local.set(priority);
return super.submit(task);
}
public <T> Future<T> submit(Runnable task, T result, int priority) {
local.set(priority);
return super.submit(task, result);
}
public Future<?> submit(Runnable task, int priority) {
local.set(priority);
return super.submit(task);
}
protected static class PriorityRunnable<E extends Comparable<? super E>> implements Runnable, Comparable<PriorityRunnable<E>> {
private final static AtomicLong seq = new AtomicLong();
private final long seqNum;
Runnable run;
private int priority;
public PriorityRunnable(Runnable run, int priority) {
seqNum = seq.getAndIncrement();
this.run = run;
this.priority = priority;
}
public int getPriority() {
return priority;
}
public void setPriority(int priority) {
this.priority = priority;
}
public Runnable getRun() {
return run;
}
@Override
public void run() {
this.run.run();
}
@Override
public int compareTo(PriorityRunnable<E> other) {
int res = 0;
if (this.priority == other.priority) {
if (other.run != this.run) {// ASC
res = (seqNum < other.seqNum ? -1 : 1);
}
} else {// DESC
res = this.priority > other.priority ? -1 : 1;
}
return res;
}
}
}
4.1.1、继承Thread类定义,创建,运行线程
Thread实现任务的局限性:
- 任务逻辑写在Thread类的run方法中,有单继承的局限性
- 创建多线程时,每个任务有成员变量时不共享,必须加static才能做到共享
public class WtyicyThread extends Thread{
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " == WtyicyThread执行了");
}
}
WtyicyThread wtyicyThread = new WtyicyThread();
// 不推荐这种方式
wtyicyThread.start();
WtyicyThreadPoolExecutor wtyicyThreadPoolExecutor = new WtyicyThreadPoolExecutor(5, 6, 3, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(3));
wtyicyThreadPoolExecutor.execute(wtyicyThread);
4.1.2、实现Runnable接口定义,创建,运行线程
Runbale相比Callable有以下的局限性:
- 任务没有返回值
- 任务无法抛异常给调用方
public class WtyicyRunnable implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " == WtyicyRunnable执行了");
}
}
WtyicyRunnable wtyicyRunnable = new WtyicyRunnable();
// 不推荐这种方式
new Thread(wtyicyRunnable).start();
wtyicyThreadPoolExecutor.execute(wtyicyRunnable);
4.1.3、实现Callable接口定义,创建,运行线程
public class WtyicyCallable implements Callable {
@Override
public String call() throws Exception {
System.out.println(Thread.currentThread().getName() + " == WtyicyCallable执行了");
return "SUCCEE";
}
}
WtyicyCallable wtyicyCallable = new WtyicyCallable();
FutureTask<String> futureTask = new FutureTask<String>(wtyicyCallable);
// 不推荐这种方式
new Thread(futureTask).start();
wtyicyThreadPoolExecutor.execute(futureTask);
System.out.println(futureTask.get());
5、线程状态
线程的状态可从 操作系统层面分为五种状态 从java api层面分为六种状态。
5.1、五种状态
- 初始状态:创建线程对象时的状态
- 可运行状态(就绪状态):调用start()方法后进入就绪状态,也就是准备好被cpu调度执行
- 运行状态:线程获取到cpu的时间片,执行run()方法的逻辑
- 阻塞状态: 线程被阻塞,放弃cpu的时间片,等待解除阻塞重新回到就绪状态争抢时间片
- 终止状态: 线程执行完成或抛出异常后的状态
5.2、六种状态
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}
- NEW 线程对象被创建
- Runnable 线程调用了start()方法后进入该状态,该状态包含了三种情况
2.1. 就绪状态 :等待cpu分配时间片
2.2. 运行状态:进入Runnable方法执行任务
2.3. 阻塞状态:BIO 执行阻塞式io流时的状态 - Blocked 没获取到锁时的阻塞状态
- WAITING 调用wait()、join()等方法后的状态
- TIMED_WAITING 调用 sleep(time)、wait(time)、join(time)等方法后的状态
- TERMINATED 线程执行完成或抛出异常后的状态
6、线程的相关方法总结
6.1、Thread类中的核心方法
方法名称 | 是否static | 方法说明 |
---|---|---|
start() | 否 | 让线程启动,进入就绪状态,等待cpu分配时间片 |
run() | 否 | 重写Runnable接口的方法,线程获取到cpu时间片时执行的具体逻辑 |
yield() | 是 | 线程的礼让,使得获取到cpu时间片的线程进入就绪状态,重新争抢时间片 |
sleep(time) | 是 | 线程休眠固定时间,进入阻塞状态,休眠时间完成后重新争抢时间片,休眠可被打断 |
join()/join(time) | 否 | 调用线程对象的join方法,调用者线程进入阻塞,等待线程对象执行完或者到达指定时间才恢复,重新争抢时间片 |
isInterrupted() | 否 | 获取线程的打断标记,true:被打断,false:没有被打断。调用后不会修改打断标记 |
interrupted() | 否 | 打断线程,抛出InterruptedException异常的方法均可被打断,但是打断后不会修改打断标记,正常执行的线程被打断后会修改打断标记 |
stop() | 否 | 停止线程运行 不推荐 |
suspend() | 否 | 挂起线程 不推荐 |
resume() | 否 | 恢复线程运行 不推荐 |
currentThread() | 是 | 获取当前线程 |
6.2、Thread类中的核心方法
方法名称 | 方法说明 |
---|---|
wait()/wait(long timeout) | 获取到锁的线程进入阻塞状态 |
notify() | 随机唤醒被wait()的一个线程 |
notifyAll() | 唤醒被wait()的所有线程,重新争抢时间片 |
7、六种线程状态和方法的对应关系
7.1、线程的礼让-yield()&线程的优先级
t2线程每次执行时进行了yield(),线程1执行的机会明显比线程2要多。
Runnable r1 = () -> {
int count = 0;
for (;;){
log.info("---- 1>" + count++);
}
};
Runnable r2 = () -> {
int count = 0;
for (;;){
// 设置礼让
Thread.yield();
log.info(" ---- 2>" + count++);
}
};
wtyicyThreadPoolExecutor.execute(r1);
wtyicyThreadPoolExecutor.execute(r2);
wtyicyThreadPoolExecutor.execute(r1 , 1 );
wtyicyThreadPoolExecutor.execute(r2,10);
// Thread t1 = new Thread(r1,"t1");
// Thread t2 = new Thread(r2,"t2");
// cpu比较忙时,优先级高的线程获取更多的时间片
// cpu比较闲时,优先级设置基本没用
// t1.setPriority(Thread.MIN_PRIORITY);
// t2.setPriority(Thread.MAX_PRIORITY);
// t1.start();
// t2.start();
7.2、守护线程
默认情况下,java进程需要等待所有线程都运行结束,才会结束,有一种特殊线程叫守护线程,当所有的非守护线程都结束后,即使它没有执行完,也会强制结束。
默认的线程都是非守护线程,垃圾回收线程就是典型的守护线程
// 设为true表示未守护线程,当主线程结束后,守护线程也结束。
// 默认是false,当主线程结束后,thread继续运行,程序不停止
t1.setDaemon(true);
7.3、线程的阻塞
线程的阻塞可以分为好多种,从操作系统层面和java层面阻塞的定义可能不同,但是广义上使得线程阻塞的方式有下面几种:
- BIO阻塞,即使用了阻塞式的io流
- sleep(long time) 让线程休眠进入阻塞状态
- a.join() 调用该方法的线程进入阻塞,等待a线程执行完恢复运行
- sychronized或ReentrantLock 造成线程未获得锁进入阻塞状态
- 获得锁之后调用wait()方法 也会让线程进入阻塞状态
- LockSupport.park() 让线程进入阻塞状态
7.3.1、sleep()
使线程休眠,会将运行中的线程进入阻塞状态。当休眠时间结束后,重新争抢cpu的时间片继续运行
try {
// 休眠2秒
// 该方法会抛出 InterruptedException异常 即休眠过程中可被中断,被中断后抛出异常
Thread.sleep(2000);
} catch (InterruptedException异常 e) {
}
try {
// 使用TimeUnit的api可替代 Thread.sleep
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
}
7.3.2、join()
join是指调用该方法的线程进入阻塞状态,等待某线程执行完成后恢复运行
Thread t = new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
r = 10;
});
t.start();
// 让主线程阻塞 等待t线程执行完才继续执行
// 去除该行,执行结果为0,加上该行 执行结果为10
t.join();
7.3.2、线程的打断-interrupt()
打断标记:线程是否被打断,true表示被打断了,false表示没有
isInterrupted() 获取线程的打断标记 ,调用后不会修改线程的打断标记
interrupt()方法用于中断线程
可以打断sleep,wait,join等显式的抛出InterruptedException方法的线程,但是打断后,线程的打断标记还是false
打断正常线程 ,线程不会真正被中断,但是线程的打断标记为true
interrupted() 获取线程的打断标记,调用后清空打断标记 即如果获取为true 调用后打断标记为false (不常用)
interrupt实例: 有个后台监控线程不停的监控,当外界打断它时,就结束运行
@Slf4j
class TwoPhaseTerminal{
// 监控线程
private Thread monitor;
public void start(){
monitor = new Thread(() ->{
// 不停的监控
while (true){
Thread thread = Thread.currentThread();
// 判断当前线程是否被打断
if (thread.isInterrupted()){
log.info("当前线程被打断,结束运行");
break;
}
try {
Thread.sleep(1000);
// 监控逻辑中被打断后,打断标记为true
log.info("监控");
} catch (InterruptedException e) {
// 睡眠时被打断时抛出异常 在该处捕获到 此时打断标记还是false
// 在调用一次中断 使得中断标记为true
thread.interrupt();
}
}
});
monitor.start();
}
public void stop(){
monitor.interrupt();
}
}
8、往期佳文
8.1、面试系列
1、吊打面试官之一面自我介绍
2、吊打面试官之一面项目介绍
3、吊打面试官之一面系统架构设计
4、吊打面试官之一面你负责哪一块
5、吊打面试官之一面试官提问
······持续更新中······
8.2、技术系列
1、吊打面试官之分布式会话
2、吊打面试官之分布式锁
3、吊打面试官之乐观锁
4、吊打面试官之幂等性问题
5、吊打面试关之分布式事务
6、吊打面试官之项目线上问题排查
······持续更新中······
8.3、源码系列
1、源码分析之SpringBoot启动流程原理
2、源码分析之SpringBoot自动装配原理
3、源码分析之ArrayList容器
4、源码分析之LinkedList容器
5、源码分析之HashMap容器
8.4、数据结构和算法系列
1、数据结构之八大数据结构
2、数据结构之动态查找树(二叉查找树,平衡二叉树,红黑树)
······持续更新中······