【修炼内功】[JVM] 细说线程

本文已收录 【修炼内功】跃迁之路

细说JVM线程状态.png

自从踏入程序猿这条不归路,便摆脱不了(进程)线程这只粘人的小妖精,尤为在硬件资源“过剩”的今天java

不论你在使用c、C++、.Net,仍是Java、Python、Golang,都免不了要踏过这一关,即便使用以“单线程”著称的Node.js,也要借助pm2相似的进程管理工具fork一批进程,来榨干机器资源linux

早些年使用c编写多线程时,须要使用宏定义来兼容多平台下不一样库的函数,而Java从一开始便宣称的"Write Once, Run Anywhere"从虚拟机层面帮咱们屏蔽了众多平台差别,那,Java线程与OS线程间有什么关系?算法

系统架构

细说JVM线程状态001.png

以*nix类系统为例,其系统体系架构主要分为用户态(user context)内核态(kernel context)segmentfault

内核,本质上讲是一种较为底层的控制计算机硬件资源的软件windows

用户态,即上层应用程序的活动空间,应用程序的执行依托于内核提供的资源,为了使上层资源访问内核资源,内核提供系统调用接口以供上层应用访问多线程

系统调用,能够看做是操做系统的最小功能单元,一种不能再简化的操做,而函数库则是对一组系统调用的封装,以下降应用程序调用内核的复杂度架构

用户态与内核态切换

在*nix类系统中,为了有效减小内核资源的访问及冲突,对不一样的操做赋予了不一样的执行等级,越是与系统相关的关键操做,越是须要高特权来执行函数

linux操做系统中主要采用了0和3两个特权等级,分别对应于内核态及用户态,运行于用户态的进程能够执行的操做及访问的资源会受到很大的限制,而运行在内核态的进程则能够执行任何操做,而且在资源的访问上也不会受到任何限制工具

通常应用程序一开始运行时都会处于用户态,当一些操做须要在内核权限下才能执行时,则会涉及一次从用户态到内核态的切换过程,当该操做执行完毕后,又会涉及一次从内核态到用户态的切换过程性能

细说JVM线程状态002.png

线程模型

回过头来,从系统层面聊一聊线程的实现模型

用户线程 v.s. 内核线程

简单来说

  • 用户线程

    由应用程序建立、调度、撤销,不须要内核的支持(内核不感知)

    • 因为不须要内核的支持,便不涉及用户态/内核态的切换,消耗的资源较少,速度也较快
    • 因为须要应用程序控制线程的轮换调度,当有一个用户线程被阻塞时,整个所属进程便会被阻塞,同时在多核处理器下只能在一个核内分时复用,不能充分利用多核优点
  • 内核线程

    由内核建立、调用、撤销,并由内核维护线程的上下文信息及线程切换

    • 因为内核线程由内核进行维护,当一个内核线程被阻塞时,不会影响其余线程的正常运行,而且多核处理器下,一个进程内的多个线程能够充分利用多核的优点同时执行
    • 因为须要内核进行维护,在线程建立、切换过程当中便会涉及用户态/内核态的切换,增长系统消耗

轻量级进程 LWP

在linux操做系统中,每每都是经过fork函数建立一个子进程来表明内核中的线程,在fork完一个子进程后,还须要将父进程中大部分的上下文信息复制到子进程中,消耗大量cpu时间用来初始化内存空间,产生大量冗余数据

为了不上述状况,轻量级进程(Light Weight Process, LWP)便出现了,其使用clone系统调用建立子进程,过程当中只将部分父进程数据进行复制,没有被复制的资源能够经过指针进行数据共享,这样一来LWP的运行单元更小、运行速度更快

LWP与内核线程一一映射,每一个LWP都由一个内核线程支持

1:1 线程模型

1:1 模型,即每个用户线程都对应一个内核线程,每一个线程的建立、调度、销毁都须要内核的支持,每次线程的建立、切换都会设计用户状态/内核状态的切换,性能开销比较大,而且单个进程可以建立的LWP的数量是有限的,但可以充分里用多核的优点

细说线程状态.003.jpeg

N:1 线程模型

N:1模型,即全部的用户线程都会对应到一个内核线程中,该模型能够在用户空间完成线程的建立、调度、销毁,不须要内核的支持,一样也就不涉及用户状态/内核状态的切换,线程的操做较快且消耗较低,而且线程数量不受操做系统的限制,但不能发挥多核的优点,只能在一个核中分时复用,而且因为内核不能感知用户态的线程,在某一线程被阻塞时,会致使整个所属进程阻塞

细说线程状态.004.jpeg

N:M 线程模型

N:M 模型是基于以上两种模型的一种混合实现,多个用户线程对应于多个内核线程,即解决了1:1模型中性能开销及线程数量的问题,也解决了N:1模型中阻塞问题,同时也能充分利用CPU的多核优点,这也是大部分协程实现的基础

细说线程状态.005.jpeg

Java在1.2以前基于用户线程实现(N:1线程模型),在1.2以后windows及linux平台下采用1:1线程模型,在solaris平台使用1:1或N:M线程模型实现(可配置)

线程状态

如下以linux平台为例

linux平台下,JVM采用1:1的线程模型,那Java中的线程状态与OS的线程状态是否也是一一对应的?

系统线程状态&生命周期

细说线程状态.006.jpeg

linux系统的线程状态及生命周期如上图,每种状态的详细解释再也不一一赘述,这里简单介绍下RUNNABLERUNNING

  • RUNNABLE

    线程处于可运行的状态,但尚未被系统调度器选中,即尚未分配到CPU时间片

  • RUNNING

    线程处于运行状态,即线程分配到了时间片,正在执行机器指令

Java线程状态&生命周期

Java中的线程状态并无使用系统线程状态一一对应的方式,而是提供了与之不一样的6种状态

如下,linux系统线程状态会使用 斜体 加以区分

细说线程状态.007.jpeg

linux系统中的RUNNABLERUNNING被Java合并成了RUNNABLE一种状态,而linux系统中的BLOCKED被Java细化成了WAITINGTIMED_WAITINGBLOCKED三种状态

Java中的线程状态与系统中的线程状态大致类似,但又略有不一样,最明显的一点是,若是因为I/O阻塞会使Java线程进入BLOCKED状态么?NO!I/O阻塞在系统层面会使线程进入BLOCKED状态,但在Java里线程状态依然是RUNNABLE

系统中的RUNNABLE表示线程正在等待CPU资源,在在Java中被认为一样是在运行中,只是在排队等待而已,故Java中将系统的RUNNABLERUNNING合并成了RUNNABLE一种状态

而对于系统中I/O阻塞引发的BLOCKED状态,在Java中被认为一样是在等待一种资源,故也认为是RUNNABLE的一种状况

Java线程的状态在Thread.State枚举中能够查看,其每种状态的释义写的很是清楚,这里再也不一一解释

  • NEW

    Thread state for a thread which has not yet started.
  • RUNNABLE

    Thread state for a runnable thread. A thread in the runnable state is executing in the Java virtual machine but it may be waiting for other resources from the operating system such as processor.
  • BLOCKED

    Thread state for a thread blocked waiting for a monitor lock. A thread in the blocked state is waiting for a monitor lock to enter a synchronized block/method or reenter a synchronized block/method after calling Object.wait.
  • WAITING

    Thread state for a waiting thread. A thread is in the waiting state due to calling one of the following methods:

    • Object.wait with no timeout
    • Thread.join with no timeout
    • LockSupport.park

    A thread in the waiting state is waiting for another thread to perform a particular action. For example, a thread that has called Object.wait() on an object is waiting for another thread to call Object.notify() or Object.notifyAll() on that object. A thread that has called Thread.join() is waiting for a specified thread to terminate.

  • TIMED_WAITING

    Thread state for a waiting thread with a specified waiting time. A thread is in the timed waiting state due to calling one of the following methods with a specified positive waiting time:

    • Thread.sleep
    • Object.wait with timeout
    • Thread.join with timeout
    • LockSupport.parkNanos
    • LockSupport.parkUntil
  • TERMINATED

    Thread state for a terminated thread. The thread has completed execution.

上下文切换与调优

上下文切换涉及到进程间上下文切换与线程间上下文切换

用户态与内核态的每一次切换都会致使进程间上限文的切换,好比java中在使用重量级锁的时候会依赖系统底层的mutex lock,而该系统操做会致使用户态/内核态的切换,进而引发进程间的上下文切换

这里重点讨论下线程间的上下文切换

什么状况会触发线程间上下文切换

一个线程由RUNNING转为BLOCKED时(线程暂停),系统会保存线程的上下文信息

当该线程由BLOCKED转为RUNNABLE时(线程唤醒),系统会获取上次的上下文信息以保证线程可以继续执行

以上的一个过程线程上下文的一次切换过程

一样,一个线程由RUNNING转为RUNNABLE,再由RUNNABLE转为RUNNING时也会发生线程间的上下文切换

即,多线程的上下文切换实际上就是由多线程两个运行状态的互相切换致使的

那,什么状况下会触发 RUNNINGBLOCKEDRUNNABLE (对应Java中 RUNNABLEBLOCKED/WAITING/TIMED_WAITINGRUNNABLE) 的状态转变呢?

一种为程序自己触发,一种为操做系统或虚拟机触发

程序自己触发很容易理解,全部会致使 RUNNABLEBLOCKED/WAITING/TIMED_WAITING 的逻辑均会触发线程间上下文切换,如synchronizedwaitjoinparksleep

操做系统触发,最多见的好比线程时间片的分配

虚拟机触发,最多见的在于进行垃圾回收时的 'stop the world'

如何优化

既然全部会致使 RUNNABLEBLOCKED/WAITING/TIMED_WAITING 的逻辑均会触发线程间上下文切换,那便从诱因入手

锁竞争

锁其实并非性能开销的根源,竞争锁才是

  1. 减小锁的持有时间

    锁的持有时间越长,就意味着可能有越多的线程在等待锁的释放,若是是同步锁,除了会形成线程间上下文切换外,还会有进程间的上下文切换 (mutex lock)

    优化方法有不少,好比将synchronized关键字从方法修饰移到方法体内,将synchronized修饰的代码块中无关的逻辑移到synchronized代码块外,等等

  2. 下降锁的粒度

    • 锁分离

      对于读操做大于写操做的逻辑,能够将传统的同步锁拆分为读写锁,即读锁与写锁,在多线程中,只有读写与写写是互斥的,避免读读状况下锁的竞争

    • 锁分段

      对于大集合或者大对象的锁操做,能够考虑将锁进一步分离,将大集合或者大对象分隔成多个段,对每个段分别上锁,以免对不一样段进行操做时锁的竞争,如ConcurrentHashMap中对锁的实现

  3. 非阻塞乐观锁代替竞争锁

    • 使用volatile

      volatile 的读写操做不会致使上下文切换,开销较小,但volatile只保证可见性,不保证原子性

    • 使用CAS

      CAS 是一个原子的 if-then-act 操做,能够在我外部锁的状况下来保证读写操做的一致性,如Atomic包中的算法

    • 其它非阻塞乐观锁

wait/notify优化

  • 使用notify()代替notifyAll()

    众所周知,notifyAll会唤醒全部相关的线程,而notify则会唤醒指定线程,以减小过多不相关线程的上下文切换

  • 使用Lock+Condition组合的方式替代wait/notify

    synchronized是基于系统层面实现的,而Lock则是应用程序层面实现的,不会形成用户态/内核态的切换

    Condition会避免相似notifyAll提早唤醒过多无关线程的问题

合理设置线程池大小

线程池数量不宜设置过大,线程池数量设置过大容易致使大量线程处于等待CPU时间片的状态(RUNNABLE),同时也会致使过多的上下文切换

使用协程实现非阻塞等待

协程能够看作是一种轻量级线程

前文介绍到,Java线程使用1:1线程模型,每一个用户线程都会映射到一个系统线程,线程由内核来管理

协程则使用N:M线程模型,协程彻底由应用程序来管理,避免了众多的上下文切换

(协程不等于没有系统线程,只是会大大减小系统线程上下文切换的次数)

总结

  • 操做系统体系架构主要分为用户态(user context)内核态(kernel context)
  • 因为系统操做分不一样的执行等级,应用程序在执行一些高等级操做时会发生用户态/内核态的切换
  • 用户线程由应用程序建立、调度、撤销,不须要内核的支持
  • 内核线程由内核建立、调用、撤销,并由内核维护线程的上下文信息及线程切换
  • 线程模型分为1:1N:1N:M三种,Java在window及linux上采用1:1线程模型,即每一个用户线程都会对应一个内核线程
  • Java中的线程状态并无使用系统线程状态一一对应的方式,而是使用NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED六种状态
  • 用户态/内核态的切换会致使进程间上下文切换
  • 多线程两个运行状态的互相切换会致使线程间的上下文切换,诸如synchronized wait join park sleep 等常见操做均会引发线程间的上下文切换
  • 理解线程上下文切换的缘由,合理优化程序,减小上下文切换,减轻系统负担

订阅号