什么是多线程?进程和线程的区别是什么?如何使用Java实现多线程?

news2024/11/25 15:54:33

在这里插入图片描述

文章目录

  • 前言
  • 我们为什么要使用线程而不是进程来实现并发编程
  • 什么是线程
  • 进程和线程的区别
  • 如何使用Java实现多线程
    • 创建线程
      • 1.创建一个继承 Thread 类的线程类
      • 2.实现 Runnable 接口
        • 匿名内部类方式实现 Runnable 接口
        • lambda 表达式实现 Runnable 接口
    • Thread 类的常见构造方法
    • Thread 的几个常见属性
    • 启动线程
    • 终止线程
      • 1.自定义标志位终止线程
      • 2.使用 Thread 自带的标志位终止线程
    • 线程等待

前言

前面我们了解了什么是进程以及如何实现进程调度,那么今天我将为大家分享关于线程相关的知识。在学习线程之前,我们认为进程是操作系统执行独立执行的单位,但其实并不然。线程是操作系统中能够独立执行的最小单元。只有掌握了什么是线程,我们才能实现后面的并发编程。

我们为什么要使用线程而不是进程来实现并发编程

实现并发编程为什么不使用多进程,而是使用多线程呢?主要体现在几个方面:

  • 创建一个进程的开销很大
  • 调度一个进程的开销很大
  • 销毁一个进程的开销很大

开销不仅体现在时间上,还体现在内存和 CPU 上。现在以”快“著称的互联网时代,这种大开销是不受人们欢迎的。那么为什么多线程就可以实现快捷的并发编程呢?

  • 共享资源:多个线程之间共用同一部分资源,大大减少了资源的浪费
  • 创建、调度、销毁的开销小:相较于进程的创建、调度和销毁,线程的创建、调度和销毁就显得很轻量,这样也大大节省了时间和资源的浪费
  • 现在的计算机 CPU 大多都是多核心模式,我们的多线程模式也更能利用这些优势

什么是线程

线程是操作系统能够独立调度和执行的最小执行单元。线程是进程内的一个执行流程,也可以看作是进程的子任务。与进程不同,线程在进程内部创建和管理,并且与同一进程中的其他线程共享相同的地址空间和系统资源。
只有当第一个线程创建的时候会有较大的开销,后面线程的创建开销就会小一点。并发编程会尽量保证每一个线程在不同的核心上单独执行,互不干扰,但也不可避免的出现在单核处理器系统中,线程在一个 CPU 核心上运行,它们通过时间片轮转调度算法使得多个线程轮流执行,给我们一种同时执行感觉。

线程是操作系统调度执行的基本单位

进程和线程的区别

一个进程中可以包含一个线程,也可以包含多个线程。

  1. 资源和隔离:进程是操作系统中的一个独立执行单位,具有独立的内存空间、文件描述符、打开的文件、网络连接等系统资源。每个进程都拥有自己的地址空间,进程间的数据不共享。而线程是进程内的执行流程,共享同一进程的地址空间和系统资源,可以直接访问和修改相同的数据。

  2. 创建和销毁开销:相对于进程,线程的创建和销毁开销较小。线程的创建通常只涉及创建一个新的执行上下文和一些少量的内存。而进程的创建需要分配独立的内存空间、加载可执行文件、建立进程控制块等操作,开销较大。

  3. 并发性和响应性:由于线程共享进程的地址空间,多个线程可以在同一进程内并发执行任务,共享数据和通信更加方便。因此,线程的切换成本较低,可以实现更高的并发性和响应性。而进程之间通常需要进程间通信(IPC)的机制来进行数据交换和共享,开销较大,响应性较低。

  4. 管理和调度:进程由操作系统负责管理和调度,每个进程之间是相互独立的。而线程是在进程内部创建和管理的,线程调度和切换由操作系统的线程调度器负责。线程的调度通常比进程的调度开销小,线程切换更快。

  5. 安全性和稳定性:由于进程之间相互独立,一个进程的崩溃不会影响其他进程的正常运行,因此进程具有更好的安全性和稳定性。而一个线程的错误或异常可能会导致整个进程崩溃。

前面我们所说的 PCB 其实也是针对线程来说的,一个线程具有一个 PCB 属性,一个进程可以含有一个或多个 PCB。

PCB 里的状态:上下文,优先级,记账信息,都是每个线程有自己的,各自记录自己的,但是同一个进程里的PCB之间,pid是一样的,内存指针和文件描述符表也是一样的。

如何使用Java实现多线程

在Java中使用一个线程大致分为以下几个步骤:

  1. 创建线程
  2. 启动线程
  3. 终止线程
  4. 线程等待

创建线程

在Java中执行线程操作依赖于 Thread 类。并且创建一个线程具有多种方法。

  1. 创建一个线程类继承自 Thread 类
  2. 实现 Runnable 接口

1.创建一个继承 Thread 类的线程类

class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("这是一个MyThread线程");
    }
}

我们需要重写 run 方法,而 run 方法是指该线程要干什么。

创建实例对象

public class TreadDemo1 {
    public static void main(String[] args) {
        Thread t = new MyThread();
    }
}

2.实现 Runnable 接口

创建一个线程我们不仅可以直接创建一个继承自 Thread 的线程类,我们也可以直接实现 Runnable 接口,因为通过源码我们可以知道 Thread 类也实现了 Runnable 接口。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

我们可以将 Runnable 作为一个构造方法的参数传进去。

class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("这是一个线程");
    }
}

public class ThreadDemo2 {
    public static void main(String[] args) {
        Thread t = new Thread(new MyRunnable());
    }
}

但是这种实现 Runnable 接口的方式会显得很麻烦,因为每个线程执行的内容大多是不同的,所以我们可以采用下面两种方式来实现 Runnable 接口。

  • 匿名内部类
  • lambda 表达式

匿名内部类方式实现 Runnable 接口

public class ThreadDemo3 {
    public static void main(String[] args) {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("这是一个线程");
            }
        });
    }
}

lambda 表达式实现 Runnable 接口

public class ThreadDemo4 {
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            System.out.println("这是一个线程");
        });
    }
}

Thread 类的常见构造方法

方法说明
Thread()创建对象
Thread(Runnable target)使用 Runnable 对象创建线程对象
Thread(String name)创建线程对象,并命名
Thread(Runnable target,String name)使用 Runnable 对象创建线程对象,并命名
Thread(ThreadGroup group,Runnable target线程可以被用来分组管理,分好的组即为线程组,这个我们目前了解即可

在这里插入图片描述

Thread 类有很多构造方法,大家有兴趣可以自己去看看。

Thread 的几个常见属性

属性获取方法
IDgetId()
名称getName()
状态getState()
优先级getPriority()
是否后台进程isDaemon()
是否存活isAlive
是否被中断isInterrupted()
  • ID 是线程的唯一标识,不同线程不会重复
  • 名称是各种调试工具用到
  • 状态表示线程当前所处的一个情况
  • 优先级高的线程理论上来说更容易被调度到
  • 关于后台线程,需要记住一点:JVM会在一个进程的所有非后台线程结束后,才会结束运行。
  • 是否存活,即简单的理解,为 run 方法是否运行结束了
  • 线程的中断问题,下面我们进一步说明

我们前面创建的都是前台进程,我们可以感知到的,那么什么叫做后台进程呢?

后台进程是指在计算机系统中以低优先级运行且不与用户交互的进程。与前台进程相比,后台进程在运行时不会占据用户界面或终端窗口,并且通常在后台默默地执行任务。

后台进程通常用于执行系统服务、长时间运行的任务、系统维护或监控等。它们在后台运行,不需要用户的直接参与或操作,而且可以持续运行,即使用户退出或注销系统。

启动线程

我们上面只是创建了线程,要想让线程真正的起作用,我们需要手动启动线程。线程对象.start()

class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("这是一个线程");
    }
}

public class ThreadDemo2 {
    public static void main(String[] args) {
        Thread t = new Thread(new MyRunnable());
        t.start();
    }
}

在这里插入图片描述

这里有人看到输出结果可能会问了,这跟我直接调用 run 方法好像没什么区别吧?我们这个代码肯定看不出来区别,所以我们稍稍修改一下代码。

class MyRunnable implements Runnable {
    @Override
    public void run() {
        while(true) {
            System.out.println("hello MyThread!");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

public class ThreadDemo2 {
    public static void main(String[] args) {
        Thread t = new Thread(new MyRunnable());
        t.start();
        while(true) {
            System.out.println("hello main!");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

这里 Thread.sleep() 的作用是使线程停止一会,防止进行的太快,我们不容易看到结果,并且这里的 Thread.sleep() 方法还需要我们抛出异常

在这里插入图片描述

我们可以看到这里的执行结果是 main 线程和 t 线程都执行了,而不是只执行其中的一个线程。不仅如此,这两个线程之间没有什么规定的顺序执行,而是随机的,这种叫做抢占式执行,每个线程都会争抢资源,所以会导致执行顺序的不确定,也正是因为多线程的抢占式执行,会导致后面的线程安全问题。

那么我们再来看看,如果直接调用 run 方法,而不是 start 方法会有什么结果。

在这里插入图片描述

当直接调用 run 方法的话,也就只会执行 t 对象的 run 方法,而没有执行 main 方法后面的代码,也就是说:当直接调用 run 方法的时候,线程并没有真正的启动,只有调用 start 方法,线程才会启动。

我们也可以通过 Java 自带的 jconsle 来查看当前有哪些Java进程。

我们需要找到 jconsole.exe 可执行程序。通常在这个目录下C:\Program Files\Java\jdk1.8.0_192\bin
在这里插入图片描述

我们也可以点进来看看。

在这里插入图片描述

在这里插入图片描述

终止线程

通常当主线程 main 执行完 mian 方法之后或者其他线程执行完 run 方法之后,线程就会终止,但是我们也可以在这之前手动终止线程。但是我们这里终止线程并不是立刻终止,也就相当于这里只是建议他这个线程停止,具体要不要停止得看线程的判断。

  • 自定义标志位来终止线程
  • 使用 Thread 自带的标志位来终止线程

1.自定义标志位终止线程

public class ThreadDemo4 {
    private static boolean flg = false;  //定义一个标志位
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            while(!flg) {
                System.out.println("hello mythread!");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        t.start();
        System.out.println("线程开始");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        flg = true;  /修改标志位使线程终止
        System.out.println("线程结束");
    }
}

在这里插入图片描述

2.使用 Thread 自带的标志位终止线程

可以使用 线程对象.interrupt() 来申请终止线程。并且使用 Thread.currentThread,isInterrupted() 来判断是否终止线程。

  • Thread.currentThread() 获取到当前线程对象
public class ThreadDemo4 {
    private static boolean flg = false;
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            while(!Thread.currentThread().isInterrupted()) {
                System.out.println("hello mythread!");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
        System.out.println("线程开始");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        t.interrupt();
        System.out.println("线程结束");
    }
}

在这里插入图片描述

发现了没,这里抛出了异常,但是线程并没有终止,为什么呢?问题出在哪里呢?

其实这里问题出在 Thread.sleep 上,如果线程在 sleep 中休眠,此时调用 interrupt() 会终止休眠,并且唤醒该线程,这里会触发 sleep 内部的异常,所以我们上面的运行结果就抛出了异常。那么为什么线程又被唤醒了呢?

interrupt 会做两件事:

  1. 把线程内部的标志位给设置成true,也就是 !Thread.current.isInterrupt() 的结果为true
  2. 如果线程在进行 sleep ,就会触发吟唱,把 sleep 唤醒

但是 sleep 在唤醒的时候,还会做一件事,把刚才设置的这个标志位,再设置回false(清空标志位),所以就导致了线程继续执行。那么如何解决呢?

很简单,因为 sleep 内部发生了异常,并且我们捕获到了异常,所以我们只需要在 catch 中添加 break 就行了。

try {
     	Thread.sleep(1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
        break;
    }

在这里插入图片描述

这也就相当于,我 t 线程拒绝了你的终止请求。

线程等待

在多线程中,可以使用 线程对象.join() 来使一个线程等待另一个线程执行完或者等待多长时间后再开始自己的线程。

方法说明
public void join()等待线程结束
public void join(long millis)等待线程结束,最多等 millis 毫秒
public void join(long millis,int nanos)同理,但可以更高精度
public class ThreadDemo5 {
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            for(int i = 0; i < 5; i++) {
                System.out.println("hello mythread!");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        t.start();
        try {
            t.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        for(int i = 0; i < 5; i++) {
            System.out.println("hello main!");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

在这里插入图片描述

在那个线程中调用的 线程对象.join() 就是哪个线程等待,而哪个线程调用 join() 方法,那么这个线程就是被等待的。而这个等待的过程也被称为阻塞。如果在执行 join 的时候,调用 join 方法的线程如果已经结束了,那么就不会发生阻塞。

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

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

相关文章

NO.1 MyBatis配置文件:配置连接数据库的环境,实现数据库连接

目录 1、MyBatis配置数据库环境的连接方式 1.1连接方式一&#xff1a;MyBatis核心配置文件配置数据库连接信息 1.2连接方式二&#xff1a;在MyBatis核心配置文件中引入properties文件&#xff0c;配置数据库的环境 2、MyBatisd核心配置文件连接数据库的环境完整配置信息 3…

基于Matlab实现心电信号小波特征提取和对应疾病识别仿真(附上源码+数据集)

本文基于Matlab平台&#xff0c;研究了心电信号的小波特征提取方法&#xff0c;并应用于心电信号疾病识别仿真实验中。首先&#xff0c;介绍了心电信号的基本特征和常见的心电疾病。然后&#xff0c;详细阐述了小波变换的原理和方法&#xff0c;并提出了一种基于小波分解和小波…

[Leetcode] [Tutorial] 多维动态规划(未完待续)

文章目录 62. 不同路径Solution 62. 不同路径 一个机器人位于一个 m ∗ * ∗ n 网格的左上角 &#xff08;起始点在下图中标记为 “Start” &#xff09;。 机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角。 问总共有多少条不同的路径&#xff1f; 示例…

线程转换状态,傻傻分不清等待和阻塞吗?你还在暴力的停止线程吗?

线程切换 线程创建之后&#xff0c;调用start()方法开始运行。当线程执行wait()方法之后&#xff0c;线程进入等待状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态&#xff0c;而超时等待状态相当于在等待状态的基础上增加了超时限制&#xff0c;也就是超…

StringJoiner

1、为什么要学习StringJoiner&#xff1f; 2、StringJoiner概述 StringJoiner跟StringBuilder一样&#xff0c;也可以看成一个容器&#xff0c;创建之后里面的内容是可变的。 2.1、作用 提高字符串的操作效率&#xff0c;而且代码编写特别简洁&#xff0c;但是目前市场上很少有…

用友时空KSOA SQL注入漏洞复现(HW0day)

0x01 产品简介 用友时空KSOA是建立在SOA理念指导下研发的新一代产品&#xff0c;是根据流通企业最前沿的I需求推出的统一的IT基础架构&#xff0c;它可以让流通企业各个时期建立的IT系统之间彼此轻松对话&#xff0c;帮助流通企业保护原有的IT投资&#xff0c;简化IT管理&#…

学习笔记整理-JS-03-表达式和运算符

[[toc]] 一、表达式和运算符 1. 表达式 表达式种类 算术、关系、逻辑、赋值、综合 二、JS基本表达式 1. 算术运算符 意义运算符加减-乘*除/取余% 加减乘除 加减的符号和数学一致&#xff0c;乘号是*号&#xff0c;除法是/号默认情况&#xff0c;乘除法的优先级高于加法和…

【软件工程】软件测试

软件测试的对象 软件程序文档 测试对象&#xff1a;各个阶段产生的源程序和文档。 软件测试的目的 基于不同的立场&#xff0c;对软件测试的目的存在着两种完全对立的观点。 &#xff08;1&#xff09;一种观点是通过测试暴露出软件中所包含的故障和缺陷(从用户的角度)&#xf…

ORB-SLAM2第一节---单目地图初始化

单目初始化 1.前提条件&#xff08;640*480&#xff09; 参与初始化的两帧各自的特征点数目都需要大于100.两帧特征点成功匹配的数目需要大于或等于100.两帧特征点三角化成功的三维点数目需要大于50. 2.针对条件三 流程如下 记录当前帧和参考帧&#xff08;第一帧&#xff…

计算机组成原理 汇编语言

..................................................

fastadmin采坑之页面调转

这里有个业务需求&#xff0c;就是一个表格页面添加一个报名按钮&#xff0c;这个报名按钮就对应另外一个表格页面的新增&#xff0c;那就不用单独写个报名页面了&#xff0c;直接添加一个报名按钮&#xff0c;然后按钮的url直接指向另外一个页面的新增页面&#xff0c;真的很方…

[vscode]vscode运行cmake时候exe不执行而且前面多一些字符

遇到一个奇怪问题,你单独打开cmd去执行vscode编译过程序没问题&#xff0c;但是你在vscode确不会执行&#xff0c;这是因为vscode没有读取到电脑环境变量导致加载DLL失败&#xff0c;但是在vscode终端不会给你提示少DLL&#xff0c;需要你自己把DLL复制到exe目录即可解决问题。…

CDH6.3应知应会

文章目录 1. CDH 简介1.1 CDH版本 2. CDH 集群的优势是什么&#xff1f;3. CDH 集群的部署方式有哪些&#xff1f;4. CDH 集群中如何进行故障排除和监控&#xff1f;5. 你有使用 CDH 部署集群的经验吗&#xff1f;6. CDH 集群如何实现高可用性&#xff1f;7. 在 CDH 集群中&…

Qt Bridge for Adobe Photoshop安装

*社区版没有这个文件夹无法安装&#xff0c;可以下载企业用的&#xff0c;然后给money&#xff0c;不多也就316刀&#xff0c;折合人民币2千多&#xff0c;或者试用10天。 ** B站找到的文章&#xff08;作者&#xff1a;原版英语 https://www.bilibili.com/read/cv17933098 出…

Redis进阶(4)——结合redis.conf配置文件深入理解 Redis两种数据持久化方案:RDB和AOF

目录 引出持久化方案RDBAOF Redis的持久化方案RDB如果采用docker stop关闭如果采用强制关闭 AOF参数设置混编方式的加载让aof进行重写 两种持久化方案的优缺点AOF优缺点RDB优势和劣势 总结 引出 1.Redis数据持久化的两种方式&#xff0c;RDB和AOF; 2.RDB采用二进制存储&#xf…

PHP8的字符串操作1-PHP8知识详解

字符串是php中最重要的数据之一&#xff0c;字符串的操作在PHP编程占有重要的地位。在使用PHP语言开发web项目的过程中&#xff0c;为了实现某些功能&#xff0c;经常需要对某些字符串进行特殊的处理&#xff0c;比如字符串的格式化、字符串的连接与分割、字符串的比较、查找等…

redis设置database 不生效剖析

设置database 不生效剖析 前言配置加载类问题commons-pool 对象池 主页传送门&#xff1a;&#x1f4c0; 传送 前言 事情是这样的 今天在拉取了同事的代码做redis缓存设置的时候&#xff0c;发现即使已经设置了database, 但是存数据的时候还是用的默认0数据库。这引起了我的好…

Java-类型和变量(基于C语言的补充)

一个简单的Java程序 args){ System.out.println("Hello,world"); } }通过上述代码&#xff0c;我们可以看到一个完整的Java程序的结构&#xff0c;Java程序的结构由如下三个部分组成&#xff1a; 1.源文件&#xff08;扩展名为*.java)&#xff1a;源文件带有类的定义…

通过金字塔模型拥抱企业数字化转型

企业要实施成功的数字化营销转型改革&#xff0c;需要践行如下金字塔模型&#xff0c;从上至下依次是认知&#xff0c;应用&#xff0c;流程和组织&#xff0c;我们一一来拆解。 01 认知层 处于最顶层的是认知层&#xff0c;数字化营销的认知没有建立&#xff0c;很难实施彻头…

用python来爬取某鱼的商品信息(2/2)

目录 上一篇文章 本章内容 设置浏览器为运行结束后不关闭&#xff08;可选&#xff09; 定位到搜索框的xpath地址 执行动作 获取cookie 保存为json文件 修改cookie的sameSite值并且导入cookie 导入cookie&#xff08;出错&#xff09; 导入cookie&#xff08;修改后&…