10分钟巩固多线程基础

10分钟巩固多线程基础

前言

多线程是并发编程的基础,本篇文章就来聊聊多线程

我们先聊聊概念,比如进程与线程,串行、并行与并发

再去聊聊线程的状态、优先级、同步、通信、终止等知识

进程与线程

什么是进程?

操作系统将资源分配给进程,使用进程进行调度,但进程遇到阻塞任务时,为了提升CPU利用率,会进行切换进程

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

线程又被称为轻量级进程(LWP),线程是操作系统的基本调度单位,当线程被分到CPU给的时间片时就能够进行调度任务

当线程等待资源遇到阻塞时,为了提升CPU利用率会将线程进行挂起,等到后续资源准备好了又将线程恢复,分配到时间片后继续执行

为了安全起见,线程分为用户态和内核态,使用线程操作普通的任务时处于用户态就可以调度执行,要完成某些有关操作系统安全性相关的操作时,需要先切换到内核态再进行操作

线程的挂起、恢复就需要在用户态与内核态中进行切换,频繁的切换线程也会带来一定的开销

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

一个进程下有一个或多个线程,进程用于管理操作系统所分配的资源,线程用于进行调度,并且同一进程下所有线程能共享进程的资源,而线程中为了存储调度的任务运行情况,也会有自己私有的内存空间对其进行存储

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

一对一模型

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

10分钟巩固多线程基础_第1张图片

但如果线程使用不当,可能导致频繁切换内核态,带来大量开销

并且内核线程资源是有限的,因此一对一模型中线程资源有上限

多对一

在多对一模型中

10分钟巩固多线程基础_第2张图片

由于多个用户线程映射同一内核线程,相比于一对一模型能够使用的用户线程更多

但是当发生阻塞时要切换到内核态进行阻塞,该内核线程对应的所有用户线程都会被阻塞,其实现也会变复杂

多对多

在多对多模型中

10分钟巩固多线程基础_第3张图片

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

但实现变得更加复杂

串行、并行与并发

为什么要用多线程?

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

串行可以理解成排队执行,当线程分到CPU的资源时开始执行调度,线程可能进行IO任务的调度

此时会等待IO资源准备好才能进行调度,这段时间内CPU啥事也没干从而没有有效的利用CPU

10分钟巩固多线程基础_第4张图片

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

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

两个线程在一段时间内看上去像在同时执行,实际上它们是交替执行,某个时刻上只有一个线程在执行

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

10分钟巩固多线程基础_第5张图片

那什么又是并行呢?

上面说的串行、并发都在单线程下可以实现,但是并行的前提就是多核

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

10分钟巩固多线程基础_第6张图片

那是不是多线程一定效率最快呢?

经过上面的分析,我们知道:线程挂起和恢复,上下文的切换会经过用户态、内核态的转换,会有性能开销

当线程太多、运行时频繁进行上下文切换,那么带来的性能开销甚至可能超过并发提升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++方法),启动线程执行任务

10分钟巩固多线程基础_第7张图片

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

线程状态

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

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

在操作系统中将运行分为就绪、运行中状态,当线程创建好后等待CPU分配时间片的状态就是就绪状态,分配到时间片运行就是运行中状态

10分钟巩固多线程基础_第8张图片

新建:线程刚创建和还未获取到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)方法让线程变成守护线程

线程同步

当多线程需要使用共享资源时,由于共享资源数量有限,它们不能同时获取

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

如果多线程同时使用共享资源可能会造成逻辑错误

在Java中常用synchronized关键字使用加锁的方式来保证同步(只有一个线程能够访问共享资源)

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

其中object就是加锁的共享资源

对于更多synchronized的描述可以查看这篇文章:15000字、6个代码案例、5个原理图让你彻底搞懂Synchronized

线程通信

等待wait / 通知 notify

使用synchronized时要去获取锁,获取锁后线程才能执行调度,当调度中不满足执行条件时,需要让出锁让其他线程执行

比如生产者/消费者模型,当生产者获取到锁要进行生产资源时,发现资源已经满了,它应该让出锁,等到消费者消费完时将它唤醒

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

使用wait、notify的前提是获取到锁

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

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

notify 唤醒等待当前锁的某个线程

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

其具体实现可以查看15000字、6个代码案例、5个原理图让你彻底搞懂Synchronized 的锁升级中重量级锁那一小节

生产者消费者模型

生产者、消费者模型中常用等待与通知进行线程通信

生产者检查到生产的资源已满时就进入等待,等待消费者消费完来唤醒,生产完再去唤醒消费者

消费者检查到没有资源时就进入等待,等待生产者生产完来唤醒,消费完再去唤醒生产者

生产

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的区别是sleep睡眠时不会释放锁、并且使用sleep时不需要先获取锁

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

最后(不要白嫖,一键三连求求拉~)

本篇文章被收入专栏 由点到线,由线到面,深入浅出构建Java并发编程知识体系,感兴趣的同学可以持续关注喔

本篇文章笔记以及案例被收入 gitee-StudyJava、 github-StudyJava 感兴趣的同学可以stat下持续关注喔~

案例地址:

Gitee-JavaConcurrentProgramming/src/main/java/A_Thread

Github-JavaConcurrentProgramming/src/main/java/A_Thread

有什么问题可以在评论区交流,如果觉得菜菜写的不错,可以点赞、关注、收藏支持一下~

关注菜菜,分享更多干货,公众号:菜菜的后端私房菜

本文由博客一文多发平台 OpenWrite 发布!

你可能感兴趣的:(Java,后端,面试,并发)