#JAVA进阶技术之二:学习JAVA多线程的编程知识
小标题: 学习Java多线程,探索并发编程的神秘森林
在 Java 编程的广袤天地里,多线程宛如一位神奇的 “幕后英雄”,拥有着点石成金的魔力,能让程序的运行效率实现质的飞跃,宛如为程序注入了一股强大的加速剂。今天,就让我们一同揭开它神秘的面纱,深入探寻其中的奥秘。
一、线程概念:程序执行最小单元
当你轻点鼠标,启动一个 Java 程序,一场精彩绝伦的数字盛宴便在计算机的世界里悄然拉开帷幕。别看表面上风平浪静,实则幕后早已 “暗流涌动”。线程,作为这场盛宴中独立的执行路径,就像是穿梭于舞台各个角落的灵动舞者,即便你未曾刻意召唤,后台早已活跃着诸多线程 “小精灵”。主线程宛如一位掌控全局的指挥家,有条不紊地调度着程序的运行节奏;而 gc 线程则如同一位默默奉献的清洁工,悄无声息地清理着程序运行过程中产生的 “垃圾”,确保这片数字舞台始终整洁有序。它们各司其职,默契配合,推动着程序的巨轮稳步向前航行。
二、线程与进程区别:轻量与重量对比
不妨想象一下,你正沉浸在火爆全网的王者游戏之中。此时,进程就好比是那个功能完备、包罗万象的王者游戏客户端,它如同一位大权在握的 “资源主宰者”,牢牢掌控着内存分配、文件访问权限等大量关键 “资源”,是当之无愧的 “资源大管家”;而线程呢,则像是游戏里一个个具体而微的执行任务,英雄释放炫酷技能的瞬间是一个线程在发力,地图实时渲染展现精美画面的过程也是一个线程在忙碌,它们在进程搭建的宏大舞台上灵活自如地穿梭调度、风驰电掣般地快速执行,彼此紧密配合,共同为玩家勾勒出一场酣畅淋漓、沉浸感十足的游戏体验。简单来说,进程就像是负责精心切分资源这块 “巨型蛋糕” 的大厨,而线程则是专注于高效享用分配到的任务 “小甜点”,将其完美消化的美食家。
三、多线程实现方式:创建并启动线程
(一)继承 Thread 类
想要为自己的编程之旅招募一位得力的线程 “小助手” 吗?继承 Thread 类无疑是一条便捷的途径。首先,我们需要自定义一个类,大方地让它继承自 Thread 类,就像下面这个示例一样:
class MyThread extends Thread {
@Override
public void run() {
// 在这里精心编排线程要执行的具体任务
for (int i = 0; i < 10; i++) {
System.out.println("继承 Thread 类的线程:" + i);
}
}
}
紧接着,在主类的舞台上,创建这个自定义类的对象,然后果断调用 start 方法,千万要注意咯,可别误把 run 方法直接拿来调用。start 方法就如同吹响了冲锋的号角,它能让线程瞬间进入就绪状态,满心期待地等待 CPU 这位 “指挥官” 的调度。一旦时机成熟,获得宝贵的 CPU 时间片,线程便会火力全开,激情澎湃地执行 run 方法里精心编写的代码:
public class Main {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
}
}
(二)实现 Runnable 接口
倘若你感觉继承 Thread 类像是戴着镣铐跳舞,存在一定的局限性,那么实现 Runnable 接口将会为你开辟一条更为宽广自由的康庄大道。动手实现这个接口吧:
class MyRunnable implements Runnable {
@Override
public void run() {
// 仔细定义线程执行的任务逻辑
for (int i = 0; i < 10; i++) {
System.out.println("实现 Runnable 接口的线程:" + i);
}
}
}
当需要执行线程任务时,巧妙地将这个实现类传入 Thread 对象,宛如为线程穿上了定制的 “战甲”,再潇洒地调用 start 方法,便能让线程如脱缰的野马般欢快地奔跑起来。而且,这种方式巧妙地避开了单继承的束缚,如同挣脱了枷锁,为代码设计赋予了更多天马行空、自由发挥的空间:
public class Main {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start();
}
}
(三)使用 Executor 框架
当面对错综复杂、千头万绪的线程管理难题时,Executor 框架宛如一位智慧超群的指挥官,闪亮登场。首先,我们要精心打造一个线程池,就如同组建一支纪律严明、训练有素的精锐部队:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Main {
public static void main(String[] args) {
// 创建一个拥有 5 名“士兵”(线程)的线程池
ExecutorService executorService = Executors.newFixedThreadPool(5);
随后,把那些实现了 Runnable 接口的任务,当作一道道紧急的作战指令,逐一提交给线程池。线程池这位智慧的指挥官会根据战场形势,智能调度线程,确保任务高效执行:
for (int i = 0; i < 10; i++) {
executorService.submit(new Runnable() {
@Override
public void run() {
System.out.println("线程池中的线程:" + Thread.currentThread().getName() + " 正在执行任务");
}
});
}
// 待任务圆满完成,别忘了优雅地关闭线程池,就像打扫战后的战场,回收宝贵的资源:
executorService.shutdown();
}
}
这一整套行云流水般的流程下来,不仅让线程管理变得井井有条,如同将杂乱的线头梳理成顺滑的丝线,还能大幅提升程序的运行性能,使其如虎添翼。
(四)使用 Callable 和 Future
还有一种更为精妙、高级的玩法,那就是实现 Callable 接口。在重写 call 方法时,你可以像一位匠心独运的工匠,精心雕琢复杂而精妙的业务逻辑,而且这里允许抛出异常,处理问题更加得心应手:
import java.util.concurrent.Callable;
class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 0; i < 10; i++) {
sum += i;
}
return sum;
}
}
接着,借助 FutureTask 这个神奇的 “魔法盒子” 精心包装,再以此为基础创建 Thread 线程,仿佛为线程赋予了神奇的魔力:
import java.util.concurrent.FutureTask;
public class Main {
public static void main(String[] args) {
MyCallable myCallable = new MyCallable();
FutureTask<Integer> futureTask = new FutureTask<>(myCallable);
Thread thread = new Thread(futureTask);
thread.start();
// 重点来了,获取 Future 对象后,调用 get 方法,就如同打开了装满宝藏的宝箱,收获线程拼搏后的执行结果。不过要留意,get 方法有时会有点 “倔强”,它是阻塞的,要是线程还没跑完 “马拉松”,它就会痴痴地等待:
try {
Integer result = futureTask.get();
System.out.println("Callable 线程执行结果:" + result);
} catch (Exception e) {
e.printStackTrace();
}
}
}
四、线程常用方法介绍:操控线程的得力工具
(一)start () 方法
start () 方法堪称是线程启动的 “金钥匙”。当你轻轻转动这把钥匙,调用 start () 方法后,线程便如同被唤醒的睡狮,瞬间进入就绪状态,精神抖擞地等待 CPU 调度分配时间片。一旦幸运地获得 CPU 的青睐,就会如同离弦之箭,开始执行 run () 方法中的代码,开启属于它的精彩旅程。这就好比运动员们在起跑线后蓄势待发,只等那一声清脆的枪响(CPU 调度),便会全力冲刺(执行任务)。
(二)run () 方法
run () 方法无疑是线程的核心 “工作间”,承载着线程要执行的具体任务逻辑。不过要切记,直接调用 run () 方法并不会启动一个新线程,它只会像普通方法一样,在当前线程中按部就班地顺序执行其中的代码,无法发挥线程并行执行的强大优势。只有通过 start () 方法巧妙地间接调用 run () 方法,才能真正让线程在多线程的天空中自由翱翔,并行不悖地执行任务。
(三)sleep () 方法
在编程的漫漫征途中,有时候我们希望线程能稍作休息,暂停执行一段时间。这时,sleep () 方法就如同一张舒适的 “小床”,派上了用场。它接收一个以毫秒为单位的参数,这个参数就像是设定的 “睡眠时间”,表示线程要休眠的时长。例如:
public class SleepExample {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
try {
System.out.println("线程开始休眠");
Thread.sleep(3000);
System.out.println("线程休眠结束");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
thread.start();
}
}
在上述示例中,线程启动后会先礼貌地向世界宣告 “线程开始休眠”,然后乖乖地躺在 sleep () 方法提供的 “小床” 上,休眠 3 秒。待休息完毕,便会精神饱满地醒来,打印出 “线程休眠结束”。需要注意的是,sleep () 方法可能会像个调皮的孩子,抛出 InterruptedException 异常,所以一定要妥善处理,别让它捣乱。
(四)join () 方法
当我们需要编排线程之间的协作舞蹈,让一个线程等待另一个线程完成后再继续执行时,join () 方法就如同一条坚韧的 “纽带”,起到了关键作用。比如有两个线程 A 和 B,在主线程这个 “大舞台” 上,如果希望线程 A 优雅地完成自己的独舞后,线程 B 再闪亮登场开始表演,可以在线程 B 的启动代码前调用线程 A 的 join () 方法: public class JoinExample { public static void main(String[] args) throws InterruptedException { Thread threadA = new Thread(() -> { for (int i = 0; i < 5; i++) { System.out.println("线程 A:" + i); } }); Thread threadB = new Thread(() -> { try { threadA.join(); for (int i = 0; i < 5; i++) { System.out.println("线程 B:" + i); } } catch (InterruptedException e) { e.printStackTrace(); } }); threadA.start(); threadB.start(); } }
在这个例子中,线程 B 会像一位谦逊的绅士,先耐心地等待线程 A 完美谢幕,然后才自信满满地开始执行自己的任务。同样,join () 方法也可能会抛出 InterruptedException 异常,要小心应对。
(五)yield () 方法
yield () 方法则像是一位彬彬有礼的 “绅士”,它提示 CPU 让出当前线程的执行权,给其他具有相同优先级的线程一个崭露头角的机会,让它们有机会获得 CPU 时间片。不过,这仅仅是一个充满善意的建议,CPU 这位 “决策者” 并不一定会采纳。例如:
public class YieldExample {
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 10; i++) {
System.out.println("线程 1:" + i);
if (i == 5) {
Thread.yield();
}
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 10; i++) {
System.out.println("线程 2:" + i);
}
});
thread1.start();
thread2.start();
}
}
在这个示例中,当线程 1 执行到 i == 5 时,它会礼貌地调用 yield () 方法,向 CPU 发出请求。此时,CPU 可能会暂停线程 1 的执行,转而将目光投向线程 2,给它一个表现的机会,但也有可能继续执着地让线程 1 继续表演。
五、线程状态转换:线程生命周期的变迁
在 Java 这片神奇的编程海洋里,线程有着丰富多彩的状态,并且会像一位变幻莫测的魔术师,在不同条件下发生奇妙的状态转换。
(一)新建(New)
当我们通过 new 关键字创造一个线程对象时,就如同孕育一个新生命,线程便处于新建状态。此时,线程对象已经在内存的 “摇篮” 中分配了空间,但还如同一个懵懂的婴儿,尚未开始执行任何任务,静静地等待着成长的契机。例如前面创建的 MyThread、MyRunnable 等自定义线程类的对象,在调用 start () 方法之前,都处于这一纯真的新建状态。
(二)就绪(Runnable)
线程一旦调用 start () 方法,就仿佛听到了成长的号角,瞬间进入就绪状态。此时的线程,已经做好了充分的准备,摩拳擦掌,如同即将踏上赛场的运动员,正在等待 CPU 调度分配宝贵的时间片。就绪状态的线程活力满满,随时有可能被 CPU 这位 “伯乐” 选中,开启属于自己的奔跑之旅,执行 run () 方法中的代码。
(三)运行(Running)
当线程幸运地获得 CPU 时间片,就像运动员在赛场上全力冲刺一样,开始执行 run () 方法中的代码,此时线程便处于令人兴奋的运行状态。在这个状态下,线程如同舞台上的主角,尽情展现自己的风采,占用 CPU 资源,全情投入地执行自己的任务。不过,由于 CPU 时间片通常如同短暂的聚光灯,是有限的,线程在运行一段时间后,可能会因为时间片耗尽,光芒暂歇,或者主动让出 CPU(如调用 yield () 方法),而不得不暂停执行,重新回到就绪状态,等待下一次机会。
(四)阻塞(Blocked)
线程在执行过程中,难免会遇到一些 “绊脚石”,从而进入阻塞状态。比如,当它等待慢吞吞的 I/O 操作完成(就像等待蜗牛般的文件读取、网络通信等)、眼巴巴地等待获取一把被众多线程争抢的锁(当多个线程竞争同一把锁时,未获取到锁的线程只能在一旁干着急,陷入阻塞)、或者主动调用 sleep () 方法小憩一会儿时。处于阻塞状态的线程,就像是被困在迷宫中的行者,暂时失去了前进的方向,无法继续执行,直到阻碍解除,才有可能重新寻机重返就绪、运行的赛道。
总结
通过对 Java 多线程知识的全方位深入探讨,我们仿佛打开了一扇通往编程魔法世界的大门,清晰地认识到它在编程领域中如同璀璨星辰般的关键地位与毁天灭地般的强大作用。
从线程的基础概念启航,我们了解到线程作为程序执行的独立 “航道”,即便程序初始宛如平静的湖面,未显露出多线程的波澜,后台诸如主线程、gc 线程等早已如同水下的暗流,默默运作,确保程序这艘巨轮平稳航行。这让我们深刻领悟到,多线程是程序内在活力的源泉,赋予程序并行处理任务的超能力,使其能在复杂的编程海洋中乘风破浪。
对比线程与进程,借助王者游戏这一鲜活生动的形象比喻,我们仿佛亲眼目睹了进程掌控资源分配大权的威严模样,如同游戏客户端管理各类关键资源的霸气;而线程聚焦任务执行的专注神情,似游戏中多样的实时任务,二者紧密协作,构建起高效运行的程序 “摩天大厦”。这为我们后续在编程战场上合理运用线程和进程提供了清晰明确的指引,让我们能依据任务特性精准抉择,是以进程为核心进行资源调配,还是以线程为利刃实现精细的任务拆解与并行,从而打造出坚不可摧的程序堡垒。
在多线程实现方式的探索之旅中,继承 Thread 类给予我们最直接的创建线程途径,自定义类继承 Thread 后重写 run 方法,再借 start 方法激活线程,简单直接,如同拿起一把利剑,直击问题核心;实现 Runnable 接口则打破单继承局限,为代码复用与拓展打开新空间,只需将实现类传入 Thread 对象并启动,便可让线程如奔腾的骏马自由驰骋,摆脱束缚,释放无限潜能;Executor 框架宛如一位智慧的指挥官,创建线程池统一管理线程资源,精准调度任务,大幅提升运行效率,任务结束后优雅关闭线程池,完成资源回收,如同指挥一场宏大的战役,有条不紊,决胜千里;使用 Callable 和 Future 带来更高级玩法,不仅能在 call 方法中精心雕琢复杂业务逻辑,还可借助 Future 获取线程执行结果,尽管 get 方法偶尔会阻塞等待,但为需要精准结果反馈的场景提供有力支撑,如同在黑暗中点亮一盏明灯,照亮前行的道路。 线程常用方法犹如操控线程的魔法指令。start 方法是启动线程的号角,吹响后线程进入就绪队列,静候 CPU 青睐;run 方法承载核心任务逻辑,需借 start 间接调用,方能发挥线程并行优势;sleep 方法让线程小憩片刻,以毫秒为单位暂停执行,期间线程进入阻塞状态,待苏醒后重新