JAVA中的多线程详解

news2025/1/12 10:51:54

1.概念

进程(Process):

进程是一个包含自身执行地址的程序,多线程使程序可以同时存在多个执行片段,这些执行片段根据不同的条件和环境同步或者异步工作,由于转换的数独很快,使人感觉上进程像是在同时运行。

现在的计算机基本上都支持多进程操作,比如使用计算机时可以变上网,边听歌。然而计算机上只有一块CPU,并不能同时运行这些进程,CPU实际上是利用不同的时间段去交替执行每个进程。

线程(Thread): 

在一个进程内部可以执行多项任务,进程内部的任务被称为线程,线程是进程中的实体,一个进程可以拥有多个线程。

多线程是指在一个程序中同时执行多个线程,每个线程都有自己独立的执行路径。在多线程中,程序的执行可以同时进行多个任务,从而提高系统的资源利用率和响应性能。

在传统的单线程编程模型中,程序按照顺序执行,一次只处理一个任务。这种方式在某些情况下可能会导致效率低下或者无法满足需求。而多线程通过将任务拆分为多个子任务,并且在不同的线程上同时执行,从而实现并发处理。

对于 Java 初学者来说,多线程的很多概念听起来就很难理解。比方说:

  • 进程,是对运行时程序的封装,是系统进行资源调度和分配的基本单位,实现了操作系统的并发。
  • 线程,是进程的子任务,是 CPU 调度和分派的基本单位,实现了进程内部的并发。

很抽象,对不对?打个比喻,你在打一把王者(其实我不会玩哈 doge):

  • 进程可以比作是你开的这一把游戏
  • 线程可以比作是你所选的英雄或者是游戏中的水晶野怪等之类的。

带着这个比喻来理解进程和线程的一些关系,一个进程可以有多个线程就叫多线程。是不是感觉非常好理解了?

❤1、线程在进程下进行

(单独的英雄角色、野怪、小兵肯定不能运行)

❤2、进程之间不会相互影响,主线程结束将会导致整个进程结束

(两把游戏之间不会有联系和影响。你的水晶被推掉,你这把游戏就结束了)

❤3、不同的进程数据很难共享

(两把游戏之间很难有联系,有联系的情况比如上把的敌人这把又匹配到了)

❤4、同进程下的不同线程之间数据很容易共享

(你开的那一把游戏,你可以看到每个玩家的状态——生死,也可以看到每个玩家的出装等等)

❤5、进程使用内存地址可以限定使用量

(开的房间模式,决定了你可以设置有多少人进,当房间满了后,其他人就进不去了,除非有人退出房间,其他人才能进)

2.线程的创建

在JAVA语言中,线程也是一种对象,但是并非任何对象都可以称为线程,只有实现了Runnable接口或者继承了Thread类的对象才能成为线程。

2.1 Thread类

public class MyThread extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(getName() + ":打了" + i + "个小兵");
        }
    }

    public static void main(String[] args) {
        //创建MyThread对象
        MyThread t1=new  MyThread();
        MyThread t2=new  MyThread();
        MyThread t3=new  MyThread();
//设置线程的名字
        t1.setName("鲁班");
        t2.setName("刘备");
        t3.setName("亚瑟");
//启动线程
        t1.start();
        t2.start();
        t3.start();

    }
}

运行结果:

 2.2 Runnable接口

创建一个类实现 Runnable 接口,并重写 run 方法。本质上来讲,Runnable接口是JAVA接口语言中用以实现线程的接口,任何实现线程功能的类都必须实现这个接口。

虽然可以使用继承Thread类的方式来实现线程,但是在java语言中,只能继承一个类,如果用户定义的类已经继承其他类,就无法再继承Thread类,无法使用线程,所以Runnable接口就这么诞生了,实现Runnable接口和继承Thread类具有相同的效果,通过实现这个接口就可以使用线程。Runnable接口定义了一个run()方法,在实例化一个Thread对象的时候,可以传入一个实现Runnable接口作为参数,Thead类会调用Runnable对象的run()方法,继而执行run()方法中的内容。

public class MyRunnable implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            try {//sleep会发生异常要显示处理
                Thread.sleep(20);//暂停20毫秒
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "打了:" + i + "个小兵");
        }
    }

    public static void main(String[] args) {
        //创建MyRunnable类
        MyRunnable mr = new MyRunnable();
//创建Thread类的有参构造,并设置线程名
        Thread t1 = new Thread(mr, "张飞");
        Thread t2 = new Thread(mr, "貂蝉");
        Thread t3 = new Thread(mr, "吕布");
//启动线程
        t1.start();
        t2.start();
        t3.start();

    }
}

运行结果:

2.3 实现 Callable 接口

  • 实现 Callable 接口,重写 call 方法,这种方式可以通过 FutureTask 获取任务执行的返回值。
  • 重写call()方法:Callable接口中的call()方法和run()方法很相似,但是call()可以返回一个值,并且可以抛出检查型异常。
  • 获取结果:使用Future对象对get()方法获取callable任务的结果
public class CallerTask implements Callable<String> {
    public String call() throws Exception {
        return "Hello,i am running!";
    }

    public static void main(String[] args) {
        //创建异步任务
        FutureTask<String> task=new FutureTask<String>(new CallerTask());
        //启动线程
        new Thread(task).start();
        try {
            //等待执行完成,并获取返回结果
            String result=task.get();
            System.out.println(result);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

3.关于线程的一些疑问

3.1 为什么要重写 run 方法?

这是因为默认的run()方法不会做任何事情。

为了让线程执行一些实际的任务,我们需要提供自己的run()方法实现,这就需要重写run()方法

public class MyThread extends Thread {
  public void run() {
    System.out.println("MyThread running");
  }
}

在这个例子中,我们重写了run()方法,使其打印出一条消息。当我们创建并启动这个线程的实例时,它就会打印出这条消息。

 2.1 run 方法和 start 方法有什么区别?

  • run():封装线程执行的代码,直接调用相当于调用普通方法。
  • start():启动线程,然后由 JVM 调用此线程的 run() 方法。

3.1 通过继承 Thread 的方法和实现 Runnable 接口的方式创建多线程,哪个好? 

实现 Runable 接口好,原因有两个:

  • ♠①、避免了 Java 单继承的局限性,Java 不支持多重继承,因此如果我们的类已经继承了另一个类,就不能再继承 Thread 类了。
  • ♠②、适合多个相同的程序代码去处理同一资源的情况,把线程、代码和数据有效的分离,更符合面向对象的设计思想。Callable 接口与 Runnable 非常相似,但可以返回一个结果。

 4.控制线程的其他方法

1.sleep

sleep方法用于暂停当前正在执行的线程的执行指定时间(以毫秒为单位)。sleep方法定义在Thread类中,其主要的用途是让当前线程暂停,让其他的线程有机会运行。需要注意的是,sleep 的时候要对异常进行处理。

public class SleepExample {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            try {
                System.out.println("线程开始执行,即将进入睡眠状态...");
                Thread.sleep(3000); // 暂停3秒
                System.out.println("线程从睡眠状态唤醒");
            } catch (InterruptedException e) {
                System.out.println("线程被中断:" + e.getMessage());
                Thread.currentThread().interrupt(); // 重置中断状态
            }
        });

        thread.start();
    }
}

2.join

等待这个线程执行完才会轮到后续线程得到 cpu 的执行权,使用这个也要捕获异常。

//创建MyRunnable类
MyRunnable mr = new MyRunnable();
//创建Thread类的有参构造,并设置线程名
Thread t1 = new Thread(mr, "张飞");
Thread t2 = new Thread(mr, "貂蝉");
Thread t3 = new Thread(mr, "吕布");
//启动线程
t1.start();
try {
    t1.join(); //等待t1执行完才会轮到t2,t3抢
} catch (InterruptedException e) {
    e.printStackTrace();
}
t2.start();
t3.start();

3.setDaemon() 

这个是用于将线程设置为守护线程,属于服务类的线程,用于为用户线程提供服务。当所有的用户线程都结束时,守护线程会自动结束。

public class CallerTask{
    public static void main(String[] args) {
        Thread daemonThread =new Thread(()->{
            System.out.println("守护线程启动");
            try{
                //模拟长时间的运行任务
                while (true){
                    Thread.sleep(1000);
                    System.out.println("守护线程正在进行");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
                System.out.println("守护线程被中断,即将结束");
            }
        });
        daemonThread.setDaemon(true);
        daemonThread.start();

        //主线程运行一小段时间后结束
        try{
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("主线程结束");
    }
}

在这个示例中,主线程启动一个守护线程,并将其设置为守护状态。主线程在运行3秒后结束,此时所有的用户线程都已结束,守护线程中断。

4.yield()

这个是Thread中的静态方法,它可以使当前正在执行的线程让出对CPU的使用权,从而使得其它同优先级的线程有机会被CPU调度执行,这个方法主要用于线程之间的协作,尤其是在需要让出CPU一遍其他线程可以运行时。

class YieldExample {
    public static void main(String[] args) {
        Thread thread1 = new Thread(YieldExample::printNumbers, "刘备");
        Thread thread2 = new Thread(YieldExample::printNumbers, "关羽");

        thread1.start();
        thread2.start();
    }

    private static void printNumbers() {
        for (int i = 1; i <= 5; i++) {
            System.out.println(Thread.currentThread().getName() + ": " + i);

            // 当 i 是偶数时,当前线程暂停执行
            if (i % 2 == 0) {
                System.out.println(Thread.currentThread().getName() + " 让出控制权...");
                Thread.yield();
            }
        }
    }
}

运行结果: 

从这个结果可以看得出来,即便有时候让出了控制权,其他线程也不一定会执行。

5.线程的生命周期

  •  第一是创建状态。在生成线程对象,并没有调用该对象的 start 方法,这是线程处于创建状态。
  • 第二是就绪状态。当调用了线程对象的 start 方法之后,该线程就进入了就绪状态,但是此时线程调度程序还没有把该线程设置为当前线程,此时处于就绪状态。在线程运行之后,从等待或者睡眠中回来之后,也会处于就绪状态。
  • 第三是运行状态。线程调度程序将处于就绪状态的线程设置为当前线程,此时线程就进入了运行状态,开始运行 run 函数当中的代码。
  • 第四是阻塞状态。线程正在运行的时候,被暂停,通常是为了等待某个时间的发生(比如说某项资源就绪)之后再继续运行。sleep,suspend,wait 等方法都可以导致线程阻塞。
  • 第五是死亡状态。如果一个线程的 run 方法执行结束或者调用 stop 方法后,该线程就会死亡。对于已经死亡的线程,无法再使用 start 方法令其进入就绪。

6.Callable,Future和FutureTask

我们讲述了创建线程的 3 种方式,一种是直接继承 Thread,一种是实现 Runnable 接口,另外一种是实现 Callable 接口。

前 2 种方式都有一个缺陷:在执行完任务之后无法获取执行结果。

如果需要获取执行结果,就必须通过共享变量或者线程通信的方式来达到目的,这样使用起来就比较麻烦。

Java 1.5 提供了 Callable、Future、FutureTask,它们可以在任务执行完后得到执行结果,今天我们就来详细的了解一下。

Runnable接口:

  • 没有返回值
  • 没有异常抛出的限制
  • 可以通过Thread类的run()方法执行

Callable接口: 

  • 有返回值,返回值类型为object
  • 可以抛出异常
  • 必须通过ExecutorService来执行,并且返回一个Future对象
// Runnable接口实现
public class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("Runnable is running");
    }
}

// Callable接口实现
public class MyCallable implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        System.out.println("Callable is running");
        return 123;
    }
}

// FutureTask使用
public class FutureTaskExample {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService executor = Executors.newSingleThreadExecutor();
        
        // 使用Runnable
        MyRunnable myRunnable = new MyRunnable();
        Thread thread = new Thread(myRunnable);
        thread.start();
        
        // 使用Callable和FutureTask
        MyCallable myCallable = new MyCallable();
        FutureTask<Integer> futureTask = new FutureTask<>(myCallable);
        executor.execute(futureTask);
        
        // 获取Callable任务的结果
        Integer result = futureTask.get(); // 阻塞直到任务完成
        System.out.println("Callable returned: " + result);
        
        executor.shutdown();
    }
}

那怎么使用 Callable 呢?

一般会配合 ExecutorServiceopen in new window(后面在讲线程池的时候会细讲,这里记住就行)来使用。

ExecutorService 是一个接口,位于 java.util.concurrent 包下,它是 Java 线程池框架的核心接口,用来异步执行任务。它提供了一些关键方法用来进行线程管理。

异步计算Future接口:

Future接口,它代表了异步的计算结果。当你将一个任务提交给ExectorService执行时,它返回一个Future对象,这个Future对象运行你查询任务的状态,获取任务结果,取消任务等等。

以下是Future接口的一些关键特性和方法:

查询任务状态:

  • isDone():检查任务是否完成
  • isCanncelled():检查任务是否被取消

获取结果:

  • get():获取任务的结果,如果任务尚未完成,则次方法会阻塞 
  • get(long timeout,TimeUnit unit):获取任务的结果,如果任务在指定的时间内未完成,则会抛出TimeoutException

 取消任务:

cancle(boolean mayInterruptIfRunning):尝试取消任务。如果任务尚未开始,或者已经开始但未完成,则可以取消任务。参数mayInterruptIfRunning分running决定是否运行中断正在运行的任务。

获取任务是否被取消:

ifCancelled():返回任务是否被取消。

获取任务是否成功完成:

isDone():返回任务是否成功完成

也就是说 Future 提供了三种功能:

  • 1)判断任务是否完成;
  • 2)能够中断任务;
  • 3)能够获取任务执行结果。

由于 Future 只是一个接口,如果直接 new 的话,编译器是会有一个 ⚠️ 警告的,它会提醒我们最好使用 FutureTask。

实际上,FutureTask 是 Future 接口的一个唯一实现类,我们在前面的例子中 executorService.submit() 返回的就是 FutureTask,通过 debug 模式可以观察到。

异步计算结果 FutureTask 实现类

我们来看一下 FutureTask 的实现:

public class FutureTask<V> implements RunnableFuture<V>

FutureTask 类实现了 RunnableFuture 接口,我们看一下 RunnableFuture 接口的实现:

public interface RunnableFuture<V> extends Runnable, Future<V> {
    void run();
}

可以看出 RunnableFuture 继承了 Runnable 接口和 Future 接口,而 FutureTask 实现了 RunnableFuture 接口。所以它既可以作为 Runnable 被线程执行,又可以作为 Future 得到 Callable 的返回值。

FutureTask 提供了 2 个构造器:

public FutureTask(Callable<V> callable) {
}
public FutureTask(Runnable runnable, V result) {
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1957099.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Kafka知识总结(事务+数据存储+请求模型+常见场景)

文章收录在网站&#xff1a;http://hardyfish.top/ 文章收录在网站&#xff1a;http://hardyfish.top/ 文章收录在网站&#xff1a;http://hardyfish.top/ 文章收录在网站&#xff1a;http://hardyfish.top/ 事务 事务Producer保证消息写入分区的原子性&#xff0c;即这批消…

nodejs与npm版本对应表

Node.js — Node.js 版本 (nodejs.org)

GB28181国标视频汇聚平台EasyCVR视频管理系统如何更改GIS地图的默认位置?

GB28181国标视频汇聚平台EasyCVR视频管理系统以其强大的拓展性、灵活的部署方式、高性能的视频能力和智能化的分析能力&#xff0c;为各行各业的视频监控需求提供了优秀的解决方案。通过简单的配置和操作流程&#xff0c;用户可以轻松地进行远程视频监控、存储和查看&#xff0…

【Qt开发】No matching signal for on_toolButton_clicked() 解决方案

【Qt开发】No matching signal for on_toolButton_clicked() 解决方案 文章目录 No matching signal for xxx 解决方案附录&#xff1a;C语言到C的入门知识点&#xff08;主要适用于C语言精通到Qt的C开发入门&#xff09;C语言与C的不同C中写C语言代码C语言到C的知识点Qt开发中…

【C51】8051 微控制器入门指南

目录 1. 理解 C51 编程环境1.1 了解 8051 微控制器架构1.2 设置开发环境 2. 编写 C51 嵌入式代码2.1 基础代码结构2.2 使用寄存器和 I/O 端口2.3 中断处理2.4 调试和测试 3. 高级特性和优化3.1 嵌套中断3.2 内存管理3.3 外设接口3.4 编译器优化 4. 示例项目4.1 LED 闪烁程序4.2…

vardaccico前端私有库

vardacico docker pull verdaccio/verdaccio:4 docker run -it --rm --name verdaccio -p 4873:4873 verdaccio/verdaccio Docker | Verdaccio 拷贝docker中的配置到宿主机 进入docker内部 docker exec -it verdaccio /bin/sh 进入到指定目录 cd /verdaccio 开始拷贝到指定目…

BOM管理挑战:识别不同业务需求下的应对策略

BOM作为制造行业中的核心概念&#xff0c;其架构的复杂度直接影响到企业的运营效率、成本控制以及市场响应速度。道合顺接下来将介绍如何在管理目标与BOM架构复杂度之间找到平衡点&#xff0c;以满足不同业务需求&#xff0c;助力企业在激烈的市场竞争中脱颖而出。 一、理解BO…

window下编译UCL

window下编译UCL 一、环境安装二、编译error: ACC conformance test failed. Stop. 一、环境安装 安装minGW minGW中安装g 和gcc 安装msys-base 二、编译 启动msys.bat C:\MinGW\msys\1.0\msys.bat 切换到ucl源码目录 执行语句 ./configure CPPFLAGS"$CPPFLAGS -stdc…

昇思25天学习打卡营第19天|DCGAN生成漫画头像

DCGAN生成漫画头像总结 实验概述 本实验旨在利用深度卷积生成对抗网络&#xff08;DCGAN&#xff09;生成动漫头像&#xff0c;通过设置网络、优化器以及损失函数&#xff0c;使用MindSpore进行实现。 实验目的 学习和掌握DCGAN的基本原理和应用。熟悉使用MindSpore进行图像…

气象水文耦合模WRF-Hydro建模技术

原文链接&#xff1a;气象水文耦合模WRF-Hydro建模技术https://mp.weixin.qq.com/s?__bizMzUzNTczMDMxMg&mid2247610398&idx4&sn34b4bbed4c74dcbbb0ac19ef8dcdaaff&chksmfa8271f9cdf5f8ef34ea6f721736a2fbbf8be896744ab7e46caa571c52a30628f056b4bd6964&t…

AI如何助力UI设计师互联网学习?

嘿&#xff0c;咱 UI 设计师想用互联网学习&#xff0c;可真不容易&#xff01;资料筛选难&#xff0c;学习资源杂&#xff0c;真让人头疼。不过还好有 AI 工具能帮忙&#xff0c;提效率&#xff01; 这一年多来&#xff0c;我在 ai123.cn 这个平台上&#xff0c;可算是找到了…

羊大师:羊奶营养揭秘,健康关爱的另一优选选择

在琳琅满目的乳制品中&#xff0c;羊奶如同一颗璀璨的明珠&#xff0c;以其独特的营养价值和健康益处&#xff0c;逐渐走进千家万户&#xff0c;成为许多人健康关爱的新选择。那么&#xff0c;羊奶究竟蕴藏着怎样的营养奥秘&#xff0c;让它能够在众多饮品中脱颖而出呢&#xf…

【MySQL】:在Centos 7 环境下的安装

朋友们、伙计们&#xff0c;我们又见面了&#xff0c;本期来给大家带来如何在Centos7环境下安装MySQL&#xff0c;如果看完之后对你有一定的启发&#xff0c;那么请留下你的三连&#xff0c;祝大家心想事成&#xff01; C 语 言 专 栏&#xff1a;C语言&#xff1a;从入门到精通…

关于C++11一些新特性的介绍(下)

文章目录 1. 可变参数模板1.1 可变参数模板介绍1.2 STL容器中的empalce相关接口函数 2. lambda表达式2.1 lambda诞生背景2.2 lambda表达式语法2.3 捕捉列表说明2.4 函数对象与lambda表达式 3. 包装器3.1 function包装器3.2 bind函数 1. 可变参数模板 1.1 可变参数模板介绍 C1…

我的创作纪念日(一)——Giser?Noder?不如“Computer”

目录 Giser&#xff1f;Noder&#xff1f;不如“Computer” 一、根源&#xff1a;保持学习习惯的刚需 二、机缘&#xff1a;processOn的另类替代 三、日常&#xff1a;对技术栈丰富的思考 四、成就&#xff1a;保持心态健康的活着 五、憧憬&#xff1a;能一直心态健康的活…

大模型学习(1)

初学者&#xff0c;仅做自己学习记录&#xff0c;如果对你有什么帮助&#xff0c;那更好了。 下面是论文《Attention Is All You Need》的经典transformer架构&#xff0c;在学习的过程中&#xff0c;有很多疑惑。 embedding层在做什么 Transformer的embedding层在做的是将输…

【C++进阶】AVL树详解

文章目录 1. AVL树的概念2. AVL树结点的定义3. AVL 树的插入3.1 关于平衡因子3.2 插入代码 4. AVL 树的旋转逻辑4.1 不需要旋转4.2 左旋4.3 右旋4.4 双旋4.4.1 先右后左单旋&#xff08;RL 旋转&#xff09;4.4.2 先左后右单旋&#xff08;LR 旋转&#xff09; 4.5 完整插入代码…

正则采集器之五——商品匹配规则

需求设计 实现分析 系统通过访问URL得到html代码&#xff0c;通过正则表达式匹配html&#xff0c;通过反向引用来得到商品的标题、图片、价格、原价、id&#xff0c;这部分逻辑在java中实现。 匹配商品的正则做成可视化编辑&#xff0c;因为不同网站的结构不同&#xff0c;同…