并发编程1:线程的基本概念
一、进程、线程、用户线程&原生线程、优先级、守护线程
什么是进程
- 是程序一次执行的过程,是系统运行程序的基本单位。
- 系统运行一次程序,就是一个进程从创建到关闭的过程。
- Java 项目从 main 方法启动,就是启动了一个 JVM 进程,而 main 函数就是由进程中的一个线程负责执行,这个线程称为主线程。
什么是线程
- 线程和进程相似,但是是一个比线程更小的单位,线程间的切换比进程的切换负担小得多,所以线程也称为轻量级进程。
- 一个进程执行时可以有多个线程,JAVA 线程之间可以共享堆和方法区资源,但是每个线程有自己的程序计数器、虚拟机栈、和本地方法栈
什么是用户线程&原生线程
- 用户线程:JDK1.2 之前 JVM 使用的是绿色线程自己摸你的多线程,这就是用户线程。用户线程对比原生线程使用起来有一些限制。用户线程由用户空间程序管理和调度,运行在用户空间。JDK1.2 之后,JAVA 线程,就是用的是内核线程了。
- 内核线程:由操作系统内核管理和调度的线程,运行在内核空间,可以操作系统提供的异步 I/O
什么是守护线程&非守护线程
- JAVA 中可以在线程 start 前使用 setDaemon() 方法指定其为守护线程,守护线程和非守护线程最大的区别在于他们如何影响程序的结束。
- 非守护线程执行完毕时,程序就会结束执行。不论是否有守护线程仍在执行。
线程优先级
可以对线程设置优先级,默认优先级为 Thread.NORM_PRIORITY(5)
,优先级从 1~10 优先级越大优先执行的概率越高但并不能保证执行顺序。
多线程和上下文切换
- 线程在执行过程中,会有自己的运行条件和状态这就是线程的上下文。例如 Java 线程私有的程序计数器,虚拟机栈、本地方法栈。
- 线程在执行过程中可能出现阻塞、等待、或者 CPU 时间片执行完毕,的情况要让出 CPU 等待下一次执行。这种情况下就会发生线程切换,下一次执行时又要恢复线程的数据,这就是上下文切换
- 因为需要上下文切换,如果是 CPU 密集型任务,线程越多反而有更多的性能损耗;
二、线程的状态和切换
JAVA 的线程状态定义在 Thread.States
枚举类中
- NEW:Thread对象已经创建,但是还没有开始执行。
- RUNNABLE:Thread对象正在Java虚拟机中运行。
- BLOCKED : Thread对象正在等待锁定。
- WAITING:Thread 对象正在等待另一个线程的动作。
- TIME_WAITING:Thread对象正在等待另一个线程的操作,但是有时间限制。
- TERMINATED:Thread对象已经完成了执行。
Java 的线程状态没有区分 操作系统层面的 Running 和 Ready 状态,而是只有 RUNNABLE 状态的原因
- 因为 现在的 CPU 都讲 CPU 资源进行分片循环调度,由于时间片非常小一般是 10~20ms,线程切换的非常快所以区分 Ready 和 Running 意义不大
线程的终止方式
- 调用 stop 方法强行终止,但是是强行终止线程可能任意时刻被中指导致任务不完整,资源未释放等问题。
- 线程中保存 volite 的退出标识,线程中循环判断该标识,是否需要终止线程,和 interrupt 很类似。
- 使用线程的中断标识,interrupt,可以通过isInterrupted()和 interrupted() 来感知外界传入的中断状态,决定是否中断线程。interrupt 碰上 sleep 或者 wait 状态 则一定会抛出异常
class Task implements Runnable {
@Override
public void run() {
try {
while (!Thread.currentThread().isInterrupted()) {
System.out.println("子线程正在运行...");
Thread.sleep(1000); // 让子线程休眠1秒
}
} catch (InterruptedException e) {
System.out.println("子线程被中断");
} finally {
System.out.println("子线程结束");
}
}
}
三、线程的创建 Runnable 接口 和 Thread 类
线程可以直接集成 Thread 类,也可以实现 Runnable 接口将 Runnable 对象作为参数创建 Thread
Runnable 接口
- Runnable 接口只有一个 run 方法,用来记录线程需要执行的任务内容
Thread 类
- 获取和设置 Thread 对象信息
- getId():该方法返回Thread对象的标识符。该标识符是在钱程创建时分配的一个正整数。在线程的整个生命周期中是唯一且无法改变的。
- getName()/setName():这两种方法允许你获取或设置Thread对象的名称。这个名称是一个String对象,也可以在Thread类的构造函数中建立
- getPriority()/setPriority():你可以使用这两种方法来获取或设置Thread对象的优先级。
- isDaemon()/setDaemon():这两种方法允许你获取或建立Thread对象的守护条件
- getState():该方法返回Thread对象的状态。
- interrupt():中断目标线程,给目标线程发送一个中断信号,线程被打上中断标记。
- interrupted():判断目标线程是否被中断,但是将清除线程的中断标记。
- **isinterrupted():**判断目标线程是否被中断,不会清除中断标记。
- **sleep(long ms):**该方法将线程的执行暂停ms时间。
- **join():**暂停线程的执行,直到调用该方法的线程执行结束为止。可以使用该方法等待另一个Thread对象结束。举例 : A 线程中执行 b.join() 则执行流程为
A->暂停->b 开始执行->b 执行完毕->A 继续
- setUncaughtExceptionHandler() 当线程执行出现未校验异常时,该方法用于建立未校验异常的控制器。
- currentThread():Thread类的静态方法,返回实际执行该代码的Thread对象。
- suspend() 和 resume() 方法:两个方法配套使用,suspend()使得线程进入阻塞状态,并且不会自动恢复,必须其对应的resume() 被调用,才能使得线程重新进入可执行状态。典型地,suspend() 和 resume() 被用在等待另一个线程产生的结果的情形:测试发现结果还没有产生后,让线程阻塞,另一个线程产生了结果后,调用 resume() 使其恢复。
- yield() 方法使得线程放弃当前分得的 CPU 时间,但是不使线程阻塞,即线程仍处于可执行状态,随时可能再次分得 CPU 时间。调用 yield() 的效果等价于调度程序认为该线程已执行了足够的时间从而转到另一个线程。
Callable 接口和 Runnable 接口
Callable 接口是一个和 Runnable 接口非常相似的接口
- Callable 接口内部是带有返回值的 call()方法,而 Runnable 接口是没有返回值的 run()方法。
- Callable 接口一般配合 Future 来一起使用,使用 Future 来包装 Callable 对象,实现 callable 的异步执行和后续的结果获取。
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyCallable myCallable = new MyCallable();
//future接口不能被异步执行,FutureTask实现了RunnableFuture接口,间接实现了Runnable接口
FutureTask<String> stringFuture = new FutureTask<String>(myCallable);
//使用Thread执行或者提交给线程池执行
Thread thread = new Thread(stringFuture);
System.out.println("外部调用任务开始" + new Date());
thread.start();
//这里阻塞获取结果,如果出结果了可以直接获取,否则需要等待任务执行完毕
System.out.println("获取结果:" + stringFuture.get());
}
/**
* 这是自定义的future类
*
* @author liuyp
* @date 2023/11/19
*/
static class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
System.out.println("任务执行开始,预计耗时5s");
Thread.sleep(5000);
return "我是任务执行的结果";
}
}