「JavaEE」线程

2024-04-23 1511阅读

温馨提示:这篇文章已超过405天没有更新,请注意相关的内容是否还可用!

🎇个人主页:Ice_Sugar_7

🎇所属专栏:JavaEE

🎇欢迎点赞收藏加关注哦!

线程

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

      🍉线程

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

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

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

      而线程就是解决上述问题的方案

      线程也可以称为轻量级进程,它在进程的基础上做出改进

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

      PCB 中有个属性,叫内存指针。多个线程 PCB 的内存指针指向的是同一个内存空间

      这意味着在创建第一个线程的时候就需要从系统分配资源。后续的线程就不必再分配,直接共用前面那份资源就 ok 了

      除了内存,文件描述符表也是多个线程共用一份的(共享经济属于是)

      「JavaEE」线程

      当然也不是随便两个线程都能共享资源,我们把能够共享资源的线程分成组,称为线程组

      而一个进程可以有多个PCB,这就意味着这个进程包含了一个线程组(多个线程)


      🍌多线程

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

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

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


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

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

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

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

      🍌多线程编程

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

      打开 idea,我们先写一个 MyThread 类继承 Thread,并写一个 run 方法:

      「JavaEE」线程

      这个 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);
              }
          }
      }
      

      运行结果如下图

      「JavaEE」线程

      可以看到两个循环都在执行,因为这两个线程就是两个独立的执行流

      具体的执行流程就是:在 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();
          }
      

      「JavaEE」线程

      进程执行后,会一直打印,只有当我们停止进程后,出现红色方框中这句话,才表示进程结束

      这是因为我们创建的线程默认是前台线程,即使 main 已经执行完了,只要前台线程没执行完,进程就不会结束

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

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

      「JavaEE」线程

      可以看到什么都没打印,进程就结束了

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

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

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

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


      🍉操作系统内核

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

      内核是操作系统中最核心部分的功能模块,它负责管理硬件,给软件提供稳定的运行环境

      操作系统的内存空间分为两块:内核空间(内核态)和用户空间(用户态)

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

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

VPS购买请点击我

免责声明:我们致力于保护作者版权,注重分享,被刊用文章因无法核实真实出处,未能及时与作者取得联系,或有版权异议的,请联系管理员,我们会立即处理! 部分文章是来自自研大数据AI进行生成,内容摘自(百度百科,百度知道,头条百科,中国民法典,刑法,牛津词典,新华词典,汉语词典,国家院校,科普平台)等数据,内容仅供学习参考,不准确地方联系删除处理! 图片声明:本站部分配图来自人工智能系统AI生成,觅知网授权图片,PxHere摄影无版权图库和百度,360,搜狗等多加搜索引擎自动关键词搜索配图,如有侵权的图片,请第一时间联系我们,邮箱:ciyunidc@ciyunshuju.com。本站只作为美观性配图使用,无任何非法侵犯第三方意图,一切解释权归图片著作权方,本站不承担任何责任。如有恶意碰瓷者,必当奉陪到底严惩不贷!

目录[+]