一、概念
1.1 线程、进程和协程
进程:是系统分配资源都基本单元,有独立的内存空间 比如一个迅雷应用程序,既不共享堆,亦不共享栈。一个进程中至少有一个线程。
线程: ,线程是操作系统调度的最小单元线程拥有自己独立的栈和共享的堆,共享堆,不共享栈比如为在迅雷程序中开启多个下载任务下载就是多线程。如果只有单个线程,那么我们就只能下载一个任务,然后等待他下载完了再下载,而开启多个线程就可以同时下载多个任务。
协程和线程一样共享堆,不共享栈,协程由程序员在协程的代码里显示调度。而线程是CPU进行调度。
协程和线程的区别是:协程避免了无意义的调度,由此可以提高性能,但也因此,**程序员必须自己承担调度的责任,**同时,协程也失去了标准线程使用多CPU的能力。
在线程里面可以开启协程,让程序在特定的时间内运行。也就是说一个线程执行不同的协程。
多进程和多线程的概念
概念:
进程是程序在计算机上的一次执行活动。当你运行一个程序,你就启动了一个进程。凡是用于完成操作系统的各种功能的进程就是系统进程,而所有由你启动的进程都是用户进程。
进程就是有一个或多个线程构成的。而线程是进程中的实际运行单位,是独立运行于进程之中的子任务。是操作系统进行运算调度的最小单位。可理解为线程是进程中的一个最小运行单元
进程特点:
独立性:进程是一个能独立运行的基本单位,同时也是系统分配资源和调度的独立单位
动态性:进程的实质是程序的一次执行过程,进程是动态产生,动态消亡的
并发性:任何进程都可以同其他进程一起并发执行
1.2 并发和并行
并行:在同一时刻,有多个指令在多个CPU上同时执行。并发:在同一时刻,有多个指令在单个CPU上交替执行。
1.3 什么是多线程上下文切换?
每个线程都是竞争CPU执行权的,比如CPU只有单核
CPU在执行一个已经运行的线程时候切换到另一个在等待获取CPU执行权的线程。,当切换执行权的时候就叫做多线程上下文切换。
上下文切换通常是计算密集型的。上下文切换对系统来说意味着消耗大量的 CPU 时间,事实上,可能是操作系统中时间消耗最大的操作。
Linux 相比与其他操作系统(包括其他类 Unix 系统)有很多的优点,其中有一项就是,其上下文切换和模式切换的时间消耗非常少。
1.4 如何减少上下文切换,提高操作系统效率
无锁并发:多线程竞争锁时,会引起上下文切换,所以多线程处理数据时,可以用一些办法来避免使用锁。
使用最少线程:避免创建不需要的线程,比如任务很少,但是创建了很多线程来处理,这样会造成大量线程都处于等待状态
协程:在单线程实现多任务的调度,并在单线程里维持多个任务间的切换
1.5 什么是线程安全
如果你的代码在多线程下执行和在单线程下执行永远都能获得一样的结果,那么你的代码就是线程安全的。
这里可以举例说明不安全的一个实例,就是多线程处理共享数据比如仅剩一张票时,多线程操作会造成重复卖票情况。也就是说在多线程环境中,能够正确地处理多个线程之间的共享变量,使程序功能正确完成。
1.6 一个线程终止,程序会终止吗
- 当所有的线程都结束的时候才说明程序运行Over了。
- 若有线程调用system.exit()则整个程序终止
1.7 一个线程如果出现了运行时异常会怎么样
Exception分为RuntimeException和非运行时异常。
非运行时异常必须处理,比如thread中sleep()时,必须处理InterruptedException异常,才能通过编译。
运行时异常可以抛出,然后线程继续运行,如果不抛出或者不处理,则该线程则会停止。
1.8 你对线程优先级的理解是什么?
每一个线程都是有优先级的,一般来说,高优先级的线程在运行时会具有优先权,但这依赖于线程调度的实现,这个实现是和操作系统相关的(OS dependent)。我们可以定义线程的优先级,但是这并不能保证高优先级的线程会在低优先级的线程前执行。线程优先级是一个int变量(从1-10),1代表最低优先级,10代表最高优先级。
一般无需设置线程优先级。
二、多线程的实现方案
-
继承Thread类的方式进行
-
实现Runnable接口的方式进行实现
-
利用Callable和Future接口方式实现,JDK5升级
- 创建线程池(当前最普遍)
2.1 继承Thread类
方法名 | 说明 |
---|---|
void run() | 在线程开启后,此方法将被调用执行 |
void start() | 使此线程开始执行,Java虚拟机会调用run方法() |
public class MyThread extends Thread { //单独MyThread类
@Override
public void run() {
for(int i=0; i<100; i++) {
System.out.println(i);
}
}
}
public class MyThreadDemo { //单独MyThreadDemo类
public static void main(String[] args) {
MyThread my1 = new MyThread();
MyThread my2 = new MyThread();
// my1.run();
// my2.run();
//void start() 导致此线程开始执行; Java虚拟机调用此线程的run方法
my1.start();
my2.start();
}
}
2.2 Runnable接口
-
定义一个类MyRunnable实现Runnable接口
-
在MyRunnable类中重写run()方法
-
创建MyRunnable类的对象
-
创建Thread类的对象,把MyRunnable对象作为构造方法的参数
-
启动线程
public class MyRunnable implements Runnable {
@Override
public void run() {
for(int i=0; i<100; i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
public class MyRunnableDemo {
public static void main(String[] args) {
//创建MyRunnable类的对象
MyRunnable my = new MyRunnable();
//创建Thread类的对象,把MyRunnable对象作为构造方法的参数
//Thread(Runnable target)
// Thread t1 = new Thread(my);
// Thread t2 = new Thread(my);
//Thread(Runnable target, String name)
Thread t1 = new Thread(my,"坦克");
Thread t2 = new Thread(my,"飞机");
//启动线程
t1.start();
t2.start();
}
}
2.3 Callable接口
-
定义一个类MyCallable实现Callable接口
-
在MyCallable类中重写call()方法
-
创建MyCallable类的对象
-
创建Future的实现类FutureTask对象,把MyCallable对象作为构造方法的参数
-
创建Thread类的对象,把FutureTask对象作为构造方法的参数
-
启动线程
-
再调用get方法,就可以获取线程结束之后的结果。
public class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
for (int i = 0; i < 100; i++) {
System.out.println("跟女孩表白" + i);
}
//返回值就表示线程运行完毕之后的结果
return "答应"; //区别前面的run方法,无返回值。
}
}
public class Demo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//线程开启之后需要执行里面的call方法
MyCallable mc = new MyCallable();
//可以获取线程执行完毕之后的结果.也可以作为参数传递给Thread对象
FutureTask<String> ft = new FutureTask<>(mc);
//创建线程对象
Thread t1 = new Thread(ft);
String s = ft.get();
//开启线程
t1.start();
//String s = ft.get();
System.out.println(s);
}
}
/* 说明
get()方法,获得线程运行之后的结果。如果线程还没有运行结束,那么get()方法会在这里死等。
*/
2.4 线程池
2.4.1 什么是线程池?为什么要用线程池(线程作用或优点)?
线程池里面存放了若干数量的线程,这些线程给 我们程序去使用,使用的时候,就去线程池里面 取一个,用完了再还回来,而不再是自我销毁。 线程池带来的好处:
- 降低资源消耗:通过池化技术重复利用已创建好的线程,降低线程创建和销毁造成的损耗。
- 提高响应速度:任务到达时,无需等待线程创建即可立即执行。
- 提高线程的可管理性:线程是稀缺资源,如果无限制创建,不仅会消耗系统资源,还会因为线程的不合理分布导致资源调度失衡,降低系统的稳定性。使用线程池可以进行统一的分配、调优和监控。
- 提供更多强大的功能:线程池具备可拓展性,允许开发人员向其中增加更多的功能。比如延迟定时线程池 ScheduledThreadPoolExecutor ,就允许任务延期执行或定期执行。
2.4.2 线程池参数七大参数
- corePoolSize:核心线程数量,一直正在保持运行的线程。
- maximumPoolSize:最大线程数,线程池允许创建的最大线程数。
- keepAliveTime:超出 corePoolSize 后创建的线程存活时间(当超过核心线程数后,又没有线程任务执行,达到该存活时间后,停止该线程)。
- unit:keepAliveTime 的时间单位。
- workQueue:任务队列,用于保持待执行的任务。
- threadFactory :线程池内部创建线程所用的工厂。
- handler:任务无法执行时的处理器(当任务被拒绝时)。
2.4.3 线程池的创建方式
分为以下几种创建方式:
- Executors.newCachedThreadPool():可缓存线程池
- Executors.newFixedThreadPool():可定长度,限制最大线程数
- Executors.newScheduledThreadPool():可定时线程池
- Executors.newSingleThreadExecutor():单例线程池
底层都是基于 ThreadPoolExecutor 构造函数封装
参考:线程和线程池的创建及使用_Henry_Lin_Wind的博客-CSDN博客_线程池创建线程以及线程的使用
2.4.4 线程池的执行流程?或者 线程池底层 ThreadPoolExecutor 底层实现原理
当线程数小于核心线程数时,创建线程。
当线程数大于等于核心线程数,且任务队列未满时,将任务放入任务队列。
当线程数大于等于核心线程数,且任务队列已满,有以下两种情况。
如果线程数小于最大线程数,创建线程。
如果线程数等于最大线程数,抛出异常,拒绝任务。
如果队列满了,且任务总数>最大线程数则当前线程走拒绝策略。可自定义拒绝异常,将该任务缓存到Redis、本地文件、mysql中,后期项目启动实现补偿。
拒绝策略有以下几种:
AbortPolicy:丢弃任务,抛出运行时异常。
CallerRunsPolicy:执行任务。
DiscardPolicy 忽视
DiscardOldestPolicy:从队列中剔除最先进入队列(最后一个执行)的任务。
实现 RejectedExecutionHandler 接口,可自定义处理器。
2.4.5 线程状态之间如何进行切换?
2.4.6 使用队列有什么需要注意的吗?
- 使用有界队列时,需要注意线程池满了后,被拒绝的任务如何处理。
- 使用无界队列时,需要注意如果任务的提交速度大于线程池的处理速度,可能会导致内存溢出。
2.4.7 线程只能在任务到达时才启动吗?
默认情况下,即使是核心线程也只能在新任务到达时才创建和启动。但是我们可以使用 prestartCoreThread(启动一个核心线程)或 prestartAllCoreThreads(启动全部核心线程)方法来提前启动核心线程
2.4.8 核心线程能成为核心线程吗?
虽然我们一直讲着核心线程和非核心线程,但是其实线程池内部是不区分核心线程和非核心线程的。只是根据当前线程池的工作线程数来进行调整,因此看起来像是有核心线程于非核心线程。
2.4.9 如何终止线程池?
shutdown:“温柔”的关闭线程池。不接受新任务,但是在关闭前会将之前提交的任务处理完毕。
shutdownNow:“粗暴”的关闭线程池,也就是直接关闭线程池,通过 Thread#interrupt() 方法终止所有线程,不会等待之前提交的任务执行完毕。但是会返回队列中未处理的任务。
2.4.10 在我们实际使用中,线程池的大小配置多少合适?
要想合理的配置线程池大小,首先我们需要区分任务是计算密集型还是I/O密集型。
对于计算密集型,设置线程数 = CPU数 + 1,通常能实现最优的利用率。
对于I/O密集型,网上常见的说法是设置 线程数 = CPU数 * 2 ,这个做法是可以的,但个人觉得不是最优的。
在我们日常的开发中,我们的任务几乎是离不开I/O的,常见的网络I/O(RPC调用)、磁盘I/O(数据库操作),并且I/O的等待时间通常会占整个任务处理时间的很大一部分,在这种情况下,开启更多的线程可以让 CPU 得到更充分的使用,一个较合理的计算公式如下:
线程数 = CPU数 * CPU利用率 * (任务等待时间 / 任务计算时间 + 1)
例如我们有个定时任务,部署在4核的服务器上,该任务有100ms在计算,900ms在I/O等待,则线程数约为:4 * 1 * (1 + 900 / 100) = 40个。
并且I/O的等待时间通常会占整个任务处理时间的很大一部分,在这种情况下,开启更多的线程可以让 CPU 得到更充分的使用,一个较合理的计算公式如下:
线程数 = CPU数 * CPU利用率 * (任务等待时间 / 任务计算时间 + 1)
例如我们有个定时任务,部署在4核的服务器上,该任务有100ms在计算,900ms在I/O等待,则线程数约为:4 * 1 * (1 + 900 / 100) = 40个。
当然,具体我们还要结合实际的使用场景来考虑。如果要求比较精确,可以通过压测来获取一个合理的值。
设置和获取线程名称【应用】
线程休眠
线程优先级
线程调度
-
两种调度方式
-
分时调度模型:所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间片
-
抢占式调度模型:优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的 CPU 时间片相对多一些
-
- 优先级相关方法
方法名 | 说明 |
---|---|
final int getPriority() | 返回此线程的优先级。优先级更高,抢到CPU的几率更高。 |
final void setPriority(int newPriority) | 更改此线程的优先级线程默认优先级是5;线程优先级的范围是:1-10 |
守护线程
线程同步
同步代码块解决数据安全问题【应用】
同步方法解决数据安全问题【应用】
死锁