本文将会介绍Java多线程中的重点知识,本文内容参考了网上的资料整理,主要为了自己看着方便,方便查找。
主要来源有:
多线程
一、为什么要使用多线程?
- 单核CPU的发展遇到了瓶颈,使用多核CPU来提高算力,并发编程能够更好的利用多核CPU的资源。
- 当遇到一个复杂任务时,如果我们只用一个线程的话,那么系统无论有多少个CPU核心,都会只使用其中的一个CPU,而创建多线程,可以将这些CPU充分地利用起来,任务执行效率便会显著提升。
二、什么是进程和线程
在Java中线程是以轻量的进程的方式实现的。
其他语言的实现方式可能不同,例如现在很火的Go语言,就是以协程的方式进行实现的。
1、进程
进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。
在 Java 中,当我们启动 main 函数时其实就是启动了一个 JVM 的进程,而 main 函数所在的线程就是这个进程中的一个线程,也称主线程。
2、线程
线程与进程相似,一个进程中的执行过程中会产生多个线程,线程是一个比进程更小的执行单位。
同类的多个线程共享进程的堆和方法区资源,每个线程有自己独立的程序计数器、虚拟机栈和本地栈。
系统中产生一个线程或者在多个线程之间切换工作时,消耗要比进程小的多,所以线程也被叫做轻量级进程。
3、进程与线程区别
- 进程包含线程。每个进程至少包含一个线程,即主线程(main函数所在)。
- 进程与进程之间不共享内存空间;同一个进程的线程共享同一块内存空间。
- 进程是系统分配资源的最小单位,线程是系统调度的最小单位。
下面放一张Guide哥的图解,方便理解:
3.1 程序计数器
程序计数器为什么是私有的?
程序计数器的私有主要是为了线程切换后能恢复到正确的执行位置。
3.2 虚拟机栈和本地方法栈
- **虚拟机栈:**每个Java方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。
- 本地方法栈: 和虚拟机栈所发挥的作用非常相似,区别是: 虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。
虚拟机栈和本地方法栈为什么是私有的?
为了保证线程中的局部变量不被别的线程访问到,虚拟机栈和本地方法栈是线程私有的。
3.3 堆和方法区
堆和方法区是所有线程共享的资源
- 堆是进程中最大的一块内存,主要用于存放新创建的对象 (几乎所有对象都在这里分配内存)。
- 方法区主要用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
4、线程的生命周期(状态)
Java 线程在运行的生命周期中的指定时刻只可能处于下面 6 种不同状态的其中一个状态(图源《Java 并发编程艺术》4.1.4 节)
线程的的状态不是固定的,而是随着代码的执行在不同的状态之间切换。Java 线程状态变迁如下图所示(图源《Java 并发编程艺术》4.1.4 节):
5、进程的状态
进程的状态与线程的生命周期大同小异。
在一个进程活动期间,至少具备三种状态:运行状态、就绪状态以及阻塞状态。
上图中各状态的的意义:
- 运行状态: 该时刻进程占用CPU。
- 就绪状态: 等待运行,由于其他进程的运行状态而暂时停止运行的进程。
- 阻塞状态: 正在等待某一事件的发生(如等待输入/输出操作的完成)而暂时停止运行,此时就算CPU给它资源,它也无法运行。
进程还有两种基本状态: - 创建状态: 进程正在被创建。
- 结束状态: 进程正在被销毁,正在从系统中消失。
所以,完整进程状态图:
状态转换:
状态转换 | 转换原因 |
---|---|
NULL -> 创建状态 | 一个新进程被创建时的第一个状态 |
创建状态 -> 就绪状态 | 当进程被创建完成并初始化后,一切就绪准备运行时,变为就绪状态,这个过程是很快的 |
就绪态 -> 运行状态 | 处于就绪状态的进程被操作系统的进程调度器选中后,就分配给 CPU 正式运行该进程 |
运行状态 -> 结束状态 | 当进程已经运行完成或出错时,会被操作系统作结束状态处理 |
运行状态 -> 就绪状态 | 处于运行状态的进程在运行过程中,由于分配给它的运行时间片用完,操作系统会把该进程变为就绪态,接着从就绪态选中另外一个进程运行 |
运行状态 -> 阻塞状态 | 当进程请求某个事件且必须等待时,例如请求 I/O 事件 |
阻塞状态 -> 就绪状态 | 当进程要等待的事件完成时,它从阻塞状态变到就绪状态 |
6、什么是上下文切换?
线程在执行过程中会有自己的运行条件和状态(也称上下文),比如上文所说到过的程序计数器,栈信息等。当出现如下情况的时候,线程会从占用 CPU 状态中退出。例如:
- 调用sleep()、wait()方法,主动让出CPU。
- 时间片用完,切换到下一个线程或者进程。
- 调用阻塞类的系统中断,比如IO请求,线程被阻塞。
- 被终止或者结束。
其中,前三种都发生了线程切换,线程切换意味着需要保存当前线程的上下文,留待线程下次占用 CPU 的时候恢复现场。并加载下一个将要占用 CPU 的线程上下文。这就是所谓的上下文切换。
三、创建线程
线程的创建在Java中有三种方式
- 继承Thread,重写run方法。
- 实现Runnable接口,重写run方法。
- 实现Callable接口。
1、继承Thread类
- 继承Thread来创建一个线程类
class MyThread extends Thread{
@Override
public void run() {
System.out.println("my thread run");
}
}
- 创建MyThread实例
Thread t = new MyThread();
- 调用start方法启动线程
t.start();
也可以使用匿名内部类的方式进行实现
Thread t2 = new Thread(){
@Override
public void run() {
System.out.println("匿名内部类的 run");
}
};
整体代码:
public class CreateThread1 {
public static void main(String[] args) {
Thread t = new MyThread();
t.start();
//匿名内部类重写run方法,本质还是继承Thread
Thread t2 = new Thread(){
@Override
public void run() {
System.out.println("匿名内部类的 run");
}
};
t2.start();
}
//继承Thread类重写run方法
private static class MyThread extends Thread{
@Override
public void run() {
System.out.println("my thread run");
}
}
}
2、实现Runnable接口
- 实现Runnable接口
class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println("my Runnable run");
}
}
- 创建 Thread 类实例, 调用 Thread 的构造方法时将 Runnable 对象作为 target 参数。
Thread t = new Thread(new MyRunnable());
- 调用start方法启动线程
t.start();
同样可以使用匿名内部类的方式进行实现,也可使用lambda方法实现
//以匿名内部类的方式实现Runnable接口
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("匿名内部类 Runnable");
}
});
//使用lambda的方法实现
Thread t3 = new Thread(()->{
System.out.println("lambda方式实现");
});
整体代码:
public class CreateThread2 {
public static void main(String[] args) {
Thread t = new Thread(new MyRunnable());
t.start();
//以匿名内部类的方式实现Runnable接口
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("匿名内部类 Runnable");
}
});
t2.run();
//使用lambda的方法实现
Thread t3 = new Thread(()->{
System.out.println("lambda方式实现");
});
t3.run();
}
//实现Runnable接口实现线程
private static class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println("my Runnable run");
}
}
}
3、实现Callable接口
Callable 是一个 interface。相当于把线程封装了一个 “返回值”。方便开发人员借助多线程的方式计算结果。
Callable 和 Runnable 相对, 都是描述一个 “任务”。Callable 描述的是带有返回值的任务, Runnable描述的是不带返回值的任务。
Callable 通常需要搭配 FutureTask 来使用。FutureTask 用来保存 Callable 的返回结果。因为Callable 往往是在另一个线程中执行的, 啥时候执行完并不确定。
FutureTask就负责这个等待结果出来的工作。
使用Callable可以简化代码,不需要再去写线程同步的代码了。
实现Callable接口创建线程:
public class CreateThread3 {
static int i = 0;
public static void main(String[] args) throws ExecutionException, InterruptedException {
//先实现callable
Callable<Integer> r = new Callable<Integer>() {
@Override
public Integer call() throws Exception {
try {
Thread.sleep(1000);
//将值返回出去
return 1;
} catch (InterruptedException e) {
throw new RuntimeException("出错了");
}
}
};
//再创建futuretask
FutureTask<Integer> task = new FutureTask<>(r);
//通过futuretask获取到线程中的值
Thread t = new Thread(task);
t.start();
System.out.println(task.get());
}
}
例如:创建线程计算 1 + 2 + 3 + … + 1000
- 不使用Callable:
- 创建一个类 Result , 包含一个 sum 表示最终结果, lock 表示线程同步使用的锁对象。
- main 方法中先创建 Result 实例, 然后创建一个线程 t。 在线程内部计算 1 + 2 + 3 + … + 1000。
- 主线程同时使用 wait 等待线程 t 计算结束。 (注意, 如果执行到 wait 之前, 线程 t 已经计算完了, 就不必等待了)。
- 当线程 t 计算完毕后, 通过 notify 唤醒主线程, 主线程再打印结果。
static class Result {
public int sum = 0;
public Object lock = new Object();
}
public static void main(String[] args) throws InterruptedException {
Result result = new Result();
Thread t = new Thread() {
@Override
public void run() {
int sum = 0;
for (int i = 1; i <= 1000; i++) {
sum += i;
}
synchronized (result.lock) {
result.sum = sum;
result.lock.notify();
}
}
};
t.start();
synchronized (result.lock) {
while (result.sum == 0) {
result.lock.wait();
}
System.out.println(result.sum);
}
}
- 使用Callable:
- 创建一个匿名内部类, 实现 Callable 接口。 Callable 带有泛型参数. 泛型参数表示返回值的类型。
- 重写 Callable 的 call 方法, 完成累加的过程. 直接通过返回值返回计算结果。
- 把 callable 实例使用 FutureTask 包装一下。
- 创建线程, 线程的构造方法传入 FutureTask . 此时新线程就会执行 FutureTask 内部的 Callable 的call 方法, 完成计算. 计算结果就放到了 FutureTask 对象中。
- 在主线程中调用
futureTask.get()
能够阻塞等待新线程计算完毕. 并获取到 FutureTask 中的结果。
Callable<Integer> callable = new Callable<Integer>() {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 1; i <= 1000; i++) {
sum += i;
}
return sum;
}
};
FutureTask<Integer> futureTask = new FutureTask<>(callable);
Thread t = new Thread(futureTask);
t.start();
int result = futureTask.get();
System.out.println(result);
4、为什么调用 start() 方法时会执行 run() 方法,为什么不能直接调用 run() 方法?
调用 start() 方法方可启动线程并使线程进入就绪状态,直接执行 run() 方法的话不会以多线程的方式执行。
详细: new 一个 Thread,线程进入了新建状态。调用 start()方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 start() 会执行线程的相应准备工作,然后自动执行 run() 方法的内容,这是真正的多线程工作。 但是,直接执行 run() 方法,会把 run() 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。
四、Thread类及常见方法
1、常见构造方法
方法 | 说明 |
---|---|
Thread() | 创建线程对象 |
Thread(Runnable target) | 使用 Runnable 对象创建线程对象 |
Thread(String name) | 创建线程对象,并命名 |
Thread(Runnable target, String name) | 使用 Runnable 对象创建线程对象,并命名 |
Thread(ThreadGroup group, Runnable target) | 线程可以被用来分组管理,分好的组即为线程组 |
Thread t1 = new Thread();
Thread t2 = new Thread(new MyRunnable());
Thread t3 = new Thread("Thread name");
Thread t4 = new Thread(new MyRunnable(), "Thread name");
2、常见属性
属性 | 获取方法 |
---|---|
ID | getId() |
名称 | getName() |
状态 | getState() |
优先级 | getPriority() |
是否为后台线程 | isDaemon() |
是否存活 | isAlive() |
是否被中断 | isInterrupted() |
- ID 是线程的唯一标识,不同线程不会重复
- 名称是各种调试工具用到
- 状态表示线程当前所处的一个情况,下面我们会进一步说明
- 优先级高的线程理论上来说更容易被调度到
- 关于后台线程,需要记住一点:JVM会在一个进程的所有非后台线程结束后,才会结束运行。
- 是否存活,即简单的理解为 run 方法是否运行结束了
- 线程的中断
代码示例:
public class ThreadDemo {
public static void main(String[] args) {
//线程各种方法的使用
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
try {
System.out.println(Thread.currentThread().getName()+":alive");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+":die");
}
},"线程1");
System.out.println(Thread.currentThread().getName()+"ID是:"+thread.getId());
System.out.println(Thread.currentThread().getName()+"名字是:"+thread.getName());
System.out.println(Thread.currentThread().getName()+"状态:"+thread.getState());
System.out.println(Thread.currentThread().getName()+"优先级:"+thread.getPriority());
System.out.println(Thread.currentThread().getName()+"后台线程:"+thread.isDaemon());
System.out.println(Thread.currentThread().getName()+"是否存活:"+thread.isAlive());
System.out.println(Thread.currentThread().getName()+"是否被中断:"+thread.isInterrupted());
thread.start();
while(thread.isAlive()){}
System.out.println(Thread.currentThread().getName()+"的状态:"+thread.getState());
}
}
3、线程的启动
只有调用了start方法,才是真正在操作系统底层创建出一个线程。
4、中断一个线程
两种方式来中断线程
- 共享的标记来进行交流,如使用带有volatile关键字的标志位。
- 调用 interrupt() 方法来通知,如 使用
Thread.interrupted()
或者Thread.currentThread().isInterrupted()
代替自定义标志位。
方法 | 说明 |
---|---|
public void interrupt() | 中断对象关联的线程,如果线程正在阻塞,则以异常方式通知,否则设置标志位 |
public static boolean interrupted() | 判断当前线程的中断标志位是否设置,调用后清除标志位 |
public boolean isInterrupted() | 判断对象关联的线程的标志位是否设置,调用后不清除标志位 |
thread 收到通知的方式有两种:
- 如果线程因为调用 wait/join/sleep 等方法而阻塞挂起,则以 InterruptedException 异常的形式通知,清除中断标志
- 当出现 InterruptedException 的时候, 要不要结束线程取决于 catch 中代码的写法. 可以选择忽略这个异常, 也可以跳出循环结束线程.
- 否则,只是内部的一个中断标志被设置,thread 可以通过
- Thread.interrupted() 判断当前线程的中断标志被设置,清除中断标志
- Thread.currentThread().isInterrupted() 判断指定线程的中断标志被设置,不清除中断标志
这种方式通知收到的更及时,即使线程正在 sleep 也可以马上收到。
示例代码:
public class ThreadInterrupt {
//线程中断
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
try {
while (!Thread.currentThread().isInterrupted()){
Thread.sleep(100000);
System.out.println(Thread.currentThread().getName()+"run");
}
} catch (InterruptedException e) {
//是否中断,由线程决定
e.printStackTrace();
}
}
});
t.start();
Thread.sleep(10000);
t.interrupt();
}
}
睡眠10s后中断
5、等待一个线程
方法 | 说明 |
---|---|
public void join() | 等待线程结束 |
public void join(long millis) | 等待线程结束,最多等 millis 毫秒 |
public void join(long millis, int nanos) | 与上相同,但可以有更高的精度 |
示例代码:
package Thread_api;
public class Join {
// 当前线程加入t线程,当前线程在此处等待
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
try {
for (int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName()+" run:"+i);
Thread.sleep(1000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
// t.join();
System.out.println(Thread.currentThread().getName()+"run");
}
}
6、获取当前线程引用
使用public static Thread currentThread();
方法获取,它的作用是返回当前线程对象的引用。
7、休眠当前线程
方法 | 说明 |
---|---|
public static void sleep(long millis) throws InterruptedException | 休眠当前线程 millis 毫 秒 |
`public static void sleep(long millis, int nanos) throws | |
InterruptedException` | 可以更高精度的休眠 |