多线程奇幻漂流:从单核到多核质变(一)

目录

一、线程和进程的区别

二、线程的五大特性

[2.1. 调度相关](#2.1. 调度相关)

[2.2. 状态](#2.2. 状态)

[2.3. 优先级](#2.3. 优先级)

[2.4. 记账信息](#2.4. 记账信息)

[2.5. 上下文](#2.5. 上下文)

三、线程的诞生(如何创建一个线程)

方法一:继承Tread类:

方法二:实现Runnable接口-------能者多劳

方法三:匿名内部类

方法四:Runnable的匿名内部类

疑点1

方法五:Lambda方法

疑点2

重点标记带:

四、前台线程和后台线程

[4.1. 核心区别](#4.1. 核心区别)

[4.2. 关键细节](#4.2. 关键细节)

[4. 3 常见误区](#4. 3 常见误区)

4.4总结

五、线程使用外面变量的那些事

六、使用join()等待线程结束

[join() 方法的作用](#join() 方法的作用)

[为什么需要 join()?](#为什么需要 join()?)

核心要点

总结


这一章博客全是干货,彦祖们都需要掌握!!!!!!!!!!!

想象一下你是一个孤岛上的国王,你日理万机,王国里只有你一个人,一次只能做一件事情·。批阅奏章,做饭,睡觉。当你做饭的时候,奏章堆积如山,当你睡觉的时候,厨房着火了........你多希望自己有影分身术啊,就是我们要探讨的(多线程)!

一、线程和进程的区别

进程是一个正在运行的程序,创建和销毁需要浪费很多资源,创建销毁的开销太大,我们可以说进程里面有很多线程,线程之间可以协同工作,干的活比较多,创建和销毁的开销要小得多!所以引入了多线程。

进程是资源分配的基本单位,而线程是CPU调度的基本单位。你可以把进程想象成一个正在运行的完整程序,线程就是这个程序执行的任务。当我们引入了线程,我们的效率就会指数级上升,有很多人帮我们干活。

资源拥有:进程有自己独立的内存空间和系统资源,而线程资源是共享的。

开销成本:创建或销毁进程的开销很大,而创建线程就小得多。

cpu执行的是线程而不是进程。

二、线程的五大特性

线程的这几个特性是操作系统进行线程调度和管理的关键依据,主要包括以下几个方面:

2.1. 调度相关

  • 概念:线程的调度是操作系统决定哪个线程获得CPU时间片并执行的过程。

  • 特性:线程是CPU调度的基本单位,操作系统会根据一定的调度算法(如优先级调度、时间片轮转等)来决定线程的执行顺序和时间。

  • 影响:调度的好坏直接影响系统的性能和响应时间。例如,在实时系统中,调度算法需要确保高优先级的线程能够及时得到执行。

当我们创建了多个线程,他们的执行顺序是不确定,这是因为,线程的调度是随机的!

2.2. 状态

  • 概念:线程在其生命周期中会经历不同的状态,这些状态反映了线程当前的执行情况。

  • 常见状态:

  • 新建状态(New):线程被创建但尚未启动。

  • 就绪状态(Runnable):线程已经准备好执行,等待CPU分配时间片。

  • 运行状态(Running):线程正在CPU上执行。

  • 阻塞状态(Blocked):线程由于等待某个事件(如I/O操作完成、等待锁等)而暂时无法执行。

  • 终止状态(Terminated):线程执行完毕或因异常退出。

  • 状态转换:线程的状态会随着其执行过程和外部事件的发生而转换,例如从就绪状态转换为运行状态,或者从运行状态转换为阻塞状态。

2.3. 优先级

  • 概念:每个线程都有一个优先级,操作系统在调度时会考虑线程的优先级。

  • 特性:优先级高的线程通常会获得更多的CPU时间片和更高的执行机会。

  • 注意事项:不同的操作系统对线程优先级的支持和实现可能不同,而且过度依赖线程优先级可能会导致线程饥饿(低优先级线程长时间无法获得执行机会)等问题。

2.4. 记账信息

  • 概念:记账信息是操作系统为了跟踪线程的资源使用情况而记录的数据。

  • 内容:包括线程的执行时间、CPU使用时间、内存使用情况等。

  • 作用:这些信息可以帮助操作系统进行资源管理和调度决策,例如根据线程的CPU使用情况调整其优先级。

2.5. 上下文

  • 概念:线程的上下文是指线程执行时的环境,包括程序计数器、寄存器的值、栈指针等。

  • 特性:当线程被切换出CPU时,操作系统会保存其上下文;当线程再次被调度执行时,操作系统会恢复其上下文,使得线程能够从上次暂停的地方继续执行。

  • 重要性:上下文切换是线程调度的重要过程,其开销会影响系统的性能。因此,减少不必要的上下文切换是提高系统性能的一个重要手段。

总结

这些特性共同构成了线程的基本属性,操作系统通过对这些特性的管理和调度,实现了多线程的并发执行,提高了系统的资源利用率和性能。在多线程编程中,了解这些特性对于编写高效、稳定的程序非常重要。

三、线程的诞生(如何创建一个线程)

在Java中创建一个线程非常重要,我们之前写的代码,main方法就是一个线程。

方法一:继承Tread类:

Tread我们就可以理解成线程的标志,和他相关的就脱离不了多线程.

由于Thread这个类继承了runnable接口所以我们要重写里面run方法.run方法我们就可以理解成线程里要执行的内容.

复制代码
package thread;

class MyThread extends Thread{
    @Override
    public void run() {
        while (true){
        System.out.println("Hello Thread");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
}}
public class Demo1 {
    public static void main(String[] args)throws InterruptedException {
        Thread thread=new MyThread();
        thread.start();
          while (true){
              System.out.println("hello main");
              Thread.sleep(1000);
          }
    }
}

方法二:实现Runnable接口-------能者多劳

相比于方法一这个方法优势在于继承在于,继承类只有一个,但是继承接口可以有多个。这种方法相比于其更加灵活。

复制代码
package thread;
 class MyRunnable implements Runnable{
     @Override
     public void run() {
         while (true){
             System.out.println("hello thread");
             try {
                 Thread.sleep(1000);
             } catch (InterruptedException e) {
                 throw new RuntimeException(e);
             }
         }
     }
 }
public class Demo2 {
    public static void main(String[] args)throws InterruptedException {
        Runnable runnable=new MyRunnable();
        Thread thread=new Thread(runnable);
        thread.start();;
        while (true){
            System.out.println("hello main");
            Thread.sleep(1000);
        }
    }
}

方法三:匿名内部类

相比于上边俩个方法不由构建一个新的类,他是匿名的,代码量减少,缺点在于只能使用一次。

复制代码
package thread;

public class Demo3 {
    public static void main(String[] args)throws InterruptedException {
        Thread thread=new Thread(){
            public void run(){
                while (true){
                    System.out.println("heloo world");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        };
        thread.start();;
        while (true){
            System.out.println("hello main");
            Thread.sleep(1000);
        }
    }
}

方法四:Runnable的匿名内部类

疑点1

需要重写run方法,这里有个问题,Runnable不是一个接口么可以创建一个对象,这里注意了,这是一个匿名内部类的写法,相当于是在这个接口的匿名内部类对象。这个类也相当于一个任务,多线程是需要靠Thread来实现的,所以我们要把这个任务交给Thread来处理!

复制代码
package thread;

public class Demo4 {
    public static void main(String[] args)throws InterruptedException {
        Runnable runnable=new Runnable() {
            @Override
            public void run() {
                while (true){
                    System.out.println("hello world");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
        }
        };
        Thread thread=new Thread(runnable);
        thread.start();
        while (true){
            System.out.println("hello main");
            Thread.sleep(1000);
        }

    }
}

方法五:Lambda方法

这个方法是我们最常用的的方法:简单、高效、函数式

复制代码
package thread;

public class Demo5 {
    public static void main(String[] args)throws InterruptedException{
        Thread thread=new Thread(()->{
           while (true){
               System.out.println("hello thread");
               try {
                   Thread.sleep(1000);
               } catch (InterruptedException e) {
                   throw new RuntimeException(e);
               }
           }
        });
        thread.start();
        while (true){
            System.out.println("hello main");
            Thread.sleep(1000);
        }
    }
}
疑点2

使用Thread.sleep()这个为什么没有import,为什么不需要导入包呢?

这是因为Thread位于Java.lang包中,这是java中的默认包,像Thread、String、System这些都不需要手写import.

重点标记带:

1.

thread.start().就是开启一个新的线程,多一个执行流,main是一个线程,这个也是一个线程。

在Thread的run方法中没有抛出异常,所以我们用try catch去处理,但是在main方法中我们可以向上抛异常交给jvm处理

2.为什么不直接调用Thread.run() 还要那么麻烦?

我们的初心是建立一个新的线程一起工作,提高效率,如果直接调用,和以前调用方法一样,没有提高效率,所以Thread.start()创建新的线程是必不可少的。

四、前台线程和后台线程

4.1. 核心区别

  • JVM 退出机制

  • 用户线程:只要存在至少一个用户线程运行,JVM 就不会退出。例如 main() 主线程是用户线程,若创建新用户线程执行耗时任务,JVM 会等待所有用户线程结束后才终止。

  • 守护线程:当所有用户线程终止后,JVM 会立即退出,无论守护线程是否执行完毕。例如垃圾回收(GC)线程是典型的守护线程,负责后台清理内存。

  • 创建方式

  • 用户线程:默认创建的线程均为用户线程(如 new Thread() )。

  • 守护线程:需显式调用 thread.setDaemon(true) (必须在 start() 前设置)。

  • 应用场景

  • 用户线程:执行核心业务逻辑,如 Web 请求处理、数据库操作、文件 IO 等。

  • 守护线程:执行辅助性任务,如日志记录、GC、监控服务、心跳检测等。

4.2. 关键细节

  • 线程优先级:用户线程与守护线程在优先级上无本质区别,由操作系统调度。

  • 资源释放风险:守护线程被强制终止时可能导致资源泄漏(如文件句柄未关闭),需谨慎处理。

  • 子线程继承性:守护线程创建的子线程默认也是守护线程。

  • 线程池默认类型: Executors 创建的线程池默认是用户线程;若需后台执行,需自定义线程工厂并设置 setDaemon(true) 。

    package thread;

    public class Demo7 {
    public static void main(String[] args)throws InterruptedException {
    Thread thread=new Thread(()->{
    while (true){
    System.out.println("hello thread");
    try {
    Thread.sleep(1000);
    } catch (InterruptedException e) {
    throw new RuntimeException(e);
    }
    }
    });
    //这样设置后台线程必须放在start之前
    //主线程结束后台线程也将会结束
    thread.setDaemon(true);
    thread.start();
    for (int i = 0; i <3 ; i++) {
    System.out.println("hello main");
    Thread.sleep(1000);
    }
    System.out.println("main 结束");
    }
    }

4. 3 常见误区

  • 误区 1:守护线程会执行完任务。

  • 正确:若 JVM 退出,守护线程可能被强制终止,未完成的任务会丢失。

  • 误区 2:守护线程不能做 IO 操作。

  • 正确:守护线程可以执行 IO,但需确保操作幂等或可恢复。

  • 误区 3: setDaemon(true) 可随时调用。

  • 正确:必须在 start() 前设置,否则抛出 IllegalThreadStateException 。

4.4总结

  • 用户线程:业务逻辑主力,JVM 需等待其完成。

  • 守护线程:后台辅助任务,随 JVM 生命周期终止。

合理选择线程类型可优化资源利用率,例如关键任务用用户线程,非关键任务用守护线程。

五、线程使用外面变量的那些事

Lambda一般使用外面的变量,当变量在mian方法里面的时候,这里就会有一个问题,lambda是一个回调函数(理解执行的时候会慢一点),这里就一种可能,当我们开始执行线程的时候·,main方法执行完了,这个局部变量就被销毁了,执行线程的时候,就捕获不了这个变量了。

复制代码
 public static void main(String[] args) throws InterruptedException{
       boolean isFinished =false;
        //lambda是回调函数,执行是很久以后,很有可能线程建好了,main方法也执行完了,才开始这个线程
        Thread t=new Thread(()->{
            while (!isFinished){
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println("thread线程结束");
        });

所以我们最好把这个变量放在main方法外面这个问题就迎刃而解了

复制代码
public class Demo10 {
    public static boolean isFinished=false;
    public static void main(String[] args) throws InterruptedException{
       //boolean isFinished =false;
        //lambda是回调函数,执行是很久以后,很有可能线程建好了,main方法也执行完了,才开始这个线程
        Thread t=new Thread(()->{
            while (!isFinished){
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println("thread线程结束");
        });
        t.start();
        Thread.sleep(3000);
        isFinished=true;
    }
}

对于终止线程Thread提供了自己变量所以我们就不用创建了。

对于Thread.currentThread()这个方法,和this用法差不多在哪个线程调用就指的是哪个线程。

主动去终止,但是这个方法还有一个作用是唤醒sleep,当线程还在休眠的时候直接把线程唤醒了,这样会使代码到catch里面处理,如果我们只是仅仅继续抛出异常解决不了问题,所以我们可以加上break,终止循环,如果什么都不写就是不终止。

复制代码
public class Demo11 {
    public static void main(String[] args) throws InterruptedException{
        Thread t=new Thread(()->{
            while (!Thread.currentThread().isInterrupted()){
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    //throw new RuntimeException(e);
                    //这个线程翻桌了
                    //加上break就是终止
                    //啥都不就是不终止
                    //有其他逻辑,然后再break,就是稍后终止
                   // break;
                }
            }
            System.out.println("thread线程终止");
        });
        t.start();
        Thread.sleep(3000);
        System.out.println("main线程尝试终止t线程");
        t.interrupt();
        //main方法调用的,此时就是main
        //System.out.println("main:"+Thread.currentThread().getName());
    }
}

那么如果catch里面什么不加,这个循环会退出吗?

答案是否定的,其实是sleep在捣鬼,正常情况下isIterrupted的值是fasle,如果调用interrupt可以把这个值改成true,但是sleep被唤醒他又把这个值改回去了,所以这个循环依旧会继续,不会中断。

六、使用join()等待线程结束

多线程编程:使用 join() 等待线程结束

在多线程编程中,线程的调度是随机的,我们不喜欢随机调度,我们想要掌控感,这给程序执行顺序带来了不确定性。为了解决这个问题,Java 提供了 join() 方法,让我们能够控制线程的结束顺序。

join() 方法的作用

join() 方法的主要作用是让一个线程等待另一个线程执行完毕。具体来说:

· 当在某个线程(如主线程)中调用 t.join() 时,当前线程会进入等待状态

· 直到线程 t 执行完成后,当前线程才会继续执行

· 这样可以确保线程之间的执行顺序符合预期

复制代码
public class Demo12 {
    public static void main(String[] args) throws InterruptedException{
        Thread t=new Thread(()->{
            for (int i = 0; i <3 ; i++) {
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }

            }
            System.out.println("thread 线程结束");
        });
        t.start();
        t.join(3000,599);
        System.out.println("main线程结束");
    }
}

正常情况下main线程如果没有任务的话瞬间会结束进程,但是有等待方法的话会等待t线程结束后结束。

还有一个问题就是如果t线程的任务需要很就或者一直进行。我们不规定等多久他会一直死等下去,我们不希望看到这种现象所以通常情况下我们要规定时间!

为什么需要 join()?

虽然可以通过 sleep() 方法设置休眠时间来控制线程执行顺序,但这种方法存在明显缺陷:

  1. 时间难以精确控制:无法准确预测线程需要执行多长时间

  2. 资源浪费:可能设置过长等待时间,降低程序效率

  3. 不可靠:如果线程执行时间超过预设值,仍会出现顺序问题

核心要点

  1. 确定性执行顺序:join() 提供了确定性的线程结束顺序控制

  2. 异常处理:join() 可能抛出 InterruptedException,需要进行异常处理

  3. 替代方案:相比通过睡眠时间控制线程顺序,join() 更加科学和可靠

总结

join() 方法是多线程编程中重要的同步工具,它解决了线程执行顺序的不确定性问题,让开发者能够编写出更加可控和可靠的多线程程序。在实际开发中,当需要确保某个线程完成后才能继续执行后续操作时,join() 是最佳选择。

相关推荐
毕设源码-赖学姐2 小时前
【开题答辩全过程】以 J2EE技术在在线购物分享应用中的应用为例,包含答辩的问题和答案
java·java-ee
Tadas-Gao2 小时前
微服务可观测性的“1-3-5”理想:从理论到实践的故障恢复体系
java·开发语言·微服务·云原生·架构·系统架构·可观测
Swift社区2 小时前
SQL 执行异常排查 java.sql.SQLException:从 SQLException 说起
java·数据库·sql
ss2732 小时前
手写MyBatis第88弹:从XML配置到可执行SQL的完整旅程
java·开发语言·mybatis
Never_Satisfied2 小时前
在JavaScript / HTML中,实现`<iframe>` 自适应高度
开发语言·javascript·html
Cx330❀2 小时前
《C++ STL:vector类(上)》:详解基础使用&&核心接口及经典算法题
开发语言·c++·经验分享·算法
那我掉的头发算什么2 小时前
【数据结构】二叉树的高频热门面试题大全
java·开发语言·数据结构·python·算法·链表·intellij idea
一人の梅雨2 小时前
买家秀接口深度开发:从内容解析到情感分析的全链路实现
开发语言·php
遇安.YuAn2 小时前
JAVA之求平方根
java·开发语言·算法