当前位置:首页 » 《随便一记》 » 正文

「JavaEE」线程

11 人参与  2024年04月18日 17:13  分类 : 《随便一记》  评论

点击全文阅读


?个人主页:Ice_Sugar_7
?所属专栏:JavaEE
?欢迎点赞收藏加关注哦!

线程

?线程?多线程?线程与进程的联系&区别?多线程编程?创建线程?Thread 其他重要属性与方法 ?操作系统内核

?线程

上篇文章中我们介绍了进程,但实际上在Java中是不太鼓励“多进程编程”的,大多数时候我们使用的是线程

进程可以很好地解决并发编程这样的问题,但是在一些特定的情况下,它的表现不尽人意。比如有些场景需要频繁创建和销毁进程,此时使用多进程编程的话,系统开销就会很大

开销是哪来的呢?一个进程刚启动时,需要把依赖的代码和数据从磁盘加载到内存中。而从系统分配一块内存并非一件易事,一般申请内存的时候需要先指定一个大小,然后系统内部把各种大小的空闲内存通过一定的数据结构组织起来,这个过程需要一定的时间开销

而线程就是解决上述问题的方案
线程也可以称为轻量级进程,它在进程的基础上做出改进

前面我们说一个进程是由 PCB 来描述的;其实 PCB 也可以用来描述一个线程

PCB 中有个属性,叫内存指针。多个线程 PCB 的内存指针指向的是同一个内存空间
这意味着在创建第一个线程的时候就需要从系统分配资源。后续的线程就不必再分配,直接共用前面那份资源就 ok 了
除了内存,文件描述符表也是多个线程共用一份的(共享经济属于是)

在这里插入图片描述
当然也不是随便两个线程都能共享资源,我们把能够共享资源的线程分成组,称为线程组
而一个进程可以有多个PCB,这就意味着这个进程包含了一个线程组(多个线程)


?多线程

多线程是指在同一个进程中同时运行多个线程,每个线程可以执行独立的任务并且能够同时运行,这样同时执行多个任务可以提高程序的性能和响应速度

但是线程也不是越多越好,当线程数量太多的时候,线程之间就会相互竞争 CPU 的资源(因为 CPU 调度执行线程的数量是有限的),导致不仅不会提高效率,还会增加调度的开销

而且多线程还有一个问题,就是线程之间可能会起冲突,这就会导致代码中出现一些逻辑上的错误(这是后面要讨论的线程安全问题);一个线程如果抛出异常,并且没有处理好,就可能导致整个进程终止


?线程与进程的联系&区别

进程是包含线程的每个线程是一个独立的执行流,可以执行一些代码,并且单独参与到 CPU 调度中每个进程有自己的资源,进程中的线程共享这一份资源(内存空间、文件描述符表等)

由2和3可以得出:进程是资源分配的基本单位;线程是调度执行的基本单位

进程与进程之间不会相互影响,但是线程会(线程安全问题)。如果同一个进程中的某个线程抛出异常,可能会影响到其他线程,甚至会导致整个进程中所有线程都异常终止线程不是越多越好,差不多就得了,如果线程太多了,调度开销可能非常明显

?多线程编程

在Java中,写代码的时候推荐使用多线程并发编程,系统提供了多线程编程的 api,而Java标准库把这些 api 封装好了,在代码中就可以直接使用,比如Thread类

打开 idea,我们先写一个 MyThread 类继承 Thread,并写一个 run 方法:
在这里插入图片描述

这个 run 方法就类似于 main 方法,是一个 Java 线程的入口方法。一个进程中至少有一个线程,这个进程的第一个线程,称为主线程,所以 main 方法也就是主线程的入口方法(因为一个进程肯定要有一个 main 方法)

然后还有一点,就是 run 是不需要我们手动调用的,它会在合适的时机(线程创建好之后)被 jvm 自动调用执行(这样的函数称为回调函数)
我们前面所学的优先级队列,往它插入个对象,需要先指定比较规则,这就要实现 Comparable 或者 Comparator 接口,分别重写 compareTo 和 compare 方法,这两个也属于回调函数

说回正题,现在要搞一个线程,就是要让这个线程执行一些代码。显然,标准库自带的 run 肯定是不知道我们的需求,这就需要我们进行拓展(Thread 类有很多属性、方法,大部分都可以复用,只用把需要拓展的进行拓展即可)

我们重写一下 run 方法,并创建一个线程:

public class MyThread extends Thread{    @Override    public void run() {        System.out.println("hello thread");    }    public static void main(String[] args) {        //根据刚才的类,创建出实例        Thread t = new MyThread();        //调用 Thread 类的 start 方法,才会真正调用系统的 api,在系统内核中创建线程(线程就会执行上面写好的 run 方法)        t.start();    }}

那么现在上面的代码就有两个线程:t 线程和 main 线程
每个线程都是一个独立的执行流,它们都能独立去 CPU 上调度执行
以上面代码为例,现在稍微修改一下,两个线程都加个死循环:

public class MyThread extends Thread{    @Override    public void run() {        while(true) {            System.out.println("hello thread");            try {                Thread.sleep(1000); //控制隔一秒才打印,降低循环速度,避免循环跑起来的时候跑太快,导致 CPU 占用率比较高            } catch (InterruptedException e) {                throw new RuntimeException(e);            }        }    }    public static void main(String[] args) throws InterruptedException {        Thread t = new MyThread();        t.start();        while(true) {            System.out.println("hello main");            sleep(1000);        }    }}

运行结果如下图

在这里插入图片描述

可以看到两个循环都在执行,因为这两个线程就是两个独立的执行流
具体的执行流程就是:在 main 方法中调用 start 创建线程之后“兵分两路”,一路沿着 main 方法继续执行,打印“hello main”,另一路进入到线程的 run 方法,打印“hello thread”

然后有个需要注意的点,当有多个线程的时候,这些线程执行的先后顺序是不确定的,这是因为操作系统内核中有一个“调度器”模块,这个模块的实现了一种类似“随机调度”的效果。所谓的随机调度,指的是:
①一个线程被调度到 CPU 上执行的时机是不确定的
②一个线程从 CPU 上下来,给其他线程让位的时机也是不确定的

这两点其实归因于线程执行采用抢占式执行的机制:操作系统根据优先级等参数来决定何时中断当前线程,并切换到其他线程
这个机制使得多线程程序可以更好地利用 CPU 资源,增加并发性和吞吐量,但是也带来了线程安全问题

还是以上面的代码为例,别看是先进入 main 方法就以为是先执行 main 线程,其实它和 thread 谁先谁后是不确定的


?创建线程

上面介绍了一种创建线程的方式,不过那不是主流的方式。我们通常使用 lambda 表达式创建一个线程

        Thread t1 = new Thread(()-> {            System.out.println("hello thread");            try {                sleep(1000);            } catch (InterruptedException e) {                throw new RuntimeException(e);            }        });        t.start();

这个写法相当于实现 Runnable 接口并重写 run 方法,lambda 代替了 Runnable 的位置


?Thread 其他重要属性与方法

方法

Thread(Runnable target) //使用 Runnable 对象创建线程对象Thread(String name) //创建线程对象并命名Thread(Runnable target,String name) //使用 Runnable 对象创建线程对象并命名

我们自己创建的线程默认是按照 Thread-0 1 2……命名的,给不同线程起不同名字对于线程的执行没有影响,主要是方便调试。此外,线程之间的名字是可以重复的,但名字别乱起,最好要有一定的描述性

属性

属性获取方法
ID(jvm自动分配的身份标识,会保证唯一性)getID()
名称getName()
状态(进程有就绪状态,阻塞状态等,线程也有状态)getState()
优先级getPriority()
是否为后台线程isDaemon()
是否存活isAlive()
是否被中断isInterrupted()

(为了让表格看上去不会冗杂,一些属性的说明放到这下面讲)
优先级:在 Java 中,由于系统是随机调度线程的,所以对线程设置优先级的效果不是很明显

后台线程:后台线程的运行不会阻止进程结束,与后台线程相对,还有前台线程,前台进程的运行,会阻止进程结束(注意这里的后台和我们平时手机的“杀后台”不是一回事)

我们来演示一下前台线程,只需把刚才代码中 main 线程的死循环去掉:

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

在这里插入图片描述

进程执行后,会一直打印,只有当我们停止进程后,出现红色方框中这句话,才表示进程结束
这是因为我们创建的线程默认是前台线程,即使 main 已经执行完了,只要前台线程没执行完,进程就不会结束

然后我们把 t 改为后台线程:

        t.setDaemon(true); //设为 true 就是改为后台,注意 setDaemon 一定要写在start前面        t.start();

在这里插入图片描述
可以看到什么都没打印,进程就结束了

isAlive:它表示内核中的线程(PCB)是否还存在。如果线程已经启动并且还没有终止,那就会返回 true;反之返回 false

Java 代码中定义的线程实例虽然表示一个线程,但是这个实例本身的生命周期和内核中 PCB 的生命周期是不完全一样的

Thread t = new Thread(()-> {...})

比如现在创建了 t 实例,由于线程还没有 start,所以此时 isAlive 的结果就是 false


?操作系统内核

我们在上文中多次提到“内核”这个概念

内核是操作系统中最核心部分的功能模块,它负责管理硬件,给软件提供稳定的运行环境
操作系统的内存空间分为两块:内核空间(内核态)和用户空间(用户态)

为什么要划分出这两个空间呢?主要是为了稳定,防止应用程序把硬件设备或软件资源搞坏了。系统封装了一些 api,这些 api 都是一些合法的操作,应用程序只能调用这些 api,这样就不至于对系统以及硬件设备产生太大危害

我们平时运行的普通应用程序,比如 idea、谷歌、微信……都是在用户态运行的。这些程序有时候需要针对一些系统提供的软硬件资源进行操作,这些操作都不是应用程序直接操作的,需要调用系统提供的 api,然后在内核中完成这些操作


点击全文阅读


本文链接:http://zhangshiyu.com/post/97274.html

<< 上一篇 下一篇 >>

  • 评论(0)
  • 赞助本站

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

关于我们 | 我要投稿 | 免责申明

Copyright © 2020-2022 ZhangShiYu.com Rights Reserved.豫ICP备2022013469号-1