目录
Thread 类中的start() 和 run()方法有什么区别?
Java虚拟机退出时Daemon线程中的finally块一定会执行?
什么是进程?
进程是系统中正在运行的一个程序,程序一旦运行就是进程。
进程可以看成程序执行的一个实例。进程是系统资源分配的独立实体,每个进程都拥有独立的地址空间。一个进程无法访问另一个进程的变量和数据结构,如果想让一个进程访问另一个进程的资源,需要使用进程间通信,比如管道,文件,套接字等。
什么是线程?
是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
线程的实现方式?
- 继承Thread类
- 实现Runnable接口
- 使用Callable和Future
Thread 类中的start() 和 run()方法有什么区别?
1.start()方法来启动线程,真正实现了多线程运行。这时无需等待run方法体代码执行完毕,可以直接继续执行下面的代码;通过调用Thread类的start()方法来启动一个线程, 这时此线程是处于就绪状态,并没有运行。 然后通过此Thread类调用方法run()来完成其运行操作的,这里方法run()称为线程体,它包含了要执行的这个线程的内容,Run方法运行结束,此线程终止。然后CPU再调度其它线程。
2.run()方法当作普通方法的方式调用。程序还是要顺序执行,要等待run方法体执行完毕后,才可继续执行下面的代码;程序中只有主线程--这一个线程,其程序执行路径还是只有一条, 这样就没有达到写线程的目的。
线程NEW状态
new创建一个Thread对象时,并没处于执行状态,因为没有调用start方法启动改线程,那么此时的状态就是新建状态。
线程RUNNABLE状态
线程对象通过start方法进入runnable状态,启动的线程不一定会立即得到执行,线程的运行与否要看cpu的调度,我们把这个中间状态叫可执行状态(RUNNABLE)。
线程的RUNNING状态
一旦cpu通过轮询货其他方式从任务可以执行队列中选中了线程,此时它才能真正的执行自己的逻辑代码。
线程的BLOCKED状态
线程正在等待获取锁。
- 进入BLOCKED状态,比如调用了sleep,或者wait方法
- 进行某个阻塞的io操作,比如因网络数据的读写进入BLOCKED状态
- 获取某个锁资源,从而加入到该锁的阻塞队列中而进入BLOCKED状态
线程的TERMINATED状态
TERMINATED是一个线程的最终状态,在该状态下线程不会再切换到其他任何状态了,代表整个生命周期都结束了。
下面几种情况会进入TERMINATED状态:
- 线程运行正常结束,结束生命周期
- 线程运行出错意外结束
- JVM Crash 导致所有的线程都结束
线程状态转化图
如何知道代码段被哪个线程调用?
System.out.println(Thread.currentThread().getName());
线程活动状态
public class Demo extends Thread{
@Override
public void run() {
System.out.println("run is "+Thread.currentThread().isAlive());
}
public static void main(String[] args) {
Demo demo = new Demo();
System.out.println("begin——"+demo.isAlive());
demo.start();
System.out.println("end——"+demo.isAlive());
}
}
sleep()方法
方法sleep()的作用是在指定的毫秒数内让当前的“正在执行的线程"休眠(暂停执行)。
如何优雅的设置睡眠时间?
jdk1.5 后,引入了一个枚举TimeUnit,对sleep方法提供了很好的封装。
比如要表达2小时22分55秒899毫秒。
Thread.sleep(8575899L);
TimeUnit.HOURS.sleep(2);
TimeUnit.MINUTES.sleep(22);
TimeUnit.SECONDS.sleep(55);
TimeUnit.MILLISECONDS.sleep(899);
可以看到表达的含义更清晰,更优雅。
停止线程
run方法执行完成,自然终止。
stop()方法,suspend()以及resume()都是过期作废方法,使用它们结果不可预期。
大多数停止一个线程的操作使用Thread.interrupt()等于说给线程打一个停止的标记,此方法不回去终止一个正在运行的线程,需要加入一个判断才能可以完成线程的停止。
interrupted 和isInterrupted
interrupted:判断当前线程是否已经中断,会清除状态。
isInterrupted:判断线程是否已经中断,不会清除状态。
yield
放弃当前cpu资源,将它让给其他的任务占用cpu执行时间。但放弃的时间不确定,有可能刚刚放弃,马上又获得cpu时间片。
测试代码:(cpu独占时间片)
class MyThread extends Thread{
@Override
public void run() {
long beginTime = System.currentTimeMillis();
int count = 0;
for (int i = 0; i < 50000000; i++) {
count+=i;
}
long endTime =System.currentTimeMillis();
System.out.println("使用时间:"+(endTime-beginTime)+"毫秒");
}
public static void main(String[] args) {
new MyThread().start();
}
}
加入yield,再来测试。(cpu让给其他资源导致速度变慢)
class MyThread extends Thread{
@Override
public void run() {
long beginTime = System.currentTimeMillis();
int count = 0;
for (int i = 0; i < 50000000; i++) {
Thread.yield();
count+=i;
}
long endTime =System.currentTimeMillis();
System.out.println("使用时间:"+(endTime-beginTime)+"毫秒");
}
public static void main(String[] args) {
new MyThread().start();
}
}
线程的优先级
在操作系统中,线程可以划分优先级,优先级较高的线程得到cpu资源比较多,也就是cpu有限执行优先级较高的线程对象中的任务,但是不能保证一定优先级高,就先执行。
Java的优先级分为1~10个等级,数字越大优先级越高,默认优先级大小为5。超出范围则抛出:java.lang.lllegalArgumentException 。
优先级继承特性
线程的优先级具有继承性,比如a线程启动b线程,b线程与a优先级是一样的。
谁跑的更快?
设置优先级高低两个线程,累加数字,看谁跑的快,上代码。
public class test {
public static void main(String[] args) {
ThreadDemo1 t1 = new ThreadDemo1();
ThreadDemo2 t2 = new ThreadDemo2();
t1.setPriority(2);
t2.setPriority(8);
t1.start();
t2.start();
try {
TimeUnit.SECONDS.sleep(2); // 主线程休眠 2 秒
t1.stop();
t2.stop();
System.out.println(t1.getName() + "|count: " + t1.getCount());
System.out.println(t2.getName() + "|count: " + t2.getCount());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class ThreadDemo1 extends Thread {
private volatile Long count = 0l;
public Long getCount() {
return count;
}
@Override
public void run() {
while (true) {
count++;
}
}
}
class ThreadDemo2 extends Thread {
private volatile Long count = 0l;
public Long getCount() {
return count;
}
@Override
public void run() {
while (true) {
count++;
}
}
}
线程种类
Java线程有两种,一种是用户线程,一种的守护线程
守护线程的特点
守护线程是一个比较特殊的线程,主要被用做程序中后台调度以及支持性工作。当Java虚拟机中不存在非守护线程时,守护线程才会随着JVM一同结束工作。
Java中典型的守护线程
GC(垃圾回收器)
如何设置守护线程
thread.setDaemon(true);
PS:Daemon属性需要再启动线程之前设置,不能再启动后设置。
Java虚拟机退出时Daemon线程中的finally块一定会执行?
Java虚拟机退出时Daemon线程中的finally块并不一定会执行。
代码示例:
public static void main(String[] args) {
Thread thread=new Thread(new DaemonRunnable(),"DaemonRunnable");
thread.setDaemon(true);
thread.start();
}
static class DaemonRunnable implements Runnable{
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(3);
}catch (Exception e){
e.printStackTrace();
}finally{
System.out.println("finally");
}
}
}
结果:
没有任何的输出,说明没有执行fnally。
设置线程上下文类加载器
获取线程上下文类加载器
public classLoader getContextClassLoader();
设置线程类加载器(可以打破Java类加载器的父类委托机制)
public void setContextClassLoader(ClassLoader cl)
join
join是指把指定的线程加入到当前线程,比如join某个线程a,会让当前线程b进入等待,直到a的生命周期结束,此期间b线程是处于blocked状态。
线程池
为什么使用线程池
几乎所有需要异步或者并发执行任务的程序都可以使用线程池。合理使用会给我们带来以下好处。
- 降低系统消耗:重复利用已经创建的线程降低线程创建和销毁造成的资源消耗。
- 提高响应速度:当任务到达时,任务不需要等到线程创建就可以立即执行。
- 提供线程可以管理性:可以通过设置合理分配、调优、监控。
线程池创建,作用
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
参数的作用如下:
- corePoolSize(核心线程数):线程池中的核心线程数,即使这些线程处于空闲状态,它们也不会被销毁,除非设置了
allowCoreThreadTimeOut
。 - maximumPoolSize(最大线程数):线程池中允许的最大线程数。当工作队列已满,并且当前线程数小于
maximumPoolSize
时,线程池会创建新的线程来处理任务。 - keepAliveTime(线程空闲时间):当线程数大于
corePoolSize
时,多余的空闲线程在终止前等待新任务的最长时间。 - unit(时间单位):
keepAliveTime
参数的时间单位,例如TimeUnit.SECONDS
。 - workQueue(工作队列):用于保存等待执行的任务的队列。当所有核心线程都在工作时,新提交的任务会被添加到这个队列中等待处理。Java提供了几种类型的队列,一
般来说可以选择如下阻塞队列:
ArrayBlockingQueue:基于数组的有界阻塞队列。
LinkedBlockingQueue:基于链表的阻塞队列。
SynchronizedQueue:一个不存储元素的阻塞队列。
PriorityBlockingQueue:一个具有优先级的阻塞队列。
- threadFactory(线程工厂):用于创建新线程的工厂。你可以通过实现
ThreadFactory
接口来定制线程的创建过程,例如设置线程名称、设置线程优先级等。 - handler(拒绝策略):当线程池无法处理新任务时(例如,工作队列已满,并且线程数已达到
maximumPoolSize
),这个RejectedExecutionHandler
会被调用。Java提供了几种预定义的拒绝策略,如AbortPolicy
(直接抛出异常)、CallerRunsPolicy
(调用者运行任务)、DiscardPolicy
(丢弃任务)和DiscardOldestPolicy
(丢弃队列中的最旧任务)。你也可以实现自己的拒绝策略。
向线程池提交任务
可以使用execute()和submit()两种方式提交任务。
execute():无返回值,所以无法判断任务是否被执行成功。
submit():用于提交需要有返回值的任务。线程池返回一个future类型的对象,通过这个future对象可以判断任务是否执行成功,并且可以通过future的get()来获取返回值,get()方法会阻塞当前线程知道任务完成。get(long timeout,TimeUnit unit)可以设置超市时间。
关闭线程池
可以通过shutdown()或shutdownNow()来关闭线程池。它们的原理是遍历线程池中的工作线程,然后逐个调用线程的interrupt来中断线程,所以无法响应终端的任务可以能永远无法停止。
shutdownNow首先将线程池状态设置成STOP,然后尝试停止所有的正在执行或者暂停的线程,并返回等待执行任务的列表。
shutdown只是将线程池的状态设置成shutdown状态,然后中断所有没有正在执行任务的线程。
只要调用两者之一,isShutdown就会返回true,当所有任务都已关闭,isTerminaed就会返回true。-般来说调用shutdown方法来关闭线程池,如果任务不一定要执行完,可以直接调用shutdownNow方法。
Demo如下
import java.util.concurrent.*;
public class ThreadPoolExecutorDemo {
public static void main(String[] args) {
// 定义线程池参数
int corePoolSize = 5; // 核心线程数
int maximumPoolSize = 10; // 最大线程数
long keepAliveTime = 60L; // 空闲线程存活时间,单位秒
TimeUnit unit = TimeUnit.SECONDS; // 时间单位
BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>(100); // 工作队列
ThreadFactory threadFactory = Executors.defaultThreadFactory(); // 使用默认线程工厂
RejectedExecutionHandler handler = new ThreadPoolExecutor.AbortPolicy(); // 拒绝策略:抛出异常
// 创建线程池
ThreadPoolExecutor executor = new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
keepAliveTime,
unit,
workQueue,
threadFactory,
handler
);
// 提交任务到线程池
for (int i = 0; i < 20; i++) {
final int taskId = i;
executor.submit(new Runnable() {
@Override
public void run() {
System.out.println("Running task: " + taskId + " with thread: " + Thread.currentThread().getName());
// 模拟任务执行时间
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
// 关闭线程池,不再接受新任务,但已提交的任务会继续执行
executor.shutdown();
// 等待所有任务执行完毕
try {
if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
// 等待超时,可以选择取消正在执行的任务
executor.shutdownNow();
}
} catch (InterruptedException e) {
// 当前线程在等待过程中被中断
executor.shutdownNow();
Thread.currentThread().interrupt();
}
}
}
线程池如何合理设置
配置线程池可以从以下几个方面考虑。
任务是cpu密集型、IO密集型或者混合型
任务优先级,高中低。
·任务时间执行长短。
任务依赖性:是否依赖其他系统资源。
cpu密集型可以配置可能小的线程,比如 n+1个线程。io密集型可以配置较多的线程,如2n个线程。混合型可以拆成io密集型任务和cpu密集型任务如果两个任务执行时间相差大,
否->分解后执行吞吐量将高于串行执行吞吐量。
否->没必要分解。
可以通过Runtime.getRuntime().availableProcessors()来获取cpu个数。
建议使用有界队列,增加系统的预警能力和稳定性。
Executor
从JDK5开始,把工作单元和执行机制分开。工作单元包括Runnable和Callable,而执行机制由Executor框架提供。
Executor框架的主要成员
ThreadPoolExecutor :可以通过工厂类Executors来创建。
可以创建3种类型的ThreadPoolExecutor:SingleThreadExecutor、FixedThreadPool、CachedThreadPool.
ScheduledThreadPoolExecutor :可以通过工厂类Executors来创建。
可以创建2中类型的ScheduledThreadPoolExecutor: ScheduledThreadPoolExecutor、SingleThreadScheduledExecutor
Future接口:Future和实现Future接口的FutureTask类来表示异步计算的结果。
Runnable和Callable:它们的接口实现类都可以被ThreadPoolExecutor或ScheduledThreadPoolExecutor执行。Runnable不能返回结果,Callable可以返回结果。
FixedThreadPool
可重用固定线程数的线程池。
源码:
public static ExecutorService newFixedThreadPool(int nThreads){return new ThreadPoolExecutor(nThreads,nThreads,
0L,TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());}
corePoolSize 和maxPoolSize都被设置成我们设置的nThreads。
当线程池中的线程数大于corePoolSize,keepAliveTime为多余的空闲线程等待新任务的最长时间,超过这个时间后多余的线程将被终止,如果设为0,表示多余的空闲线程会立即终止。
工作流程:
1.当前线程少于corePoolSize,创建新线程执行任务。
2.当前运行线程等于corePoolSize,将任务加入LinkedBlockingQueue。
3.线程执行完1中的任务,会循环反复从LinkedBlockingQueue获取任务来执行。
LinkedBlockingQueue作为线程池工作队列(默认容量Integer.MAX VALUE)。因此可能会造成如下赢下。
- 当线程数等于corePoolSize时,新任务将在队列中等待,因为线程池中的线程不会超过corePoolSize。
- maxnumPoolSize等于说是一个无效参数。
- keepAliveTime等于说也是一个无效参数。
- 运行中的FixedThreadPool(未执行shundown或shundownNow))则不会调用拒绝策略。
- 由于任务可以不停的加到队列,当任务越来越多时很容易造成OOM。
SingleThreadExecutor
源码
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1,1,
0l, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
corePoolSize和maxnumPoolSize被设置为1。其他参数和FixedThreadPool相同。
执行流程以及造成的影响同FixedThreadPoo1.
CachedThreadPool
源码
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
corePoolSize设置为0,maxmumPoolSize为Integer.MAX_VALUE。keepAliveTime为60秒。
工作流程:
- 初始化阶段:
- 当使用
newCachedThreadPool()
方法时,线程池在初始化阶段是空的,没有任何线程在运行。
- 当使用
- 新任务的提交:
- 当一个新任务被提交给线程池时,线程池会检查是否有空闲的线程可用。
- 如果有空闲线程,则直接将任务分配给其中一个空闲线程来执行。
- 如果没有空闲线程,线程池会立即创建一个新的线程,并将任务分配给该线程来执行。
- 当一个新任务被提交给线程池时,线程池会检查是否有空闲的线程可用。
- 工作线程的执行:
- 被分配任务的线程会执行任务的
run()
方法,完成具体的业务逻辑。 - 任务的执行时间可能会有所不同,取决于任务的复杂性和执行时间。
- 被分配任务的线程会执行任务的
- 线程池的线程数量控制:
- 在 CachedThreadPool 中,线程的数量是不受限制的(理论上)。但实际上,线程数量受到系统资源的限制。
- 如果当前没有可用的线程,线程池会立即创建一个新的线程。
- 如果线程池中有闲置的线程,并且这些线程在一段时间内(默认为60秒)没有执行任务,那么这些线程将会被终止并从线程池中移除,以释放系统资源。
- 任务队列:
- 尽管 CachedThreadPool 主要通过动态创建和销毁线程来处理任务,但在某些实现中,它也可能使用一个内部队列来管理任务。
- 当线程池中的线程都在忙碌时,新提交的任务可能会被放入这个队列中等待处理。但请注意,由于 CachedThreadPool 的动态线程特性,这种队列的使用可能并不普遍或显著。
- 异常处理:
- 在线程创建与回收过程中,线程池模块负责处理可能发生的异常。