多线程基础
并发编程是目前很多大公司面试考核的重点内容,为什么并发编程那么重要呢?这还要从CPU的发展讲起,考量一块CPU性能高不高的重要一个因素就是CPU的计算能力,起初,为了提升CPU的计算能力,硬件厂家们尽可能的缩小其内部每个计算单元所占的体积,保证每块CPU上尽可能的装有更多的计算单元,但受限于工艺水平,硬件厂家们发现,当一块CPU内计算单元的数目越多时,它成为残次品的概率也会提高,相应成本也会提高,为了解决这一问题,硬件厂家们发明了现在常见的多核CPU,及一个CPU上存在多个核心。为了充分利用多核技术,逐渐发展起来了多进程和多线程。下面就让我来带大家深入理解多线程的奥秘吧。
1.线程和进程
线程是什么?
一个线程就是一个 " 执行流 ". 每个线程之间都可以按照顺讯执行自己的代码 . 多个线程之间 " 同时 " 执行着多份代码.
为啥要有线程?
- 单核 CPU 的发展遇到了瓶颈. 要想提高算力, 就需要多核 CPU. 而并发编程能更充分利用多核 CPU资源.
- 有些任务场景需要 "等待 IO", 为了让等待 IO 的时间能够去做一些其他的工作, 也需要用到并发编程.
进程和线程的区别?
- 进程是包含线程的. 每个进程至少有一个线程存在,即主线程。
- 进程和进程之间不共享内存空间. 同一个进程的线程之间共享同一个内存空间.
- 进程是系统分配资源的最小单位,线程是系统调度的最小单位。
虽然多进程也能实现 并发编程 , 但是线程比进程更轻量 .
- 创建线程比创建进程更快.
- 销毁线程比销毁进程更快.
- 调度线程比调度进程更快.
最后 , 线程虽然比进程轻量 , 但是人们还不满足 , 于是又有了 " 线程池 "(ThreadPool) 和 " 协程 "
(Coroutine)
Java 的线程 和 操作系统线程 的关系
线程是操作系统中的概念 . 操作系统内核实现了线程这样的机制 , 并且对用户层提供了一些 API 供用户使用( 例如 Linux 的 pthread 库 ).
Java 标准库中 Thread 类可以视为是对操作系统提供的 API 进行了进一步的抽象和封装
使用jconsole观察线程
在jdk的bin文件夹下,Java为我们提供了一个工具jconsole.exe,启动这个工具我们便可以清晰的观察自己电脑内Java线程的启动和销毁了。
2.创建线程的多种方式
方法一:继承Thread来创建一个线程类。
代码语言:javascript复制//创建一个线程类
class MyThread extends Thread {
@Override
public void run() {
System.out.println("这里是线程运行的代码");
}
}
MyThread t = new MyThread();//创建 MyThread 类的实例
t.start(); // 线程开始运行
方法二: 实现Runnable接口
代码语言:javascript复制class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("这里是线程运行的代码");
}
}
//创建 Thread 类实例, 调用 Thread 的构造方法时将 Runnable 对象作为 target 参数.
Thread t = new Thread(new MyRunnable());
t.start(); // 线程开始运行
方法三:使用ExecutorService、Callable、Future实现有返回结果的多线程
代码语言:javascript复制import java.util.concurrent.*;
import java.util.Date;
import java.util.List;
import java.util.ArrayList;
/**
* 有返回值的线程
*/
@SuppressWarnings("unchecked")
public class Test {
public static void main(String[] args) throws ExecutionException,
InterruptedException {
System.out.println("----程序开始运行----");
Date date1 = new Date();
int taskSize = 5;
// 创建一个线程池
ExecutorService pool = Executors.newFixedThreadPool(taskSize);
// 创建多个有返回值的任务
List<Future> list = new ArrayList<Future>();
for (int i = 0; i < taskSize; i ) {
Callable c = new MyCallable(i " ");
// 执行任务并获取Future对象
Future f = pool.submit(c);
// System.out.println(">>>" f.get().toString());
list.add(f);
}
// 关闭线程池
pool.shutdown();
// 获取所有并发任务的运行结果
for (Future f : list) {
// 从Future对象上获取任务的返回值,并输出到控制台
System.out.println(">>>" f.get().toString());
}
Date date2 = new Date();
System.out.println("----程序结束运行----,程序运行时间【"
(date2.getTime() - date1.getTime()) "毫秒】");
}
}
class MyCallable implements Callable<Object> {
private String taskNum;
MyCallable(String taskNum) {
this.taskNum = taskNum;
}
public Object call() throws Exception {
System.out.println(">>>" taskNum "任务启动");
Date dateTmp1 = new Date();
Thread.sleep(1000);
Date dateTmp2 = new Date();
long time = dateTmp2.getTime() - dateTmp1.getTime();
System.out.println(">>>" taskNum "任务终止");
return taskNum "任务返回运行结果,当前任务时间【" time "毫秒】";
}
}
tips:
上述的方法一,方法二均可以通过匿名内部类的方式创建线程,还可用lambda表达式来简化
代码语言:javascript复制// 使用匿名类创建 Thread 子类对象
Thread t1 = new Thread() {
@Override
public void run() {
System.out.println("使用匿名类创建 Thread 子类对象");
}
};
// 使用匿名类创建 Runnable 子类对象
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("使用匿名类创建 Runnable 子类对象");
}
});
// 使用 lambda 表达式创建 Runnable 子类对象
Thread t3 = new Thread(() -> System.out.println("使用匿名类创建 Thread 子类对象"));
Thread t4 = new Thread(() -> {
System.out.println("使用匿名类创建 Thread 子类对象");
});
3.Thread类及其常见方法
Thread 类是 JVM 用来管理线程的一个类,换句话说,每个线程都有一个唯一的 Thread 对象与之关联。 Thread 类的对象 就是用来描述一个线程执行流的, JVM 会将这些 Thread 对象组织起来,用于线程调度,线程管理。
Thread类的常见构造方法
方法 | 说明 |
---|---|
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 t4 = new Thread(new MyRunnable(), "这是我的名字");
Thread类的常见属性
属性 | 获取方法 |
---|---|
ID | getId() |
名称 | getName() |
状态 | getState() |
优先级 | getPriority() |
是否后台线程 | isDaemon() |
是否存活 | isAlive() |
是否被中断 | isInterrupted() |
- ID 是线程的唯一标识,不同线程不会重复
- 名称是各种调试工具用到
- 状态表示线程当前所处的一个情况,下面我们会进一步说明
- 优先级高的线程理论上来说更容易被调度到
- 关于后台线程,需要记住一点:JVM会在一个进程的所有非后台线程结束后,才会结束运行。
- 是否存活,即简单的理解,为 run 方法是否运行结束了
- 线程的中断问题,下面我们进一步说明
Thread类的常见方法
(1).线程启动(start()方法)
前面我们知道覆写run方法来创建一个线程对象,但run方法只是为了给线程启动提供一个要做的事情清单,不能通过调用run方法来使线程启动。
调用start方法,可以真正在操作系统的底层创建一个线程。
(2).线程中断(interrupt()方法)
interrupt()、interrupted() 和 isInterrupted() 方法是 Java 中用于线程中断的相关方法,它们有着不同的功能和用法。下面我将逐一解释它们的区别。
我们把run方法运行结束叫做线程的中断,通常情况下我们为了保证线程的存在,会在run方法内部自定义设置一个循环条件等于true,让循环能一直存在,我们把这个条件叫做标记值,当标记值被改为false时,循环结束,run方法也会执行完毕。当run方法内的代码运行完毕之后,内核中的线程就会被摧毁,称为线程中断。除了自定义标记值之外,Thread内部还提供了一个boolean类型的变量可以作为标记值(isInterrupted),
- 清除中断标志:将 isInterrupted 的值设置为 false
- 设置中断标志:将 isInterrupted 的值 设置为 true
3.2.1 interrupt()
当调用线程的 interrupt() 方法时,如果线程处于阻塞状态,就会抛出 InterruptedException 异常并清除中断标志,接着结束睡眠。
如果线程未处于阻塞状态,仅仅是设置中断标志,并不是真正的中断线程。线程的中断由线程决定,线程可以通过检查中断标志来决定是否中断自己的执行。
代码语言:javascript复制public class MyThread extends Thread {
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
// 线程任务逻辑
// ...
}
}
}
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start();
// 等待一段时间后中断线程
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
thread.interrupt();
}
3.2.2 interrupted()
interrupted() 方法是一个静态方法,用于判断当前线程是否被中断,并返回中断状态,而且在判断中断状态后,还会自动清除中断标志。
- 如果线程没有被中断,则返回 false;
- 如果线程被中断,则返回 true;
public class ThreadDemo {
private static class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i ) {
System.out.println(Thread.interrupted());
}
}
}
public static void main(String[] args) throws InterruptedException {
MyRunnable target = new MyRunnable();
Thread thread = new Thread(target, "李四");
thread.start();
thread.interrupt();
}
}
3.2.3 isInterrupted()
isInterrupted() 用于检查当前线程是否被中断,并且不会清除线程的中断状态。
- 当线程被中断时,返回 true。
- 当线程没有被中断时,返回 false。
public class ThreadDemo {
private static class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i ) {
System.out.println(Thread.currentThread().isInterrupted());
}
}
}
public static void main(String[] args) throws InterruptedException {
MyRunnable target = new MyRunnable();
Thread thread = new Thread(target, "李四");
thread.start();
thread.interrupt();
}
}
(3).线程等待(join()方法)
java中线程是并发执行的,线程的调度是抢占式的,所以操作系统对于线程调用的顺序的不知道的,我们无法判断哪个线程先结束,为了能够控制线程的结束顺序,因此 java 提供了 join()方法。join方法内可以添加long类型参数,译为等待线程结束,最多等多少毫秒。
在 Java 中,join() 方法是 Thread 类的一个方法,它允许一个线程等待另一个线程执行完毕再执行接下来的步骤。当一个线程调用另一个线程的 join() 方法时,调用线程将被阻塞。
代码语言:javascript复制public class JoinExample {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
try {
// 模拟线程执行耗时的操作
Thread.sleep(2000);
System.out.println("子线程执行完毕");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
thread.start(); // 启动子线程
thread.join(); // 主线程等待子线程执行完毕
System.out.println("主线程继续执行");
}
}
(4).获取线程引用(currentThread()方法)
public static Thread currentThread(); | 返回当前线程对象的引用 |
---|
public class ThreadDemo {
public static void main(String[] args) {
Thread thread = Thread.currentThread();
System.out.println(thread.getName());
}
}
(5).线程休眠(sleep()方法)
让线程休眠一段时间,不去参与CPU的竞争,与阻塞状态不同,需要注意的是,因为线程的调度室不可控的,所以,这个方法只能保证实际休眠时间>=参数的设置的休眠时间。
代码语言:javascript复制public class ThreadDemo {
public static void main(String[] args) throws InterruptedException {
System.out.println(System.currentTimeMillis());
Thread.sleep(3 * 1000);
System.out.println(System.currentTimeMillis());
}
}
4.线程的各种状态
线程的状态是一个枚举类型 Thread.State,我们可以通过这段代码得到线程的状态:
代码语言:javascript复制public class ThreadState {
public static void main(String[] args) {
for (Thread.State state : Thread.State.values()) {
System.out.println(state);
}
}
}
Java中线程的状态主要有以下几种:
(1)新建状态(NEW) Thread对象创建了。但是还没有调用start,操作系统内核中的线程还没创建
(2)终止状态(TERMINATED) run方法执行完毕,内核中的线程已经销毁
(3)可运行状态(RUNNABLE) 线程就绪(正在cpu上执行)
(4)超时等待状态(TIMED_WAITING) 由sleep等带有有时间的方法进入的阻塞
(5)等待状态(WAITING) 不带时间的阻塞,线程在等待其他线程的特定操作,如wait / join
(6)阻塞状态(BLOCKED) 由于锁竞争产生的阻塞
5.使用多线程的风险(线程安全问题)
虽然我们现在使用的CPU拥有多个核心,在正常使用计算机时,一个核心上仍然会同时运行多个线程,怎样保持多个线程同时运行呢,其实是通过高频切换来完成的,即一个线程在CPU上运行一会,再切换为另一个线程运行,因为中间切换的时间很短,且CPU执行速率非常快,在宏观层面我们就认为是多个线程在同时运行。如果同时运行的这几个线程在操作同一件事,且这件事不具有原子性就很容易发生线程安全问题。
什么是原子性
我们把一段代码想象成一个房间,每个线程就是要进入这个房间的人。如果没有任何机制保证, A 进入房间之后,还没有出来;B 是不是也可以进入房间,打断 A 在房间里的隐私。这个就是不具备原子性的。
那我们应该如何解决这个问题呢?是不是只要给房间加一把锁, A 进去就把门锁上,其他人是不是就进不来了。这样就保证了这段代码的原子性了。
有时也把这个现象叫做同步互斥,表示操作是互相排斥的。
一条 java 语句不一定是原子的,也不一定只是一条指令
比如我们之前常看到的 n ,其实是由三步操作组成的:
1. 从内存把数据读到 CPU
2. 进行数据更新
3. 把数据写回到 内存
当两个线程同时执行n 操作时,有可能其中一个线程刚执行完第一步,CPU就切换线程了,此时第二个线程执行n 的操作执行完毕重新写回内存,这时CPU重新切回第一个线程,第二个线程继续之前的执行第二步操作,因为两个线程都是在同一个n的基础上进行了 1操作,最后写会内存中的n只加了一次1,但我们执行了两次n 内存中的正确结果应该是n要加两次1,就会出现bug,想像一下,如果这种情况出现在银行存钱的时候,你和你家人同存钱,却只显示存进去了一份,这会是一件多么严重的bug,为了解决这一问题,Java也给我们提供了很多方法,我会在下篇文章详细介绍如何避免发生线程安全问题。
❤️