Java 线程创建方式全过程
作者:大大杰哥
1、继承Thread类
特点:
1、编程简单直观
直接在类中编写线程逻辑
适合简单的线程场景
2、受单继承限制,不利于资源共享,耦合度高,不适合线程池
类代码:
public class ThreadExtend extends Thread {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("线程执行:" + Thread.currentThread().getName());
}
}
}
主程序代码:
public class main {
public static void main(String[] args) {
ThreadExtend thread1=new ThreadExtend();
thread1.start();
ThreadExtend thread2=new ThreadExtend();
thread2.start();
}
}
//结果
线程执行:Thread-0
线程执行:Thread-0
线程执行:Thread-0
线程执行:Thread-0
线程执行:Thread-0
线程执行:Thread-1
线程执行:Thread-1
线程执行:Thread-1
线程执行:Thread-1
线程执行:Thread-1
线程执行:Thread-1
线程执行:Thread-1
线程执行:Thread-1
线程执行:Thread-1
线程执行:Thread-1
线程执行:Thread-0
线程执行:Thread-0
线程执行:Thread-0
线程执行:Thread-0
线程执行:Thread-02、实现Runnable接口
特点:
1、避免单继承局限
Java 是单继承语言,实现 Runnable 后还可以继承其他类
更灵活,符合面向对象的设计原则
2、适合资源共享
多个线程可以共享同一个 Runnable 实例
适合多个线程处理同一资源的场景
3、便于线程池管理
线程池主要接受 Runnable/Callable 任务
更容易与现代并发工具集成
4、代码解耦
将任务逻辑与线程控制分离
更符合"组合优于继承"的设计原则
类代码:
//演示共享资源
public class ThreadRunnable implements Runnable{
private int nums=20;
@Override
public void run() {
while(nums>0) {
System.out.println("线程执行:" + Thread.currentThread().getName());
nums--;
}
}
}主程序代码:
public class main {
public static void main(String[] args) {
ThreadRunnable threadRunnable = new ThreadRunnable();
Thread t1=new Thread(threadRunnable,"线程1");
Thread t2=new Thread(threadRunnable,"线程2");
Thread t3=new Thread(threadRunnable,"线程3");
t1.start();
t2.start();
t3.start();
}
}
//示例结果出现了线程安全问题,输出大于20个,可以在while循环内加锁
线程执行:线程3
线程执行:线程3
线程执行:线程3
线程执行:线程3
线程执行:线程3
线程执行:线程3
线程执行:线程3
线程执行:线程3
线程执行:线程3
线程执行:线程3
线程执行:线程3
线程执行:线程3
线程执行:线程3
线程执行:线程3
线程执行:线程3
线程执行:线程3
线程执行:线程3
线程执行:线程2
线程执行:线程2
线程执行:线程2
线程执行:线程1
线程执行:线程33、实现Callable接口
特点:
1、
需要重写call()方法,有返回值,可以通过FutureTask获取状态。
FutureTask常用方法
(1)get()-阻塞获取结果
String result = futureTask.get(); // 一直等待直到任务完成
(2)get(long timeout, TimeUnit unit) - 超时获取
// 最多等待5秒,超时抛 TimeoutException String result = futureTask.get(5, TimeUnit.SECONDS);
(3)isDone() - 是否完成
boolean done = futureTask.isDone(); // true: 任务已完成(正常完成、异常或取消) // false: 任务还在执行中
(4)isCancelled() - 是否被取消
boolean cancelled = futureTask.isCancelled(); // true: 任务在正常完成前被取消 // false: 任务未被取消
(5)cancel(boolean mayInterruptIfRunning) - 取消任务
boolean cancelled = futureTask.cancel(false); // false: 不中断正在运行的任务,只阻止未开始的任务 // true: 尝试中断正在运行的任务 // 返回值: // true: 取消成功 // false: 取消失败(任务已完成或已被取消)
cancel(false)在线程还在线程池排队没运行的时候能停下来
cancel(true)需要在run()代码中判断Thread.currentThread().isInterrupted(),检测线程是否收到中断信号来手动中断。或者执行sleep()等响应中断的阻塞方法也会使得任务退出。
(6)run() - 执行任务
futureTask.run(); // 通常在 Thread 中调用 // new Thread(futureTask).start(); 内部会调用这个方法
Thread.start()调用第二次会直接报错抛出异常。
FutureTask.run()调用第二次不会报错但是会被静默忽略不会真正执行。
(7)runAndReset() - 执行并重置
boolean reset = futureTask.runAndReset(); // 用于可重复执行的任务,执行后重置状态
这个方法可以重复执行,但是不能通过get得到结果。
2、
可以抛出异常。
类代码:
import java.util.concurrent.Callable;
public class ThreadCallable implements Callable<String> {
@Override
public String call() throws Exception {
for (int i = 0; i < 10; i++) {
System.out.println("线程执行:" + Thread.currentThread().getName());
}
return Thread.currentThread().getName()+"线程执行完毕";
}
}主程序代码:
public static void main(String[] args) {
FutureTask<String> futureTask1 = new FutureTask<>(new ThreadCallable());
Thread thread1 = new Thread(futureTask1);
thread1.start();
FutureTask<String> futureTask2 = new FutureTask<>(new ThreadCallable());
Thread thread2 = new Thread(futureTask2);
thread2.start();
try{
System.out.println(futureTask1.get());
}
catch(Exception e){
e.printStackTrace();
}
try{
System.out.println(futureTask2.get());
}
catch(Exception e){
e.printStackTrace();
}
}
//结果
线程执行:Thread-1
线程执行:Thread-1
线程执行:Thread-1
线程执行:Thread-1
线程执行:Thread-1
线程执行:Thread-1
线程执行:Thread-1
线程执行:Thread-1
线程执行:Thread-1
线程执行:Thread-1
线程执行:Thread-0
线程执行:Thread-0
线程执行:Thread-0
线程执行:Thread-0
线程执行:Thread-0
线程执行:Thread-0
线程执行:Thread-0
线程执行:Thread-0
线程执行:Thread-0
线程执行:Thread-0
Thread-0线程执行完毕
Thread-1线程执行完毕4、线程池
参数:
public ThreadPoolExecutor(
int corePoolSize, // 1. 核心线程数
int maximumPoolSize, // 2. 最大线程数
long keepAliveTime, // 3. 空闲线程存活时间
TimeUnit unit, // 4. 时间单位
BlockingQueue<Runnable> workQueue, // 5. 任务阻塞队列
ThreadFactory threadFactory, // 6. 线程工厂
RejectedExecutionHandler handler // 7. 拒绝策略
)corePoolSize(核心线程数):线程池中平时常驻的线程数量,即使空闲也不会被销毁。
maximumPoolSize(最大线程数):线程池允许创建的最大线程数(核心线程 + 临时线程)。
keepAliveTime(空闲存活时间):当线程数超过核心线程数时,多余的空闲线程在销毁前等待新任务的最长时间。
unit(时间单位),为keepAliveTime的时间单位
workQueue(任务队列):当核心线程都在忙碌时,新提交的任务会被放入这个队列中等待。 threadFactory(线程工厂):用于创建新线程,通常用来给线程设置有业务意义的名称,方便排查问题。
handler(拒绝策略):当队列满了,且线程数达到了最大值时,对新任务的兜底处理策略(如直接抛出异常、让提交者自己执行等)。
常用阻塞队列:
ArrayBlockingQueue:
基于数组实现的有界阻塞队列,初始化时必须指定容量。它采用 FIFO(先进先出)原则,且内部使用一把全局锁来保证线程安全。
LinkedBlockingQueue
基于链表实现的阻塞队列。它既可以是有界的,也可以是无界的(如果初始化时不指定容量,默认容量为 Integer.MAX_VALUE)。它采用了读写分离的双锁机制,在高并发下吞吐量ArrayBlockingQueue 更高。注意无界时候容易栈溢出。
SynchronousQueue(零容量、手递手)
核心特点:一个不存储任何元素的特殊队列(容量为 0)。它的每次放入操作必须等待一个对应的取出操作
生产建议:通常会配合较大的 maximumPoolSize,因为没有排队缓冲,新任务来了必须立刻创建线程接手。
PriorityBlockingQueue
一个支持优先级的无界阻塞队列。任务不再是先进先出,而是根据自定义的比较器或自然顺序来决定执行顺序。
延迟队列:
使用 JDK 自带的 ScheduledThreadPoolExecutor,内部配置了DelayedWorkQueue,这个队列和ThreadPoolExecutor不适配。
常用拒绝策略:
AbortPolicy(默认策略)
直接抛出一个 RejectedExecutionException 运行时异常,阻止任务提交。
CallerRunsPolicy(调用者运行策略)
不抛出异常,也不丢弃任务,而是将当前被拒绝的任务退回到提交该任务的线程(即调用者线程)中去执行。
适合允许延迟,任务不可丢失的的后台任务
原理:因为调用者线程需要亲自去执行被拒绝的任务,这会占用调用者的时间,从而自然地降低新任务提交的速度,让系统在高负载下实现平滑降级。
DiscardPolicy(丢弃策略)
静默地直接丢弃当前被拒绝的任务,不抛出任何异常,也不做任何通知。
DiscardOldestPolicy(丢弃最旧任务策略)
丢弃任务队列中等待时间最长(即队首)的那个任务,然后尝试重新提交当前这个最新的任务。
特点:
1、降低资源消耗,提高响应速度(线程复用)
传统方式:线程的创建和销毁是非常消耗系统资源的操作,需要频繁进行系统调用和上下文切换。
线程池:通过预先创建一批线程并长期存活,任务执行完毕后线程不会被销毁,而是归还到池中等待下一个任务。这种线程复用机制,极大地降低了频繁创建和销毁线程带来的 CPU 和内存开销。
2、增强系统的稳定性与可控性(避免资源耗尽)
传统方式:如果无限制地手动创建线程,会迅速耗尽系统的内存和 CPU 资源,极易导致系统卡顿甚至发生内存溢出崩溃。
线程池:可以对线程的最大并发数进行严格控制。当任务过多时,多余的任务会被放入队列等待,或者根据预设的拒绝策略进行处理,从而有效防止资源过载,保障系统的稳定性。
3、统一管理与功能丰富
传统方式:手动创建的线程分散且独立,开发者需要自己处理线程的生命周期、同步、优先级等,管理难度大且容易出错。
线程池:提供了统一的线程调度、生命周期管理、状态监控以及定时/周期性任务执行等高级功能,大大简化了并发编程的复杂度。
类代码:
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class ThreadPoolUtil {
public static ThreadPoolExecutor getThreadPoolExecutor()
{
return new ThreadPoolExecutor(
5,
10,
60L, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(100),
new ThreadPoolExecutor.DiscardOldestPolicy()
);
}
}
主程序代码:
import java.util.concurrent.FutureTask;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class main {
public static void main(String[] args) {
ThreadPoolExecutor executor = ThreadPoolUtil.getThreadPoolExecutor();
try{
for(int i=1;i<=20;i++)
{
int taskId=i;
executor.execute(()->{
System.out.println("任务"+taskId+"正在被"+Thread.currentThread().getName()+"执行");
try{
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
}
catch (Exception e)
{
e.printStackTrace();
}
finally{
executor.shutdown();//不再接受新的任务,线程池会等待所有任务执行完毕,再关闭
try {
if(!executor.awaitTermination(60, TimeUnit.SECONDS)){//阻塞等待60秒线程池所有任务还没有完成返回false
executor.shutdownNow();
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
//运行结果
任务3正在被pool-1-thread-4执行
任务4正在被pool-1-thread-5执行
任务2正在被pool-1-thread-3执行
任务1正在被pool-1-thread-2执行
任务0正在被pool-1-thread-1执行
任务5正在被pool-1-thread-4执行
任务6正在被pool-1-thread-5执行
任务7正在被pool-1-thread-2执行
任务9正在被pool-1-thread-1执行
任务8正在被pool-1-thread-3执行
任务10正在被pool-1-thread-1执行
任务11正在被pool-1-thread-3执行
任务12正在被pool-1-thread-2执行
任务13正在被pool-1-thread-5执行
任务14正在被pool-1-thread-4执行
任务15正在被pool-1-thread-1执行
任务16正在被pool-1-thread-3执行
任务17正在被pool-1-thread-4执行
任务18正在被pool-1-thread-5执行
任务19正在被pool-1-thread-2执行
示例补充:
示例代码中核心线程数为5个,因为20个任务并没有超过核心线程数+队列数,所以不会启动扩容创建新线程,因为线程运行中睡眠一秒,能看到输出情况是每一秒输出五个。
5、虚拟线程
特点:
1、资源消耗:极致的轻量
传统线程:每个线程都有一个固定的、较大的栈内存(默认 1MB 左右),且由操作系统管理。如果你尝试创建几十万个传统线程,系统内存会瞬间耗尽。
虚拟线程:
它的栈空间不是连续分配的,而是按需动态伸缩的(初始只有几 KB)。你可以轻松在一台普通电脑上创建数百万个虚拟线程,而不会导致内存溢出。
2、虚拟现场被设置为强制后台线程,哪怕继承了主线程,JVM也不会等待。
3、应用场景:
适合:高吞吐量的 Web 服务、微服务调用、数据库访问。
不适合:CPU 密集型任务(如复杂的数学计算、图像处理)。
原因:虚拟线程的优势在于 I/O 阻塞时的切换。如果是纯计算任务,它不会阻塞,会一直占用载体线程,此时它就退化成了普通线程,且因为调度层多了一层,反而可能略慢。对于 CPU 密集型任务,还是应该使用传统的线程池。
创建虚拟线程:
Thread.Builder builder = Thread.ofVirtual().name("worker-", 0);
for (int i = 0; i < 3; i++) {
int taskId = i;
builder.start(() -> {
System.out.println("[线程3] 任务 " + taskId + " 在 " + Thread.currentThread().getName() + " 中执行");
});
}
// 主线程等待1秒,让虚拟线程有时间执行
Thread.sleep(1000);
//结果
[线程3] 任务 2 在 worker-2 中执行
[线程3] 任务 0 在 worker-0 中执行
[线程3] 任务 1 在 worker-1 中执行
//或者更简单的方法
//使用 Thread.startVirtualThread()
Thread vThread1 = Thread.startVirtualThread(() -> {
System.out.println("[线程1] 虚拟线程名称: " + Thread.currentThread().getName());
System.out.println("[线程1] 是否是虚拟线程: " + Thread.currentThread().isVirtual());
});
vThread1.join();创建虚拟线程池:
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
// 提交多个任务
for (int i = 1; i <= 5; i++) {
int taskId = i;
executor.submit(() -> {
System.out.println("[线程池] 任务 " + taskId + " 开始执行 - " + Thread.currentThread().getName());
// 模拟 I/O 操作
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println("[线程池] 任务 " + taskId + " 完成执行");
return "结果-" + taskId;
});
}
// 等待所有任务完成
executor.shutdown();
executor.awaitTermination(10, TimeUnit.SECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println("[线程池] 所有任务已完成\n");
//结果
[线程池] 任务 3 开始执行 -
[线程池] 任务 2 开始执行 -
[线程池] 任务 5 开始执行 -
[线程池] 任务 1 开始执行 -
[线程池] 任务 4 开始执行 -
[线程池] 任务 1 完成执行
[线程池] 任务 5 完成执行
[线程池] 任务 4 完成执行
[线程池] 任务 3 完成执行
[线程池] 任务 2 完成执行
[线程池] 所有任务已完成定时任务调度线程
(1)Timer,TimerTask
原理
使用 Timer 和 TimerTask 创建线程,并不是一种完全独立、全新的底层创建线程的方法。它本质上是基于 Java 最基础的实现 Runnable 接口这一方式,并在此基础上进行了更高层次的封装,专门用于实现定时或周期性任务。
有以下缺点:
单线程隐患:一个 Timer 只有一个工作线程。如果其中一个 TimerTask 执行时间过长,会延迟其他所有任务的执行;更严重的是,如果某个任务抛出了未捕获的异常,整个 Timer 的工作线程就会直接挂掉,导致后续所有任务都无法再执行。
依赖系统时间:Timer 是基于系统的绝对时间来调度的,如果操作系统的系统时间被修改,定时任务的执行就会变得不准确。
所以现在不常用。
(2)ScheduledThreadPoolExecutor
特点:
1. 继承自 ThreadPoolExecutor,这意味着它天然拥有了标准线程池的所有基础能力,比如线程的复用、并发控制、线程生命周期管理等。
2、核心任务队列:DelayedWorkQueue
底层结构:这是一个基于最小堆(Min-Heap)数据结构实现的优先级队列。
核心作用:它会把所有提交的任务,按照“距离执行时间”的长短进行排序。距离下次执行时间最近的任务,永远会排在队列的最前面(堆顶)。工作线程每次只需要盯着堆顶的任务,如果时间没到就继续等待,从而实现了精准的定时调度。
3、核心任务封装:ScheduledFutureTask
当你提交一个定时任务时,ScheduledThreadPoolExecutor 不会直接把你的任务扔进队列,而是会先把它包装成一个 ScheduledFutureTask 对象。
核心作用:这个包装类里不仅包含了你的业务逻辑,还额外记录了任务的触发时间和周期。
周期性原理:对于周期性任务,这个包装类在执行完一次任务后,会自动计算出下一次应该执行的时间,然后把自己重新塞回 DelayedWorkQueue 中排队。这就形成了一个“执行 -> 计算下次时间 -> 重新入队 -> 等待执行”的完美闭环。
示例代码:
// 创建定时任务线程池
ScheduledThreadPoolExecutor scheduler = new ScheduledThreadPoolExecutor(3);
System.out.println("程序启动时间:" + System.currentTimeMillis());
// 1. 延迟5秒执行一次
scheduler.schedule(() -> {
System.out.println("[延迟任务] 执行时间:" + System.currentTimeMillis());
}, 5, TimeUnit.SECONDS);
// 2. 固定频率执行:初始延迟2秒,每3秒执行一次
scheduler.scheduleAtFixedRate(() -> {
System.out.println("[固定频率] 执行时间:" + System.currentTimeMillis());
}, 2, 3, TimeUnit.SECONDS);
// 3. 固定延迟执行:初始延迟1秒,每次执行完延迟2秒
scheduler.scheduleWithFixedDelay(() -> {
System.out.println("[固定延迟] 执行时间:" + System.currentTimeMillis());
try {
Thread.sleep(500); // 模拟任务执行
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, 1, 2, TimeUnit.SECONDS);
// 主线程等待15秒后关闭线程池
try {
Thread.sleep(15000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
// 关闭线程池
scheduler.shutdown();示例补充:
scheduleWithFixedDelay:保证任务之间有固定的"休息时间",适合需要控制执行频率、避免系统过载的场景
scheduleAtFixedRate:保证任务按固定时间点执行,适合对时间精度要求高的场景
在使用scheduleAtFixedRate的时候如果执行时间大于设置周期,任务会一个接一个立即执行,没有等待时间,长时间下去会导致任务堆积,内存溢出,线程饥饿等问题。
--------------------------------------------------------------------------------------------------------------------
到这里就结束喽✿✿✿✿✿✿
到此这篇关于Java 线程创建方式全过程的文章就介绍到这了,更多相关Java 线程创建方式内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
