关注「Java视界」公众号,获取更多技术干货

【一】多线程 —— 基础概念

一、进程、线程与协程

进程

  • 程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至 CPU,数据加载至内存。在指令运行过程中还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理 IO 的。
  • 当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程。
  • 进程就可以视为程序的一个实例。大部分程序可以同时运行多个实例进程(例如记事本、画图、浏览器 等),也有的程序只能启动一个实例进程(例如网易云音乐、360 安全卫士等)

线程

  • 一个进程之内可以分为一到多个线程。
  • 一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给 CPU 执行 。
  • Java 中, 线程作为最小调度单位进程作为资源分配的最小单位。 在 windows 中进程是不活动的,只是作为线程的容器

二者对比

  • 进程基本上相互独立的,而线程存在于进程内,是进程的一个子集 进程拥有共享的资源,如内存空间等,供其内部的线程共享。进程间通信较为复杂,同一台计算机的进程通信称为IPC(Inter-process communication)。不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,例如 HTTP
  • 线程通信相对简单,因为它们共享进程内的内存,一个例子是多个线程可以访问同一个共享变量。线程更轻量,线程上下文切换成本一般上要比进程上下文切换低。

协程
为什么要引入协程?

在Java中,我们一般通过回调来处理异步任务,但是当异步任务嵌套时,往往程序就会变得很复杂与难维护
举个例子,当我们需要完成这样一个需求:查询用户信息 --> 查找该用户的好友列表 --> 查找该好友的动态
看一下Java回调的代码:

getUserInfo(new CallBack() {
    @Override
    public void onSuccess(String user) {
        if (user != null) {
            System.out.println(user);
            getFriendList(user, new CallBack() {
                @Override
                public void onSuccess(String friendList) {
                    if (friendList != null) {
                        System.out.println(friendList);
                        getFeedList(friendList, new CallBack() {
                            @Override
                            public void onSuccess(String feed) {
                                if (feed != null) {
                                    System.out.println(feed);
                                }
                            }
                        });
                    }
                }
            });
        }
    }
});

如果用kotlin协程实现同样的需求呢?

val user = getUserInfo()
val friendList = getFriendList(user)
val feedList = getFeedList(friendList)

相比之下,可以说是非常简洁了
Kotlin 协程的核心竞争力在于:它能简化异步并发任务,以同步方式写异步代码
这也是为什么要引入协程的原因了:简化异步并发任务

什么是协程?
一种非抢占式(协作式)的任务调度模式,程序可以主动挂起或者恢复执行。

协程与线程的区别是什么?
协程基于线程,但相对于线程轻量很多,可理解为在用户层模拟线程操作;
每创建一个协程,都有一个内核态线程动态绑定,用户态下实现调度、切换,真正执行任务的还是内核线程。
线程的上下文切换都需要内核参与,而协程的上下文切换,完全由用户去控制,避免了大量的中断参与,减少了线程上下文切换与调度消耗的资源。
线程是操作系统层面的概念,协程是语言层面的概念。

线程与协程最大的区别在于:线程是被动挂起恢复,协程是主动挂起恢复。

二、上下文切换

进程
内核为每一个进程维持一个上下文。上下文就是内核重新启动一个被抢占的进程所需的状态。包括以下内容:

  • 通用目的寄存器
  • 浮点寄存器
  • 程序计数器
  • 用户栈
  • 状态寄存器
  • 内核栈
  • 各种内核数据结构:比如描绘地址空间的页表,包含有关当前进程信息的进程表,以及包含进程已打开文件的信息的文件表

线程
因为以下一些原因导致 cpu 不再执行当前的线程,转而执行另一个线程的代码。

被动原因:
①线程的 cpu 时间片用完(每个线程轮流执行,看前面并行的概念)
②垃圾回收(暂停所有用户线程)
③有更高优先级的线程需要运行

主动原因:
线程自己调用了 sleep、yield、wait、join、park、synchronized、lock 等方法

当 Context Switch 发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,Java 中对应的概念 就是程序计数器(Program Counter Register),它的作用是记住下一条JVM指令的执行地址,是线程私有的。

频繁Context Switch会影响性能,所以并不是线程数越多越好,线程多会引起CPU频繁上下文切换,要结合cpu核数。

进程切换和线程切换的主要区别
进程切换涉及虚拟地址空间的切换而线程不会。因为每个进程都有自己的虚拟地址空间,而线程是共享所在进程的虚拟地址空间的,因此同一个进程中的线程进行线程切换时不涉及虚拟地址空间的转换。

三、并发与并行

在这里插入图片描述
多核 cpu下,每个 核(core) 都可以调度运行线程,这时候线程可以是并行的。
在这里插入图片描述

  • 并发是一个CPU在不同的时间去不同线程中执行指令。
  • 并行是多个CPU同时处理不同的线程。

引用 Rob Pike 的一段描述:
并发(concurrent)是同一时间应对(dealing with)多件事情的能力
并行(parallel)是同一时间动手做(doing)多件事情的能力

  • 1) 单核 cpu 下,多线程不能实际提高程序运行效率,只是为了能够在不同的任务之间切换,不同线程轮流使用 cpu ,不至于一个线程总占用 cpu,别的线程没法干活
  • 2)多核 cpu 可以并行跑多个线程,但能否提高程序运行效率还是要分情况的。有些任务,经过精心设计,将任务拆分,并行执行,当然可以提高程序的运行效率。但不是所有计算任 务都能拆分。也不是所有任务都需要拆分,任务的目的如果不同,谈拆分和效率没啥意义
  • 3)IO 操作不占用 cpu,只是我们一般拷贝文件使用的是【阻塞 IO】,这时相当于线程虽然不用 cpu,但需要一直等待 IO 结束,没能充分利用线程。所以才有后面的【非阻塞 IO】和【异步 IO】优化。

四、同步 & 异步

从方法调用的角度来讲,如果:

  • 需要等待结果返回,才能继续运行就是同步
  • 不需要等待结果返回,就能继续运行就是异步

注意:同步在多线程中还有另外一层意思,是让多个线程步调一致

五、临界区

  • 要是单线程时不会出现这种问题;
  • 多个线程访问的不是共享变量也不会有问题;
  • 多个线程访问的是共享变量,但只有读的操作也不会有问题;
  • 多个线程访问的是共享变量,有写的操作,但是不发生指令交错也不会有问题;

一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区

例如:

    static int counter = 0;
   
   	void add()
    // 临界区
	{
        counter++;
    }
    
    void minus()
	// 临界区
    {
        counter--;
    }

六、竞态条件 Race Condition

多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件

七、线程状态

1. 五种状态
这是从 操作系统 层面来描述的。
在这里插入图片描述

  • 新建:创建了线程对象,还未与操作系统线程关联
  • 可运行:就绪状态,就是调用了start()方法
  • 运行:指获取了 CPU 时间片运行中的状态。当 CPU 时间片用完,会从【运行状态】转换至【可运行状态】,会导致线程的上下文切换
  • 阻塞:处于运行状态中的线程由于某种原因,暂时放弃对 CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才 有机会再次被 CPU 调用以进入到运行状态。
  • 死亡:表示线程已经执行完毕,run()方法执行完毕,生命周期已经结束,不会再转换为其它状态

2. 六种状态
这是从 Java API 层面来描述的根据 Thread.State 枚举,分为六种状态。
在这里插入图片描述

  • NEW 线程刚被创建,但是还没有调用 start() 方法
  • RUNNABLE 当调用了 start() 方法之后,注意,Java API 层面的 RUNNABLE 状态涵盖了 操作系统 层面的【可运行状态】、【运行状态】和【阻塞状态】(由于 BIO 导致的线程阻塞,在 Java 里无法区分,仍然认为是可运行)
  • BLOCKED ,WAITING,TIMED_WAITING 都是 Java API 层面对【阻塞状态】的细分
  • TERMINATED 当线程代码运行结束

NEW --> RUNNABLE

  • 当调用 t.start() 方法时,由 NEW --> RUNNABLE

RUNNABLE <–> WAITING

  • t 线程用 synchronized(obj) 获取了对象锁后 调用 obj.wait() 方法时,t 线程从 RUNNABLE -->WAITING
  • 调用 obj.notify() , obj.notifyAll() , t.interrupt() 时
    竞争锁成功,t 线程从 WAITING --> RUNNABLE
    竞争锁失败,t 线程从 WAITING --> BLOCKED

RUNNABLE <–> TIMED_WAITING

  • 调用 obj.wait(long n) 方法时,t 线程从 RUNNABLE --> TIMED_WAITING
  • 当前线程调用 t.join(long n) 方法时,当前线程从 RUNNABLE --> TIMED_WAITING
  • 当前线程等待时间超过了 n 毫秒,或t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程TIMED_WAITING --> RUNNABLE

RUNNABLE <–> BLOCKED

  • t 线程用 synchronized(obj) 获取了对象锁时如果竞争失败,从 RUNNABLE --> BLOCKED
  • 持 obj 锁线程的同步代码块执行完毕,会唤醒该对象上所有 BLOCKED 的线程重新竞争,如果其中 t 线程竞争成功,从 BLOCKED --> RUNNABLE ,其它失败的线程仍然 BLOCKED

RUNNABLE <–> TERMINATED

  • 当前线程所有代码运行完毕,进入 TERMINATED
posted @ 2022-06-25 14:01  沙滩de流沙  阅读(30)  评论(0编辑  收藏  举报

关注「Java视界」公众号,获取更多技术干货