本文主要介绍了Java并发编程的基础知识,包括进程、线程、协程的概念及关系,并发、并行、串行的含义,CPU 核心数和线程数的关系、上下文切换,线程创建方式及原理,常用方法如start与run方法、线程让步方法等,还涉及线程中断、合并、存活状态、守护线程、状态转换以及通过Callable和FutureTask创建有返回结果的线程等内容。梳理本文的目的是为了加深对java线程的理解。
一、进程、线程、协程
1、什么是进程?
在操作系统中,进程是基本的资源分配单位,操作系统通过进程来管理计算机的资源,如CPU、内存、磁盘等。每个进程都有一个唯一的进程标识符(PID),用于区分不同的进程。通俗说法:可看做是正在执行的程序如QQ.exe、 微信、浏览器等。
2、什么是线程?
线程是操作系统中的基本执行单元(能够直接执行的最小代码块),它是进程中的一个实体,是CPU调度和分派的基本单位。一个进程可以包含多个线程,每个线程都可以独立执行不同的任务,但它们共享进程的资源。同一时刻,一个CPU核心只能运行一个线程,也就是CPU内核和同时运行的线程数是1:1的关系,也就是说8核CPU同时可以执行8个线程的代码。
3、什么是协程?
协程又叫虚拟线程、纤程,可以在一个线程内部创建多个协程,这些协程之间可以共享同一个线程的资源。协程是在同一个进程内部运行的,不需要操作系统的介入,可以在用户空间内实现协作式多任务处理。因此协程的创建和销毁开销很小,可以更高效地利用系统资源。Java19才开始支持协程。
4、进程、线程、协程之间的关系
首先需要有进程,然后在进程可以创建多个线程,线程是依附在进程里面的,线程里面可以包含多个协程,进程之间不共享全局变量,线程之间共享全局变量,但是要注意资源竞争的问题(线程安全问题),协程之间共享同一个线程的资源。
二、并发、并行、串行
1、什么是并发?
在操作系统中,安装了多个程序,并发的是同一时间段内宏观上有多个程序同时运行,这在单CPU系统中,每一时刻只能有一道程序执行,即微观上这些程序是分时的交替运行,只不过是给人的感觉是同时运行,那是因为分时交替运行的时间是非常短的。
2、什么是并行?
在多核CPU系统中,这些同一时刻的程序可以分配到多个处理器上(CPU),实现多任务并行执行,即利用每个处理器来处理一个可以并发执行的程序,这样多个程序便可以同时执行。目前电脑市场上说的多核CPU,便是多核处理器,核越多,并行处理的程序越多,能大大的提高电脑运行的效率。
3、什么是串行?
如单核CPU,同一时刻只能运行一个程序,如果存在多个程序,需要按照先后顺序执行。我打开qq后,不能再同时打开微信,只能等qq执行完成(关闭)后才能打开微信,线程的串行亦是如此,一次只能执行一个线程代码指令,其他线程需要排队等待。
三、CPU核心数和线程数的关系
目前主流CPU都是多核的,线程是CPU调度的最小单位。同一时刻,一个CPU核心只能运行一个线程,也就是CPU内核和同时运行的线程数是1:1的关系,也就是说8核CPU同时可以执行8个线程的代码。但Intel引入超线程技术后,产生了逻辑处理器的概念,使核心数与线程数形成1:2的关系。在Java中提供了Runtime.getRuntime().availableProcessors(),可以让我们获取当前的CPU核心数,注意这个核心数指的是逻辑处理器数。获得当前的CPU核心数在并发编程中很重要,并发编程下的性能优化往往和CPU核心数密切相关。
四、CPU上下文切换
1、为了提高并发性,启动线程越多越好?
由于现在大多计算机都是多核CPU,多线程往往会比单线程更快,更能够提高并发,但提高并发并不意味着启动更多的线程来执行。更多的线程意味着线程创建销毁开销加大、上下文非常频繁,你的程序反而不能支持更高的TPS。
2、什么是时间片?
多任务系统往往需要同时执行多道作业。作业数往往大于机器的CPU数,然而一颗CPU同时只能执行一项任务,如何让用户感觉这些任务正在同时进行呢?操作系统的设计者巧妙地利用了时间片轮转的方式,时间片是CPU分配给各个任务(线程)的时间。
3、什么是CPU上下文切换?
线程上下文是指某一时间点CPU寄存器和程序计数器的内容,CPU通过时间片分配算法来循环执行任务(线程),因为时间片非常短,所以CPU通过不停地切换线程执行。换言之,单CPU这么频繁,多核CPU一定程度上可以减少上下文切换。
五、线程创建方式及创建原理
1、线程创建方式
(1)、继承Thread类
- 显示定义
// 定义Thread类
public class TestThread extends Thread {
@Override
public void run() {
// 线程执行的业务代码...
System.out.println("线程Thread");
}
}
// 开启异步线程
TestThread testThread = new TestThread();
testThread.start();
- 匿名定义
public static void createThread() {
// new Thread匿名内部类
Thread t2 = new Thread() {
@Override
public void run() {
// 线程执行的业务代码...
System.out.println("线程Thread");
}
};
t2.start();
}
(2)、实现Runnable接口
实际使用的话,建议采用实现Runnable接口的方式创建线程, 因为继承Thread类存在父类限制的问题,java只支持继承一个父类, 而可以实现多个接口,可扩展性更强。
- 显示定义
public class TestRunnable implements Runnable {
@Override
public void run() {
// 线程执行的业务代码...
System.out.println("线程Runnable");
}
}
// 开启异步线程
Thread thread = new Thread(new TestRunnable());
thread.start();
- 匿名定义
public static void createRunnable() {
// new Runnable匿名内部类
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
// 线程执行的业务代码...
System.out.println("线程Runnable");
}
});
t1.start();
}
// 因为Runnable 是函数式接口,所以可以用lambda表达式来简化以上代码,如下所示:
public static void createRunnable() {
// new Runnable匿名内部类
Thread t1 = new Thread(() -> {
// 线程执行的业务代码...
System.out.println("线程Runnable");
});
t1.start();
}
2、线程创建原理
第一种:继承Thread,并重写Thread的run方法,启动过程为:thread.start() -> 中间过程 -> thread.run()。
第二种:实例化Thread,传递一个Runnable任务,启动过程为:thread.start() -> 中间过程 -> thread.run() -> runnable.run()。 注意两处标粗的thread.run(),此run非彼run。第一处run方法已经被我们重写了,是真正的业务逻辑,而第二处是Thread类里面的默认逻辑,它会调用runnable.run()方法,业务逻辑都在runnable.run()里面。
六、线程常用方法
1、start与run方法
- run方法是同步方法,run方法的作用是存放任务代码,执行run方法它不会产生新线程,run方法可以被执行无数次;
- start方法是异步方法,start方法是启动线程,start方法会产生新线程,start方法只能被执行一次,原因就在于线程不能被重复启动。
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
log.info("2.子线程启动...");
});
log.info("1.开始创建线程");
// t1.run(); // 同步执行
t1.start(); // 异步执行
t1.start(); // 异步执行,测试重复启动一个线程
log.info("3.主线程结束");
}
// 报错信息如下:
15:22:56.280 [Thread-0] INFO StartAndRun - 2.子线程启动...
Exception in thread "main" java.lang.IllegalThreadStateException Create breakpoint
at java.lang.Thread.start(Thread.java:708)
at StartAndRun.main(StartAndRun.java:17)
2、setName、getName与sleep方法
(1)、setName与getName方法
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
System.out.println(Thread.currentThread().getName());
log.info("2.子线程启动...");
});
log.info("1.开始创建线程");
t1.setName("eagle");
t1.start();
log.info("3.主线程结束");
System.out.println(Thread.currentThread().getName());
}
// 执行结果如下:
09:16:15.751 [main] INFO StartAndRun - 1.开始创建线程
09:16:15.761 [main] INFO StartAndRun - 3.主线程结束
main
eagle
09:16:15.762 [徐庶1] INFO StartAndRun - 2.子线程启动...
(2)、sleep方法
- sleep简单使用
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
try {
// 单位毫秒
// Thread.sleep(1000);
// 这种方式可读性更好
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
// 其他线程可以使用interrupt方法打断正在睡眠的线程,使其抛InterruptedException异常
e.printStackTrace();
}
System.out.println("线程执行完毕, 从睡眠中醒来, 线程结束");
});
}
- sleep解决cpu飙升问题
public class SleepThread {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
while (true) {
try {
// 让出cpu时间片, 让其他线程去执行,减少死循环一直对cpu的消耗
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t1.start();
}
}
// springboot中内嵌的tomcat采用sleep方式创建阻塞的非守护线程
// Start the server to trigger initialization listeners
this.tomcat.start();
// We can re-throw failure exception directly in the main thread
rethrowDeferredStartupExceptions();
try {
ContextBindings.bindClassLoader(context, context.getNamingToken(), getClass().getClassLoader());
} catch (NamingException ex) {
// Naming is not enabled. Continue
}
// Unlike Jetty, all Tomcat threads are daemon threads. We create a
// blocking non-daemon to stop immediate shutdown
// 重点看这个方法的实现
startDaemonAwaitThread();
// startDaemonAwaitThread具体实现如下:
private void startDaemonAwaitThread() {
Thread awaitThread = new Thread(new Runnable() {
@Override
public void run() {
TomcatWebServer.this.tomcat.getServer().await();
}
}, "container-" + (containerCounter.get()));
awaitThread.setContextClassLoader(getClass().getClassLoader());
awaitThread.setDaemon(false);
awaitThread.start();
}
// 继续看下26行 await()方法的实现- 44行 while循环中处理 降低cpu的消耗
@Override
public void await() {
// Negative values - don't wait on port - tomcat is embedded or using a different port
if (getPortWithOffset() == -2) {
// undocumented yet - for embedding apps that are around, also for tests
return;
}
if (getPortWithOffset() == -1) {
try {
awaitThread = Thread.currentThread();
while (!stopAwait) {
try {
Thread.sleep(10000);
} catch (InterruptedException ex) {
// continue and check the flag
}
}
} finally {
awaitThread = null;
}
}
return;
// ...
}
3、线程让步方法
(1)、sleep方法实现让步
// 让出当前的CPU使用权,允许其他线程运行。这并不会让线程休眠,而是立即让操作系统重新进行一次CPU时间片的分配,即CPU调度
Thread.sleep(0)
(2)、yield方法实现让步
Thread.yield() 方法作用是:暂停当前正在执行的线程对象(及放弃当前拥有的CPU资源),并执行其他线程。
yield() 做的是让当前运行线程回到可运行状态,以允许具有相同优先级的其他线程获得运行机会。因此,使用yield() 的目的是让相同优先级的线程之间能适当的轮转执行。但是,实际中无法保证yield() 达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。
yield() 方法并不能保证线程一定会让出CPU资源,它只是一个提示,告诉调度器当前线程愿意让出CPU资源。具体是否让出CPU资源,还是由线程调度器决定。
class Task1 implements Runnable {
@Override
public void run() {
for (int i = 0; i < 200; i++) {
System.out.println("A:"+i);
}
}
}
class Task2 implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
Thread.yield();
System.out.println("B:"+i);
}
}
}
Thread thread1 = new Thread(new Task2());
thread1.start();
Thread thread2 = new Thread(new Task1());
thread2.start();
// 这里Thread.yield() 会提示线程调度器当前线程(thread1)愿意让出对CPU的使用权,但最终是否能让出,取决于线程调度器及当前cpu的使用情况, cpu越空闲,线程调度器重新进行CPU调度的几率越小, 所以这里执行结果是随机的。
4、线程执行优先级设置方法
class Task1 implements Runnable {
@Override
public void run() {
for (int i = 0; i < 200; i++) {
System.out.println("线程2:"+i);
}
}
}
class Task2 implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("线程1:"+i);
}
}
}
Thread thread1 = new Thread(new Task2());
thread1.setPriority(Thread.MAX_PRIORITY);
thread1.start();
Thread thread2 = new Thread(new Task1());
thread2.setPriority(Thread.MIN_PRIORITY);
thread2.start();
// 这里使用setPriority可以设置线程的调度优先级,虽然线程thread1的优先级设置的最高,但并不能表示线程thread1一直会被执行在thread2的前面,和Thread.yield()一样,具体也取决于线程调度器及当前cpu的使用情况,所以这里执行结果也是随机的。
5、打断线程方法
public static boolean interrupted(): 判断当前线程是否被打断, 并清除打断标记
public boolean isInterrupted(): 判断当前线程是否被打断, 不清除打断标记
- interrupted()示例:
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
try {
// 会清除打断标记 --> 撤销打断
log.info(Thread.interrupted() + "");
// 单位毫秒 完成延迟任务,用的少,更多自己测试 模拟业务执行
// Thread.sleep(1000);
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("睡眠完毕");
});
t1.start();
// 线程中断
t1.interrupt();
}
// 输出结果:
19:53:22.783 [Thread-0] INFO SleepThread - true
睡眠完毕
- isInterrupted()示例:
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
try {
// 单位毫秒 完成延迟任务,用的少,更多自己测试 模拟业务执行
// Thread.sleep(1000);
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("睡眠完毕");
});
t1.start();
// 不清除打断标记 --> 不撤销打断
log.info(t1.isInterrupted()+"");
// 线程中断
t1.interrupt();
// 不清除打断标记 --> 不撤销打断
log.info(t1.isInterrupted()+"");
}
// 输出结果:
false
true
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at java.lang.Thread.sleep(Thread.java:342)
at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
at cn.iocoder.springboot.labs.lab10.springdatarediswithjedis.Test01.lambda$main$0(Test01.java:55)
at java.lang.Thread.run(Thread.java:750)
睡眠完毕
6、线程中断
public void interrupt(): 仅仅是设置线程的中断状态为true,不会停止线程
注意: 当中断睡眠抛出InterruptedException异常时会清除中断标记,设置为false
// 优雅中断线程
public static void main(String[] args) {
Thread thread = new Thread(() -> {
while (true) {
// 每隔1s将时间片清除
try {
Thread.sleep(millis: 1000);
} catch (InterruptedException e) {
// 注意:当出现InterruptedException会清除中断标记 false
e.printStackTrace();
// 这里再次加上中断标记
Thread.currentThread().interrupt(); // true
}
// 如果中断的标记为true
// 获取线程中断标记,并且会清除标记
System.out.println(Thread.currentThread().isInterrupted());
if (Thread.interrupted()) {
System.out.println(Thread.currentThread().isInterrupted());
break;
}
// 长任务 - 定时监控
System.out.println("定时监控");
}
});
thread.start();
// 只是通知线程需要中断,线程不会立马中断,只是给线程做个标记,给线程打了中断标记=true
thread.interrupt();
}
// 执行结果如下:
java.lang.InterruptedException Create breakpoint: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at com.tl.juc.InterruptThread.lambda$main$0(InterruptThread.java:19) <1 internal line>
true
false
7、线程合并join
public final void join(): 等待这个线程结束
public final void join(long millis): 等待这个线程millis毫秒,0意味着永远等待
public class JoinThread {
static int value = 1;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
// todo... 业务
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
value = 10;
System.out.println("线程Runnable");
});
t1.start(); // 异步
t1.join(); // 主线程等待 t1线程执行结束,以下执行结果为:10
// t1.join(1000); // 主线程等待t1线程执行1s之后继续执行,以下执行结果为:1
System.out.println("主线程:" + value);
}
}
8、线程存活状态
public final native boolean isAlive(): 线程是否存活(还没运行完毕)
public class JoinThread {
static int value = 1;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
// todo... 业务
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t1.start(); // 异步
System.out.println(t1.isAlive());
t1.join();
System.out.println(t1.isAlive());
}
}
// 执行结果:
true
false
9、守护线程
public final void setDaemon(boolean on): 将此线程标记为守护线程或用户线程(又叫普通线程)
- 默认情况下我们创建的线程都是用户线程(普通线程),进程需要等待所有的线程执行完毕后,进程才会结束。
- 守护线程.setDaemon(true): 设置守护线程
- 想要查看线程到底是用户线程还是守护线程,可以通过
Thread.isDaemon()
方法来判断,如果返回的结果是true
则为守护线程,反之则为用户线程。 - 当所有的用户线程退出后,守护线程会立马结束。
public static void main(String[] args) {
// 创建线程(默认前台线程)
Thread d1 = new Thread(() -> {
for (int i = 0; i < 100; i++) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
// 设置线程为守护线程
d1.setDaemon(true); // 主线程结束 d1线程会立即结束
d1.start();
System.out.println("主线程结束");
}
- 应用场景
- 垃圾回收器线程属于守护线程:当进程中断时,垃圾回收立马停止
- tomcat用来接受处理外部的请求的线程就是守护线程:假如tomcat 关闭了,所有tomcat接收请求全部停止。
10、线程的状态与转换
public state getState(): 获取线程状态,java中线程状态共6个,如下图所示:
- NEW: 初始状态,线程被创建,但还没调用start()方法
- RUNNABLE: 运行状态,java线程将操作系统中的就绪状态和运行状态统称为“运行中”
- BLOCKED: 阻塞状态
- WAITING: 等待状态
- TIME_WAITING: 超时等待状态
- TERMINATED: 终止状态,表示当前线程已执行完毕
- 线程的状态是按照箭头方向来走的,比如线程从 New 状态是不可以直接进入 Blocked 状态的,它需要先经历 Runnable 状态。
- 线程生命周期不可逆:一旦进入 Runnable 状态就不能回到 New 状态;一旦被终止就不可能再有任何状态的变化。
- 所以一个线程只能有一次 New 和 Terminated 状态,只有处于中间状态才可转换。也就是这两个状态不会参与相互转化。
public static void main(String[] args) {
// 创建线程(默认前台线程)
Thread d1 = new Thread(() -> {
for (int i = 0; i < 100; i++) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
System.out.println(d1.getState());
d1.start();
System.out.println(d1.getState());
}
// 输出结果
NEW
RUNNABLE
11、第三种创建线程的方法callable和futureTask
一般情况下,使用Runnable
接口、Thread
实现的线程我们都无法返回结果的。但是如果对一些场合需要线程返回的结果,就要使用Callable
、Future
这几个类。Callable
只能在ExecutorService
的线程池中跑,但有返回结果,也可以通过返回的Future
对象查询执行状态。
- Callable源码
package java.util.concurrent;
/**
* A task that returns a result and may throw an exception.
* Implementors define a single method with no arguments called
* {@code call}.
*
* <p>The {@code Callable} interface is similar to {@link
* java.lang.Runnable}, in that both are designed for classes whose
* instances are potentially executed by another thread. A
* {@code Runnable}, however, does not return a result and cannot
* throw a checked exception.
*
* <p>The {@link Executors} class contains utility methods to
* convert from other common forms to {@code Callable} classes.
*
* @see Executor
* @since 1.5
* @author Doug Lea
* @param <V> the result type of method {@code call}
*/
@FunctionalInterface
public interface Callable<V> {
/**
* Computes a result, or throws an exception if unable to do so.
*
* @return computed result
* @throws Exception if unable to compute a result
*/
V call() throws Exception;
}
// 可以看到 Callable接口是一个函数式接口, 可以使用Lambda表达式来简化创建Callable接口
// 1、不需要实现抛异常的场景
Callable<String> callable = () -> {
// 在这里执行一些计算,然后返回一个字符串结果
return "Hello, World!";
};
// 2、需要实现抛异常的场景
Callable<Void> callable = () -> {
if (某个条件存在) {
throw new Exception("Something went wrong");
}
// 执行其他逻辑
return null;
} throws Exception;
- 基础使用
public static void main(String[] args) {
/* class Task implements Callable<Integer> {
@Override
public Integer call() throws Exception {
return 5;
}
}
Task task = new Task(); // 第2步,创建Callable实现类实例*/
// 第3步,使用FutureTask类来包装Callable对象,可以创建匿名对象
// 也可以直接用lambda省略1、2步
FutureTask<Integer> future = new FutureTask<>(() -> {
System.out.println("2.子线程运行中...");
Thread.sleep(5000);
return 20;
});
// 第4步,使用Future' Task对象作为Thread对象的target创建、并启动新线程。
new Thread(future).start();
System.out.println("1.已启动...");
try {
// FutureTask的get()方法会自动阻塞,直到得到任务执行结果为止
Integer value = future.get(); // 第5步,调用FutureTask对象的方法来获取子线程执行结束后的返回值
System.out.println("3.返回值" + value);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
// 执行结果:
1.已启动...
2.子线程运行中...
3.返回值20
- 结合线程池使用
public static void main(String[] args) {
ExecutorService executor = Executors.newSingleThreadExecutor();
Callable<Integer> callable = () -> {
Thread.sleep(5000);
System.out.println("子线程执行...");
// 执行一些计算任务
return 42;
};
Future<Integer> future = executor.submit(callable);
try {
System.out.println("准备获取异步任务的结果...");
Integer result = future.get(); // 获取异步任务的结果,会阻塞等待子线程执行完
System.out.println("异步任务的结果: " + result);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
// 关闭线程池
executor.shutdown();
}