每日鸡汤 ——
一颗红豆为何想单挑这宇宙,都要怪你在我心中播了种 —— 五月天 约翰蓝侬
1. 相关概念
-
程序:完成特定任务而编写的一组指令的集合,是一个静态的概念,就是我们写的代码
-
进程:运行中的程序,是一个动态的过程,自身就有产生、存在和消亡的过程。例如当我们启动了QQ,就启动了一个进程。操作系统会给进程分配一段空间。
-
线程:线程是由进程创建的,是进程的一个实体,一个进程可以创建拥有多个线程。
-
单线程: 同一个时刻,只能运行一个线程
-
多线程:同一个时刻,可以同时运行多个线程
-
并发:同一个时刻,多个任务交替执行,造成的“貌似同时”的错觉。简单的说,单核CPU实现的多任务就是并发。
-
并行:同一个时刻,多个任务同时执行。多核CPU可以实现并行。
2. 线程的创建
- 继承 Thread 类,重写其 run 方法
- 实现 Runnable 接口,重写其 run 方法
- 实现 Callable 接口,重写其 Call 方法
Thread 类源码中的 run 方法
@Override
public void run() {
if (target != null) {
target.run();
}
}
2.1 方式一 : 继承 Thread 类,重写 run 方法
关于通过继承 Thread 类,重写 run 方法的方式创建线程的语法,请直接看如下示例,关键代码解析用注释给出
示例:
package threadUse;
/**
* @description: 通过继承 Thread 类创建线程
* @author: Liuwanqing
* @date: 2022-10-12 15:53
*/
public class Thread01 {
public static void main(String[] args) throws InterruptedException {
// 创建 Cat 对象,当作线程使用
Cat cat = new Cat();
cat.start();
// 当 main 线程启动子线程,主线程不会阻塞
for (int i=0; i<8; i++){
System.out.println("汪汪,我是小狗狗~" + i + " 线程名" + Thread.currentThread().getName() );
Thread.sleep(1000);
}
}
}
// 继承了 Thread 类的类就可以当作线程使用
// 我们会重写 run 方法,重写自己的业务代码
class Cat extends Thread{
@Override
public void run() {
int time = 0;
while (true) {
// 该线程逻辑为:每隔一秒,这个小猫咪叫一声
System.out.println("喵喵喵~,我是最可爱的小猫咪~" + time + " 线程名:" + Thread.currentThread().getName());
try { // 休眠
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
time++;
if(time == 8){ // 叫了8次,小猫咪就不叫了
break;
}
}
}
}
注意点:
main线程启动子线程时,主线程不会阻塞。同时主线程结束,不代表程序结束,要等其子线程也运行结束,才结束。
问题:
为什么不直接调用 cat 线程的 run 方法,而是要调用 start 方法
回答:
当直接调用 cat 线程的 run 方法的时候,是主线程去执行 run 方法,我们可以通过去输出执行 喵喵叫 线程的名字,可以发现是主线程(main)在执行 run 方法,而不是 cat 线程去执行 run 方法。
读源码 Thread.class —— 理解start() 方法
start() 方法调用 start0,start0 是一个本地方法,由 JVM 调用,由 C/C++ 实现
public synchronized void start() {
/**
* This method is not invoked for the main method thread or "system"
* group threads created/set up by the VM. Any new functionality added
* to this method in the future may have to also be added to the VM.
*
* A zero status value corresponds to state "NEW".
*/
if (threadStatus != 0)
throw new IllegalThreadStateException();
/* Notify the group that this thread is about to be started
* so that it can be added to the group's list of threads
* and the group's unstarted count can be decremented. */
group.add(this);
boolean started = false;
try {
start0(); // 调用 start0() 核心实现方法
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
/* do nothing. If start0 threw a Throwable then
it will be passed up the call stack */
}
}
}
private native void start0();
2.2 方式二: 实现 Runable 接口,重写 run 方法
通过 new 一个进程,将 实现 Runnable 接口的对象实例作为参数传入,来创建线程。
示例:
package threadUse;
/**
* @description: 通过实现 Runnable 接口创建线程
* @author: Liuwanqing
* @date: 2022-10-12 16:46
*/
public class Thread02 {
public static void main(String[] args) {
Dog dog = new Dog();
Thread thread = new Thread(dog);
thread.start();
}
}
class Dog implements Runnable{
@Override
public void run() {
int count = 0;
while (true) {
System.out.println("汪汪~我是可爱的小狗狗~ " + "线程名:" + Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
count++;
if(count == 8){
break;
}
}
}
}
原理 —— 使用静态代理模式实现
模拟静态代理原理代码:
public class Thread02 {
public static void main(String[] args) {
Tiger tiger = new Tiger();
ThreadProxy threadTiger = new ThreadProxy(tiger);
threadTiger.start();
}
}
class Animal{};
class Tiger extends Animal implements Runnable{
public void run(){
System.out.println("嗷嗷~,我是小老虎~");
}
}
// 使用静态代理模式实现,模拟极简的 Thread
// 我自己没有 start 方法,请代理帮忙做这件事,但是最终结果还是我
class ThreadProxy implements Runnable{
private Runnable target = null;
@Override
public void run() {
if(target != null){
target.run(); // 动态绑定,运行类型为传入类型
}
}
public ThreadProxy(Runnable target) {
this.target = target;
}
public void start(){
start0();
}
public void start0(){
// 实现多线程核心原理
}
}
2.3 实现 Callable 接口,重写其 Call 方法
通过 new 一个进程,将 实现 Callable 接口的对象实例作为参数传入,来创建线程,这种方法不常用,了解即可。
使用 Callable 接口创建线程具有如下特点 :
- 可以定义返回值
- 可以提交异常
示例:
import java.util.concurrent.*;
/**
* @Author: WanqingLiu
* @Date: 2023/02/04/11:49
* 通过实现 Callable 接口创建线程
* Callable _ 可以定义返回值,可以提交异常
*/
public class TestCallable implements Callable<Boolean> {
// 重写 call 方法
@Override
public Boolean call() throws Exception {
System.out.println("线程" + Thread.currentThread().getName());
return true;
}
// 主线程
public static void main(String[] args) throws ExecutionException, InterruptedException {
TestCallable t1 = new TestCallable();
TestCallable t2 = new TestCallable();
TestCallable t3 = new TestCallable();
// 1. 创建线程池
ExecutorService service = Executors.newFixedThreadPool(3);
// 2. 提交执行
Future<Boolean> r1 = service.submit(t1);
Future<Boolean> r2 = service.submit(t2);
Future<Boolean> r3 = service.submit(t3);
// 3. 获取结果
System.out.println("r1" + (boolean)r1.get());
System.out.println("r2" + (boolean)r2.get());
System.out.println("r3" + (boolean)r3.get());
// 4. 关闭服务
service.shutdown();
}
}
3. 线程池
3.1 使用场景
并发情况下,需要频繁地创建和消耗线程时,为了提升性能,需要使用线程池
3.2 线程池思路
提前创建好多个线程放入线程池中,使用时直接获取,使用完放回池中
3.3 线程池优点
- 提升响应速度(减少了创建新线程的时间)
- 降低资源消耗(重复利用池中线程)
- 便于线程管理 (核心池大小、最大线程数、线程终止等待时间)
3.4 线程池 API —— ExecutorService & Executor
3.4.1 ExecutorService
真正线程池接口,常见子类如 ThreadPoolExecutor,其常用方法如下:
- execute(Runnable task)
- Future submit Future submit(Callable task)
- shutdown()
ThreadPoolExecutor 线程池参数
- 核心线程数量
- 最大线程数量
- 阻塞队列
- 核心线程之外的线程销毁等待时间数
- 时间单位
- 饱和策略
饱和策略
线程池中线程数量达到最大线程数量并且队列被放满任务时的处理策略
- 抛出异常并拒绝新任务
- 任务退回给调用者线程,调用者线程执行新任务(直接让主线程执行)
- 直接丢弃
- 丢弃最早的未处理的
阻塞队列
新任务被放到阻塞队列中(当前运行的线程达到了核心线程数)
线程池工作流程
- 是否达到核心线程数量
- 是否阻塞队列已满
- 是否达到最大线程数量
- 按照饱和策略处理
线程池命名空间
使用第三方包提供的线程工厂 (把这个线程工厂作为参数传入给线程池)
自己实现线程工厂
3.4.2 Executor
线程池工厂类,用于创建返回不同类型线程池
3.5 线程池案例
/**
* @Author: WanqingLiu
* @Date: 2023/02/04/11:41
* 测试线程池 Runnable
*/
public class TestPool {
public static void main(String[] args) {
// 1. 创建线程池
// 参数为线程池大小
// 执行
ExecutorService service = Executors.newFixedThreadPool(10);
service.execute(new MyThread());
service.execute(new MyThread());
service.execute(new MyThread());
service.execute(new MyThread());
// 2. 关闭连接
service.shutdown();
}
}
class MyThread implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}
/**
* @Author: WanqingLiu
* @Date: 2023/02/04/11:49
* 通过实现 Callable 接口创建线程
*/
public class TestCallable implements Callable<Boolean> {
// 重写 call 方法
@Override
public Boolean call() throws Exception {
System.out.println("线程" + Thread.currentThread().getName());
return true;
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
TestCallable t1 = new TestCallable();
TestCallable t2 = new TestCallable();
TestCallable t3 = new TestCallable();
// 1. 创建线程池
ExecutorService service = Executors.newFixedThreadPool(3);
// 2. 提交执行
Future<Boolean> r1 = service.submit(t1);
Future<Boolean> r2 = service.submit(t2);
Future<Boolean> r3 = service.submit(t3);
// 3. 获取结果
System.out.println("r1" + (boolean)r1.get());
System.out.println("r2" + (boolean)r2.get());
System.out.println("r3" + (boolean)r3.get());
// 4. 关闭服务
service.shutdown();
}
}
4. 扩展知识
4.1 查看自己电脑的 cpu数量 ——
(1)方法一 打开资源管理器查询
(2)方法二 使用Java 程序查询
public class Class1 {
public static void main(String[] args) {
Runtime runtime = Runtime.getRuntime();
int cpuNums = runtime.availableProcessors();
System.out.println("我的电脑的可用 cpu 数量是" + cpuNums);
}
}
结果和我们通过资源管理器查看的结果相同,都是12核
4.2 通过 jconsole 工具观察线程运行情况
5. 继承 Thread 类和 实现Runnable 接口创建线程的区别
- 从 Java 的设计上看,没有本质区别( 都是 start() -> start0() ),Thread 类本身就实现了 Runnable 接口
- 实现 Runnable 接口的方式更加适合多个线程共享一个资源的情况
创建一个 t3 可以供多个线程使用,thread1 和 thread2 共享 t3 的资源