一、背景 -- 进程与线程🚀
🔥 多线程是提升程序性能非常重要的一种方式,也是Java编程中的一项重要技术。在程序设计中,多线程就是指一个应用程序中有多条并发执行的线索,每条线索都被称作一个线程,它们会交替执行,彼此可以通信。
🌈 进程(process)是计算机中程序的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。
注意:虽然进程在程序执行时产生,但进程并不是程序
对计算机用户而言,计算机似乎能够同时执行多个进程,如听音乐、玩游戏、语音聊天等,都能在同一台计算机上同时进行。但实际上,一个单核的CPU同一时刻只能处理一个进程,用户之所以认为同时会有多个进程在运行,是因为计算机系统采用了多道程序设计技术
多道程序设计技术(了解)
假如现在内存中只有3个进程——A、B、C,那么CPU时间片的分配情况大致如下
🐸 虽然在同一个时间片中,CPU只能处理一个进程,但CPU划分的时间片是非常微小的,且CPU运行速度极快(1秒可执行约10亿条指令)
🦌 进程对 CPU 的使用权是由操作系统内核分配的,操作系统内核必须知道内存中有多少个进程,并且知道此时正在使用CPU的进程,这就要求内核必须能够区分进程,并可获取进程的相关属性。
🔥 通过上面可以知道,每个运行的程序都是一个进程,在一个进程中还可以有多个执行单元同时运行,这些执行单元可以看作程序执行的线程(thread)。每一个进程中都至少存在一个线程。
单线程与多线程
所谓多线程是指一个进程在执行过程中可以产生多个线程,这些线程在运行时是相互独立的,它可以并发执行的。过程如下:
💢 注意:多条线程看起来是同时执行的;其实不然,它们和进程一样,也是由CPU轮流执行的,只不过CPU运行速度很快,因此给人同时执行的感觉。
3.1 线程的优点
3.2 进程与线程的区别
💢 Java 提供了 3 种多线程的创建方式:
在学习多线程之前,我们先来看一个代码
class MyThread{
public void run() {
while(true) {
System.out.println("MyThread类的 run() 方法在运行");
}
}
}
public class thread_create {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.run();
while(true) {
System.out.println("main()方法在运行");
}
}
}
⚜️如果我们想让上面代码中的 两个while 循环中的语句能够 并发执行,就需要去实现 多线程
修改后的代码如下:
class MyThread extends Thread{
public void run() {
while(true) {
System.out.println("MyThread类的 run() 方法在运行");
}
}
}
public class thread_create {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start(); // 开启线程
while(true) {
System.out.println("main()方法在运行");
}
}
}
运行如下:
为了使读者更好地理解单线程程序和多线程程序的执行过程,下面通过下图分析单线程和多线程的区别
从上图可以看出
🔥 上面通过继承Thread类实现了多线程,但是这种方式有一定的局限性。因为Java只支持单继承,一个类一旦继承了某个父类,就无法再继承 Thread类
🐸 为了克服这种弊端,Thread类提供了另一个构造方法 ---- Thread(Runnable target)
如下:
class MyThread implements Runnable{
public void run() {
while(true) {
System.out.println("MyThread类的 run() 方法在运行");
}
}
}
public class thread_create {
public static void main(String[] args) {
MyThread myThread = new MyThread(); // 创建实例对象
Thread thread = new Thread(myThread); // 创建线程对象
thread.start(); // 开启线程
while(true) {
System.out.println("main()方法在运行");
}
}
}
运行如下:
🔖 通过 Thread 类和 Runnable 接口实现多线程时,需要重写run()方法,但是由于run()方法没有返回值,无法从新线程中获取返回结果。为了解决这个问题,Java 提供了Callable接口来满足这种既能创建新线程又有返回值的需求。
通过实现 Callable接口的方式创建并启动线程的主要步骤如下:
// 定义实现 Callable 的实现类
class MyThread implements Callable<Object> {
// 重写 call 方法
public Object call() throws Exception {
int i = 0;
while (i++ < 3){
System.out.println(Thread.currentThread().getName() + "的方法在运行");
}
return i;
}
}
public class thread_create {
public static void main(String[] args) throws InterruptedException,ExecutionException{
MyThread myThread = new MyThread(); // 创建实例对象
// 使用 FutureTask 封装
FutureTask<Object> ft = new FutureTask<>(myThread);
// 使用 Thread(Runnable target, string name) 构造方法创建线程对象
Thread thread = new Thread(ft, "thread");
thread.start(); // 启动线程
// 通过FutureTask 对象的方法管理返回值
System.out.println(Thread.currentThread().getName() + "的返回结果:" + ft.get());
int i = 0;
while(i++ < 3) {
System.out.println("main()方法在运行");
}
}
}
运行结果如下:
🌙 Callable 接口方式实现的多线程是通过 FutureTask类来封装和管理返回结果的,FutureTask类的直接父接口是 RunnableFuture,从名称上可以看出 RunnableFuture 是 Runnable 和Future的结合体。
由上图 可以知道:FutureTask 的本质是 Runnable 接口 和 Future 接口的实现类。其中 Future 接口用于管理线程返回结果,它共有 5 个方法如下:
方法声明 | 功能描述 |
---|---|
boolean cancel (boolean mayInterruptIfRunning) | 用于取消任务。 参数 mayInterruptIfRunning 表示是否允许取消正在执行却没有执行完毕的任务。若参数设为 true,则表示可以取消正在执行的任务 |
boolean isCancelled () | 判断任务是否被成功取消 |
boolean isDone () | 判断任务是否已完成 |
V get() | 用于获取执行结果。注意:这个方法会发生堵塞,一直等到任务执行完毕才返回执行结果 |
V get (long timeout, TimeUnit unit) | 用于在指定时间内获取执行结果 |
🌈多线程的实现方式有3种,其中Runnable 接口和 Callable 接口实现多线程的方式基本相同,主要区别就是Callable接口中的方法有返回值而 Runnable 接口中的方法没有返回值。
通过继承 Thread 类和实现 Runnable 接口实现多线程方式会有一定的区别
下面通过一个应用场景来分析说明:
小知识:currentThread() 方法
方法 | 说明 |
---|---|
public static Thread currentThread(); | 返回当前线程对象的引用 |
(1)首先通过继承 Thread 类的方式创建多线程来演示售票情景,如下:
class TicketWindow extends Thread{
private int tickets = 3;
public void run(){
while (tickets > 0){
Thread t = Thread.currentThread(); // 获取当前线程
String t_name = t.getName(); // 获取当前线程名字
System.out.println(t_name + "正在发售第 " + tickets-- + " 张票");
}
}
}
public class thread_ticket {
public static void main(String[] args) {
new TicketWindow().start();
new TicketWindow().start();
}
}
运行结果如下:
从上图可以看出,每张票都被打印了 2 次。出现这个现象的原因是 2 个线程没有共享 3 张票,而是各自出售了 3 张票
需要注意的是:上面的每个线程都有自己的名字,主线程默认的名字是main,用户创建的第一个线程的名字默认为Thread-0,第二个线程的名字默认为Thread-1,以此类推。 由于现实中铁路系统的车票资源是共享的,因此上面的运行结果显然不合理
为了保证资源共享,在程序中只能创建一个售票对象,然后开启多个线程运行同一个售票对象的售票方法,简单来说,就是 2 个线程运行同一个售票程序
修改代码使用构造方法 Thread(Runnable target, String name) 在创建线程对象时指定线程名称
class TicketWindow implements Runnable{
private int tickets = 3;
public void run(){
while (tickets > 0){
Thread t = Thread.currentThread(); // 获取当前线程
String t_name = t.getName(); // 获取当前线程名字
System.out.println(t_name + "正在发售第 " + tickets-- + " 张票");
}
}
}
public class thread_ticket {
public static void main(String[] args) {
TicketWindow tw = new TicketWindow();
new Thread(tw, "窗口1").start();
new Thread(tw, "窗口2").start();
}
}
运行结果如下:
注意:Thread 类下 也可以实现资源共享,还记得我们之前学的 static 全局共享嘛,我们可以给 ticket 票加上这个属性,就可以使所有窗口共享资源了,如下:
🔥 在多线程中,主线程如果不等待子线程的返回结果,那么主线程与子线程没有先后顺序,有可能主线程先结束了,子线程还没结束。
💧 有人可能会认为,当 main() 方法中创建并启动的 2 个新线程的代码执行完毕后,主线程也就随之结束了。然而,通过程序的运行结果可以看出,虽然主线程结束了,但整个Java程序却没有随之结束,仍然在执行售票的代码。
对Java程序来说,只要还有一个前台线程在运行,这个进程就不会结束;如果一个进程中只有后台线程运行,这个进程就会结束。
🎐注意:这里提到的前台线程和后台线程是一种相对的概念
class DemoThread implements Runnable{
public void run() {
while(true){
System.out.println(Thread.currentThread().getName() + "---在运行");
}
}
}
public class thread_back {
public static void main(String[] args) {
// 判断是否为后台线程
System.out.println("main 线程是后台线程嘛? --> " + Thread.currentThread().isDaemon());
DemoThread dt = new DemoThread();
Thread thread = new Thread(dt, "后台线程");
System.out.println("thread 线程默认是后台线程嘛? --> " + thread.isDaemon());
// 将线程 thread 对象设置为 后台线程
thread.setDaemon(true);
thread.start();
for(int i = 1;i <= 3; i++) {
System.out.println(i);
}
}
}
上面演示了一个后台线程结束的过程,运行如下
此时结果竟然没有出现死循环的情况,分析如下:
注意:要使某个线程设置为后台线程,必须在该线程启动之前进行设置,也就是说 setDaemon() 方法必须在 start() 方法之前进行调用,否则后台进程设置无效
public static void main(String[] args) {
// 匿名内部类 继承 Thread,重写run
Thread t =new Thread() {
@Override
public void run() {
System.out.println("使用匿名类创建 Thread 子类对象");
}
};
t.start();
System.out.println("main () 主方法");
}
public static void main(String[] args) {
// 匿名内部类 实现 Runnable,重写run
Thread t = new Thread(new Runnable(){
@Override
public void run() {
System.out.println("使用匿名类创建 Runnable 子类对象");
}
});
t.start();
System.out.println("main () 主方法");
}
lambda 表达式可以简化多线程的创建和调用过程,在创建线程时候可以指定线程调用的方法,格式如下:
Thread t = new Thread(()->{
}
});
使用如下:
public static void main(String[] args) {
Thread t = new Thread(() ->{
System.out.println("lambda 表达式创建 Runnable 子类对象");
});
t.start();
System.out.println("main () 主方法");
}
可以观察多线程在一些场合下是可以提高程序的整体运行效率的。
public class thread_advantage {
private static void concurrency() throws InterruptedException {
long begin = System.nanoTime();
// 利用一个线程计算 a 的值
Thread thread = new Thread(new Runnable(){
@Override
public void run() {
int a = 0;
for (long i = 0; i < count; i++) {
a--;
}
}
});
thread.start();
// 主线程内计算 b 的值
int b = 0;
for (long i = 0; i < count; i++) {
b--;
}
// 等待 thread 线程运行结束
thread.join();
// 统计耗时
long end = System.nanoTime();
double ms = (end - begin) * 1.0 / 1000 / 1000;
System.out.printf("并发: %f 毫秒%n", ms);
}
private static void serial() {
// 全部在主线程内计算 a、b 的值
long begin = System.nanoTime();
int a = 0;
for (long i = 0; i < count; i++) {
a--;
}
int b = 0;
for (long i = 0; i < count; i++) {
b--;
}
long end = System.nanoTime();
double ms = (end - begin) * 1.0 / 1000 / 1000;
System.out.printf("串行: %f 毫秒%n", ms);
}
// 多线程并不一定就能提高速度,可以观察,count 不同,实际的运行效果也是不同的
private static final long count = 10_0000_0000;
public static void main(String[] args) throws InterruptedException {
// 使用并发方式
concurrency();
// 使用串行方式
serial();
}
}
// 运行结果如下:
并发: 381.155400 毫秒
串行: 674.797500 毫秒
以上我们就把线程的创建、后台线程、以及进程与线程的区别讲啦,希望大家可以通过这篇文章打开对多线程学习的大门,后面我会继续更新多线程的相关知识的呀
【*★,°*:.☆( ̄▽ ̄)/$:*.°★* 】那么本篇到此就结束啦,如果我的这篇博客可以给你提供有益的参考和启示,可以三连支持一下 !!