当前位置: 首页 > 图灵资讯 > 技术篇> 10分钟巩固多线程基础

10分钟巩固多线程基础

来源:图灵教育
时间:2023-09-28 10:34:54

巩固多线程基础10分钟

多线程是并发编程的基础。本文将讨论多线程

先说概念,比如过程和线程,串行,并行并发

然后谈谈线程状态、优先级、同步、通信、终止等知识

进程与线程

过程是什么?

操作系统将资源分配给过程并调度使用过程,但当过程遇到堵塞任务时,为了提高CPU的利用率,将切换过程

由于切换过程的成本太高,线程诞生了

又称线程,又称轻量级进程(LWP),线程是操作系统的基本调度单元。当线程被分配到CPU给出的时间片时,可以进行调度任务

当线程等待资源被堵塞时,线程将被挂起,以提高CPU的利用率。当后续资源准备好后,线程将被恢复,并在分配到时间片后继续执行

为了安全起见,线程分为用户态和内核态。当使用线程操作普通任务时,可以在用户态中进行调度和执行。为了完成一些与操作系统安全相关的操作,在操作前需要切换到内核态

线程的悬挂和恢复需要在用户状态和核心状态之间切换,频繁切换线程也会带来一定的成本

当我们点击打开浏览器时,浏览器程序可能会启动一个或多个过程

一个过程中有一个或多个线程,用于管理操作系统分配的资源,用于调度,所有线程都可以在同一过程中共享过程的资源。为了存储调度任务的运行,线程中也会有自己的私人内存空间存储

实现用户态和内核态的线程模型有三种:用户线程和内核线程一对一、多对一、多对多

一对一模型

一对一模型实现简单,用户线程映射内核线程,Java中使用的模型是一对一

image.png

但是,如果线程使用不当,可能会导致内核状态频繁切换,造成大量费用

而且核心线程资源有限,所以一对一模型中线程资源有上限

多对一

在多对一模型中

image.png

与一对一模型相比,多个用户线程映射相同的内核线程可以使用更多的用户线程

但是,当发生阻塞时,应切换到内核状态进行阻塞,内核线程对应的所有用户线程都会被阻塞,实际上会变得复杂

多对多

在多对多模型中

image.png

不仅解决了一对一模型线程的上限问题,还解决了多对一模型中所有用户线程的核线程阻塞问题

但实现变得更加复杂

串行、并行、并发

为什么要使用多线程?

随着硬件的发展,大多数机器不再是单个核心CPU的机器,大量机器使用多核超线程技术

串行可以理解为排队执行。当线程分配到CPU资源时,调度开始,线程可以调度IO任务

此时,我们将等待IO资源的准备进行调度。在此期间,CPU没有做任何事情,因此没有有效地使用CPU

image.png

为了提高CPU的利用率,A线程等待IO资源时,可以先挂起A线程,将CPU资源分配给B线程

等待A线程的IO资源准备好后,将B线程挂起恢复A线程继续执行

在一段时间内,两个线程似乎同时执行。事实上,它们是交替执行的。在某个时刻,只执行一个线程

并发提高CPU的利用率,但也会带来线程上下文切换的费用

image.png

什么是平行的?

上述串行和并发可以在单线程下实现,但并行的前提是多核

并行是指在某一时刻同时执行多个线程,因此需要多核

image.png

多线程效率最快吗?

通过以上分析,我们知道上下文之间的切换将通过用户状态和核心状态的转换,以及性能费用

当线程过多,运行时上下文切换频繁时,性能成本甚至可能超过并发提高CPU利用率带来的收入

创建线程

JDK中为我们提供的线程类是java.lang.Thread,它实现了Runnable接口,并通过结构接受Runnable

  public class Thread implements Runnable {      private Runnable target;  }

Runnable接口是函数接口,其中只有Run方法。Run方法中的实现表示线程启动后要执行的任务

  public interface Runnable {      public abstract void run();  }

Java创建线程的方法只有一种:创建Thread对象,然后调用start方法启动线程

我们可以通过构造器设置线程名称,并设置要实现的任务(打印线程名称 + hello)

      public void test(){          Thread a = new Thread(() -> {              //线程A hello              System.out.println(Thread.currentThread().getName() + " hello");          }, "线程A");          //main hello          a.run();          a.start();      }

在主线程中调用run方法时,实际上是主线程执行runnable接口的任务

正如我们前面所说,Java中的线程模型是一对一模型,一个线程对应一个核心线程

只有在调用start方法时,才能调用本地方法(C++方法),启动线程执行任务

image.png

如果调用两次start,就会抛出IllegalThreadStateException异常

线程状态

Java中Thread的状态分为新建、运行、堵塞、等待、超时等待、终止

 public enum State {     //新建     NEW,     //运行     RUNNABLE,     //阻塞     BLOCKED,     //等待     WAITING,     //超时等待     TIMED_WAITING,     //终止     TERMINATED; }

在操作系统中,运行分为就绪状态和运行状态。线程创建后,等待CPU分配时间片的状态为就绪状态,分配时间片的运行为运行状态

image.png

新建:刚刚创建线程并未获得CPU分配的时间片

操作:线程获取CPU分配的时间片,并进行任务调度

阻塞:在线程调度过程中,因无法获得共享资源而进入阻塞状态(如被synchronized阻塞)

等待:在线程调度过程中执行wait、join等方法进入等待状态,等待其他线程唤醒

超时等待:Sleep(1)在线程调度过程中执行、wait(1)、join(1)设置等待时间的方法时,进入超时等待状态

终止:线程执行调度任务或异常执行进入终止状态

优先级

线程调度任务的前提是获得CPU资金源(CPU分配的时间片)

在Java中提供setPriority获取CPU资源的优先级为1~10,默认为5

  //最小  public final static int MIN_PRIORITY = 1;   //默认  public final static int NORM_PRIORITY = 5;    //最大  public final static int MAX_PRIORITY = 10;

但设置的优先级只在Java层面,映射到操作系统的优先级不同

例如,在Java中设置优先级5或6,可能映射到操作系统的优先级处于同一水平

守护线程

什么是守护线程?

守护线程可以理解为后台线程。当程序中所有非守护线程完成任务时,程序将结束

简而言之,无论守护线程是否完成,只要非守护线程完成,程序就会结束

因此,防护线程可用于检查资源的背景操作

使用setDaemon(true)使线程成为守护线程的方法

线程同步

多线程需要共享资源时,由于共享资源数量有限,无法同时获得

每时每刻只能获得一个线程,其他未获得共享资源的线程需要被阻塞

同时使用多线程共享资源可能会导致逻辑错误

synchronized关键字常用于Java中,以确保同步(只有一个线程可以访问共享资源)

         synchronized (object){             System.out.println(object);         }

object是加锁共享资源

对于更多synchronized的描述,请参阅本文:15000字,6个代码案例,5个原理图,让您彻底了解synchronized

等待waittttwaittt等待线程通信 / 通知 notify

使用synchronized时,需要获得锁,只有获得锁后线程才能执行调度。当调度不符合执行条件时,需要让出锁执行其他线程

例如,在生产者/消费者模型中,当生产者获得生产资源锁时,他们发现资源已经满了。他们应该把锁放出去,等到消费者消费完成后再把它叫醒

这种等待/通知模式是实现线程通信的一种方式,Java提供waitt、notify方法实现等待/通知模式

使用wait、notify的前提是获得锁

wait让当前线程释放锁进入等待模式,等待其他线程用notify唤醒

wait(1)也可以携带等待时间ms,到达时自动唤醒,开始竞争锁

notify 唤醒等待当前锁定的线程

notifyAll 唤醒所有等待当前锁定的线程

它的具体实现可以查看15000字,6个代码案例,5个原理图,让你彻底了解Synchronizeded 锁升级中重量级锁的一小部分

生产消费者模型

线程通信通常用于生产者和消费者模型中的等待和通知

当生产者检查到生产资源已满时,他们会进入等待,等待消费者消费后醒来,然后在生产后唤醒消费者

当消费者发现没有资源时,他们会进入等待,等待生产者在生产后醒来,然后在消费后唤醒生产者

生产

public void produce(int num) throws InterruptedException {    synchronized (LOCK) {        //如果生产 资源 已满 等待消费者消费        while (queue.size() == 10) {            System.out.println("队列满了,生产者等待");            LOCK.wait();        }                Message message = new Message(num);        System.out.println(Thread.currentThread().getName() + "生产了" + message);        queue.add(message);        //唤醒 所有线程        LOCK.notifyAll();    }}

消费

public void consume() throws InterruptedException {    synchronized (LOCK) {        //如果队列是空的 等待生产者生产        while (queue.isEmpty()) {            System.out.println("队列空了,消费者等待");            LOCK.wait();        }        Message message = queue.poll();        System.out.println(Thread.currentThread().getName() + "消费了" + message);        //唤醒 所有线程        LOCK.notifyAll();    }}
sleep 睡眠

sleep 该方法用于让线程睡一段时间ms

与wait不同的是,slep睡觉时不会释放锁,使用slep时也不需要先获得锁

join 等待

join方法用于等待线程执行

例如,调用主线程thread.join()要等到thread线程完成,join方法才会返回

同时,join还支持设置等待时间ms,自动返回超时

终止线程

终止线程一般采用安全终止方式:中断线程

线程运行时会保存一个标记位,默认为false,表示没有其他线程中断

当你想停止一个线程时,你可以中断它,比如线程A.interrupt(): 线程A执行中断操作 ,此时,线程A的中断标志为true

在线程调度任务期间,当轮询中断标志为true时,可以停止使用线程A.isInterrupted(): 查看线程A的中断标记

当线程进入等待状态时,其他线程中断会出现中断异常,标志位清晰,中断异常抛出;清理资源或资源的释放可以在catch块中捕获

当按中断标识循环执行时,也可以中断自己,停止执行

         Thread thread = new Thread(() -> {             ///中断标识为false循环执行任务             while (!Thread.currentThread().isInterrupted()) {                 try {                     //执行任务                     System.out.println(" ");                                          //假设等待资源                     TimeUnit.SECONDS.sleep(1);                                          ///获取资源后执行                                      } catch (InterruptedException e) {                     ///等待时中断线程会在抛出异常时恢复标志位置                     //捕捉异常时,重新中断标志(自己中断)                     Thread.currentThread().interrupt();                                          /////结束前处理其他资源                 }             }             // true             System.out.println(" 中断标识位:" + Thread.currentThread().isInterrupted());         });

还有一种检测中断的方法Thread.interrupted(): 检查当前线程的中断标记,清除当前线程的中断标记,将中断标记恢复到false