Java线程的学习—多线程(一)

1.概念

⼀个线程就是⼀个 "执行流". 每个线程之间都可以按照顺序执行自己的代码. 多个线程之间 "同时" 执行着多份代码.

还是回到我们之前的银行的例子中。之前我们主要描述的是个人业务,即一个人完全处理自己的业务。我们进一步设想如下场景: 一家公司要去银行办理业务,既要进行财务转账,又要进行福利发放,还得进行缴社保。 如果只有张三一个会计就会忙不过来,耗费的时间特别长。为了让业务更快的办理好,张三又找来两位同事李四、王五一起来帮助他,三个人分别负责一个事情,分别申请一个号码进行排队,自此就有了三个执行流共同完成任务,但本质上他们都是为了办理一家公司的业务。 此时,我们就把这种情况称为多线程,将一个大任务分解成不同小任务,交给不同执行流就分别排队执行。其中李四、王五都是张三叫来的,所以张三一般被称为主线程(MainThread)。

用生活例子理解:线程就像 工厂里一条独立的生产线。工厂要完成很多任务,单靠一条生产线(比如只生产杯子)效率低。有多条生产线(线程),就能同时生产杯子、盘子等不同物品,让工厂整体效率变高。放在电脑里,程序通过线程实现 "同时做几件事",比如一边下载文件(一个线程)、一边播放音乐(另一个线程)

(1)为什么要有线程

官方解释

首先,"并发编程" 成为 "刚需":

  • 单核 CPU 的发展遇到瓶颈,提升算力需依赖多核 CPU,而并发编程能更充分利用多核 CPU 资源。
  • 部分任务存在 "等待 IO" 的场景(如网络请求、文件读写),利用并发编程可在等待 IO 的时间里处理其他工作,避免资源闲置。

其次,虽多进程也能实现并发,但线程比进程更轻量:

  • 线程的创建速度比进程更快。
  • 线程的销毁速度比进程更快。
  • 操作系统对线程的调度速度比进程更快。

最后,即便线程已较轻便,仍需进一步优化,因此衍生出 "线程池"(ThreadPool)和 "协程"(Coroutine)等技术。

理解

生活中,线程可类比餐厅里的多个服务员:若只有 1 个服务员(单线程),一次只能为 1 位顾客完成点单、上菜、结账全流程,效率极低;但如果有多个服务员(多线程),就能同时为不同顾客分工服务 ------ 有的负责点单、有的负责上菜、有的负责结账,餐厅可同时处理更多顾客需求,整体服务效率大幅提升。

**(2)**进程和线程的区别

• 进程是包含线程的. 每个进程至少有⼀个线程存在,即主线程。

• 进程和进程之间不共享内存空间. 同⼀个进程的线程之间共享同⼀个内存空间

比如之前的多进程例子中,每个客户来银行办理各自的业务,但他们之间的票据肯定是不想让别人知道的,否则钱不就被其他人取走了么。而上面我们的公司业务中,张三、李四、王五虽然是不同的执行流,但因为办理的都是一家公司的业务,所以票据是共享着的。这个就是多线程和多进程的最大区别。

• 进程是系统分配资源的最雄安单位,线程是系统调度的最小单位。

• ⼀个进程挂了⼀般不会影响到其他进程. 但是⼀个线程挂了, 可能把同进程内的其他线程⼀起带走(整个进程崩溃).

理解

1. 轻量级进程(线程的本质)

线程是 "轻量级进程",同一进程的多个线程共享同一份系统资源(如内存、文件描述符表等)。

  • 优势:创建线程时省去申请资源的开销,销毁线程时省去释放资源的开销,效率远高于进程。
2. 独立执行流

每个线程都是独立的执行流,可独自参与 CPU 调度(即系统会单独给线程分配 CPU 时间片)。

3. 进程与线程的角色区分
  • 进程 :是系统分配资源的基本单位(负责申请内存、文件句柄等资源)。
  • 线程 :是系统调度执行的基本单位(负责具体任务的执行,由操作系统调度到 CPU 上运行)。
  • 关系:一个进程可包含多个线程,这些线程共享进程的资源,同时各自独立执行任务。

(3)进程和线程核心区别

  • 资源占用:比如电脑上同时运行的音乐播放器程序和浏览器程序,它们是两个不同的进程,有各自独立的资源。而线程共享所属进程的资源,相当于大房子里不同的人,共享房子里的空间和设施 。
  • 独立性:进程相互独立,不同进程之间不能直接共享数据,就像不同房子之间是隔开的。线程之间的隔离性没那么强,同一进程内的线程可以直接访问共享数据,比如一个程序里多个线程可以同时访问程序中的全局变量。
  • 上下文切换开销:进程切换时,因为各自独立,要保存和恢复大量信息,开销大,就像把一个大房子里的所有东西都收拾好放到一边,再把另一个大房子的东西拿出来布置,很麻烦。线程切换相对简单,因为共享进程的资源,开销小,类似大房子里不同人的位置变换,没那么复杂。
  • 控制方式:进程的创建和管理一般通过操作系统的指令或接口来操作;线程在程序里就可以通过代码灵活创建、销毁和管理,比如在 Java 中可以用代码轻松创建新线程。

(4)Java 的线程 和 操作系统线程 的关系

线程是操作系统中的概念.操作系统内核实现了线程这样的机制,并且对用户层提供了⼀些API供用用户使用(例如Linux的pthread库).

Java 标准库中Thread类可以视为是对操作系统提供的API进行了进⼀步的抽象和封装.

联系:

Java 线程需要依赖操作系统线程来实现底层的运行和调度。就像你用手机 APP(Java 线程),APP 的各种操作最终要靠手机系统(操作系统线程)来执行。

Java 虚拟机(JVM )在运行时会和操作系统交互,把 Java 线程映射到操作系统线程上 ,让 Java 线程能利用系统资源执行任务。
区别:

  • 调度方式:操作系统线程由操作系统内核调度,而 Java 线程是由 JVM 调度。可以理解为,操作系统线程是听从操作系统这个 "大管家" 的安排,Java 线程则是在 JVM 这个 "小管家" 的管理下运行 。
  • 上下文切换成本:Java 线程切换时,要先在 JVM 中保存和恢复线程状态等信息,开销比操作系统线程切换更高。比如切换两个操作系统线程像在两个相邻房间快速走动;而切换 Java 线程,就像在房间里还要先整理好东西再出去,更费时间。
  • 系统资源占用:操作系统线程有自己的线程栈、线程控制块等系统资源;Java 线程除了这些,还需要 Java 堆内存和栈内存,所以 Java 线程消耗的系统资源略多一些 。

2.第⼀个多线程程序

2.1感受多线程程序和普通程序的区别:

• 每个线程都是⼀个独立的执行流

• 多个线程之间是"并发"执行的

java 复制代码
package thread;

class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("hello thread");
    }
}

public class Demo1 {
    public static void main(String[] args) {
        Thread t = new MyThread();
        // 真正在系统中创建出一个线程
        t.start();
// t.run();
    }
}

2.2 start 和 run 的 区别是什么呢?

"异步" 核心是:两个线程的执行流程是 "并行" 的,调用者(主线程)不需要等待被调用者(新线程的 run())完成,各自推进自己的逻辑

"阻塞" 指的是一个线程(或进程)因等待某个资源或事件(如网络请求、文件读写、锁获取等)而暂时停止执行,直到该资源可用或事件发生,才会继续执行的状态。

2.3 来感受一下两个进程的效果

java 复制代码
package thread;

class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("hello thread");
        while (1==1 ){
            System.out.println("hello thread");
        }
    }
}

public class Demo1 {
    public static void main(String[] args) {
        Thread t = new MyThread();
        // 真正在系统中创建出一个线程
       t.start();
       while (1 == 1) {
           System.out.println("hello main");
       }
    }
}

主线程会继续执行自己后续的代码

新线程则独立执行 run() 方法中的逻辑

两者是并行运行的关系。

2.4 Thread.sleep 这个方法

java 复制代码
Thread.sleep(1000);
//1000 单位是毫秒

这是一个静态的方法

是 Java 中用于让当前线程暂停执行指定时间的方法,属于线程控制的核心 API 之一。它的作用是让线程进入 "休眠" 状态,暂时让出 CPU 使用权,直到指定时间结束后再重新进入就绪状态,等待调度。

InterruptedException 是 Java 中的一个受检异常(checked exception),通常在线程的阻塞状态被打断时抛出 。它的核心含义是:当前线程正在等待、休眠或阻塞时,被其他线程通过 interrupt() 方法强制 "唤醒",以响应中断请求。

Java 规定:子类重写父类方法时,声明的受检异常范围不能超过父类方法。具体来说:

  • 子类方法可以不抛出任何异常(即使父类方法有异常声明)。
  • 子类方法可以抛出父类方法声明的异常的子类(缩小异常范围)。
  • 子类方法不能抛出父类方法未声明的受检异常(扩大异常范围)

重写 Thread 类的 run() 方法不能throws ,只能try -catch,但是try - catch 还是向上抛出异常,只不过是换了个形式,实际开发中就啥也没干,就不科学

但是在main 方法中的是可以进行throws, 点击第一个就是将异常添加到方法签名中

在实际开发中,我们解决异常的方法:

  • 记录异常信息作为日志,在发现异常的时候根据日志发现问题
  • 重启能解决90%的问题
  • 通过短信/微信/电话/报警通知程序员,马上处理这个问题
java 复制代码
package thread;

class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("hello thread");
        while (1==1 ){
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("hello thread");
        }
    }
}

public class Demo1 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new MyThread();
        // 真正在系统中创建出一个线程
       t.start();
       while (1 == 1) {
           Thread.sleep(1000);
           System.out.println("hello main");

       }
    }
}

执行后,我们能发现,谁先执行,谁后执行都有可能,这个是无法预测的

调度顺序:是由操作系统的内核的调度器控制的,没有办法再程序应用编程控制的(没有提供API)

2.5不知道JDK 安装到哪里了?

点击进入到该界面

2.6 jconsole.exe

jconsole.exe 是 Java 开发工具包(JDK)提供的一款图形化监控工具 ,全称为 Java Console,主要用于监控和管理 Java 应用程序(包括本地和远程的 Java 进程)

在路经中找放到它,点击

只会列出Java写的进程,其他语言的进程就不会出现

找到自己的项目然后连接

这是当前进程的线程,启动任何Java进程都会自带这些线程

可以在里面的选择哪一个线程,直接观察即可

3.创建多线程的方法

3.1继承Thread ,重写 run 方法

3.2实现runnable ,重写 run 方法

  1. 实现Runnable接口
java 复制代码
 class MyRunnable implements Runnable {
 @Override
 public void run() {
 System.out.println("这⾥是线程运⾏的代码");
 }
 }

2.创建Thread类实例,调用Thread的构造方法时将Runnable对象作为target参数.

java 复制代码
Thread t = new Thread(new MyRunnable());

3.调用start方法

java 复制代码
package thread;

import com.sun.source.tree.Tree;

public class demo2 {
    public static void main(String[] args) throws InterruptedException {
        // 创建任务,并设置终止标志
        MyRunnable task = new MyRunnable();
        // 创建线程(传入任务)
        Thread t = new Thread(task);
        t.start();

while(true) {
    Thread.sleep(1000);
    System.out.println("hello ,main");
}
    }

}
class MyRunnable implements Runnable {
    public void run() {
        while (1 == 1) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("hello runnable ");

        }

    }
}

3.3ThreadRunnable 的分工设计思路

  • 线程创建的核心载体 :最终必须通过 Thread 类来真正创建操作系统级别的线程。
  • 任务逻辑的分离表示 :线程要执行的 "具体做什么"(任务逻辑),推荐通过 Runnable 接口来定义,而非直接重写 Thread 类的 run 方法。
  • 任务定义的位置选择 :明确了两种方式的区别 ------ 任务可以定义在 Thread 类内部(通过继承 Thread 并重写 run),也可以定义在 Thread 类外部的 Runnable 实现类中(更推荐,实现 "任务与线程控制的分离")。

通过 Runnable 实现 "任务与线程解耦合" 的设计优势,可提取如下信息:

  • 核心概念:解耦合让 "要执行的任务本身" 与 "线程这个概念" 分离,不再强绑定。

  • 带来的好处:便于代码维护与扩展 后续若需变更任务的执行方式(比如不通过线程执行,改用其他方式),采用 Runnable 方案时,代码修改会更简单。

相关推荐
人间打气筒(Ada)3 小时前
yum安装k8s集群----基于centos7.9
java·容器·kubernetes
忧郁奔向冷的天3 小时前
视觉SLAM十四讲2nd—学习笔记(二)20250817
笔记·学习
应用市场3 小时前
PHP microtime()函数精度问题深度解析与解决方案
android·开发语言·php
Filotimo_3 小时前
Spring MVC 数据校验
java·spring·mvc
长存祈月心3 小时前
Rust HashSet 与 BTreeSet深度剖析
开发语言·后端·rust
长存祈月心3 小时前
Rust BTreeMap 红黑树
开发语言·后端·rust
沐浴露z3 小时前
Kafka Consumer 详解API,分区分配策略以及消费offset
java·kafka
weixin_404551244 小时前
openrewrite Lossless Semantic Trees (LST)
java·tree·yaml·lossless·openrewrite·lst·semantic
一 乐4 小时前
口腔健康系统|口腔医疗|基于java和小程序的口腔健康系统小程序设计与实现(源码+数据库+文档)
java·数据库·vue.js·小程序·毕设