0%

并发知识

何为进程?何为线程?(以Java为例)

  1. 进程

    在面向进程设计的系统(如早期的UNIX,Linux 2.4及更早的版本)中,进程是程序的基本执行实体;
    在面向线程设计的系统(如当代多数操作系统、Linux 2.6及更新的版本)中,进程本身不是基本运行单位,而是线程的容器。

    若干进程有可能与同一个程序相关系,且每个进程皆可以同步(循序)或异步(平行)的方式独立运行。

  2. 线程

    线程(英语:thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。
    一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。

    线程是独立调度和分派的基本单位。线程可以为操作系统内核调度的内核线程,如Win32线程;由用户进程自行调度的用户线程,如的POSIX Thread;或者由内核与用户进程,如Windows 7的线程,进行混合调度。

    同一进程中的多条线程将共享该进程中的全部系统资源,如虚拟地址空间,文件描述符和信号处理等等。
    但同一进程中的多个线程有各自的调用栈(call stack),自己的寄存器环境(register context),自己的线程本地存储(thread-local storage)。

    线程是比进程更轻量级的调度执行单位,线程的引入,可以把一个进程的资源调配和执行调度分开,各个线程既可以共享进程资源(内存地址、文件I/O等),又可以独立调度(线程是CPU调度的基本单位)

关于进程和线程

  1. 进程之间是隔离的。Java的虚拟机就是一个进程,虚拟机可以屏蔽不同操作系统的差异。

    线程之间可以共享进程资源,又有各自的调用栈、寄存器环境和线程本地存储。(虚拟机栈、本地方法栈、程序计数器)

  2. 在一个进程内部,要同时干多件事,就需要同时运行多个“子任务”,我们把进程内的这些“子任务”称为线程(Thread)。

    单线程一次只能干一件事,无法并发和并存。

  3. 进程是拥有资源的基本单位;线程是cpu调度的基本单位。

实现线程的的3种方式

实现线程主要有3种方式:使用内核线程实现、使用用户线程实现和使用用户线程加亲良机进程混合实现。

1. 使用内核线程实现

内核线程:直接由操作系统内核支持的线程,这种线程由内核来完成线程切换,内核通过操纵调度器对线程进行调度,并负责将线程的任务映射到各个处理器上。

每个内核线程可以视为内核的一个分身,这样操作系统就有能力同时处理多件事情,支持多线程的内核就叫做多线程内核。

程序一般不会直接使用内核线程,而是去使用内核线程的一种高级接口 —— 轻量级进程(我们通常意义上所说的线程)。

由于每个轻量级进程都由一个内核线程支持,因此只有先支持内核线程,才能有轻量级进程。两者之间的关系为 1:1

轻量级进程局限性:
- 基于内核线程实现的线程的操作(如创建、析构及同步),都需要进行系统调用。而系统调用的代价相对较高,需要在用户态和内核态中来回切换,效率会受到限制。
- 每个轻量级进程都需要有一个内核线程支持,会消耗一定的内核资源(如内核线程的栈空间),也因此一个系统支持轻量级进程的数量是有限的

2. 使用用户线程实现

用户线程:广义上,不是内核线程的线程,就是用户线程。狭义上,用户线程指完全建立在用户空间的线程库上,系统内核不能感知线程存在的实现。

用户线程的建立、同步、销毁和调度完全是在用户态中完成的,不需要内核帮助,因此操作可以是快速且低消耗的,也可以支持规模更大的线程数量。

这种进程与用户线程之间的关系为 1:N

用户线程优势在不需要系统内核支援,劣势也在于没有系统内核的支援。由于没有系统内核的支援,线程的创建、切换、调度等问题的解决会变得复杂。

3. 使用用户线程加轻量级进程混合实现

这种混合实现下,既存在用户线程,也存在轻量级进程。用户线程还是完全建立在用户空间中,因此用户线程的创建、切换、析构等操作依然廉价,并且可以大规模的用户线程并发。
而操作系统提供支持的轻量级进程则作为用户线程和内核线程的桥梁,这样可以使用内核提供的线程调度功能及处理器映射,并且用户线程的系统调用要通过轻量级线程来完成,大大降低了整个进程被完全阻塞的风险。

两者的关系为 M:N

线程调度方式

线程调度是指系统为线程分配处理器使用权的过程,主要的调度方式有两种: 协同式线程调度 和 抢占式线程调度。

  1. 协同式调度:线程的执行时间由线程本身控制,线程工作完成后主动通知系统切换到其他线程。

    好处:线程的切换操作对线程自身是可知的,·所以没有什么线程同步的问题。
    坏处:线程执行时间不可控制,若线程运行有问题,一直不告知系统进行线程切换,那么程序会一直阻塞。

  2. 抢占式调度:线程的执行时间由系统来分配,线程切换不由线程本身决定

    好处:线程的执行时间由系统控制,不会有一个线程导致整个进程阻塞的问题

    线程优先级:

    Java语言一共设置了10个级别的线程优先级

线程状态转换

Java语言定义了5种线程状态:新建、运行、无限期等待、限期等待、阻塞、结束

  • 新建(New):创建后尚未启动的线程处于这种状态

  • 运行(Runnable): Runnable包括了操作系统线程状态中的Running和Ready,也就是处于此状态的线程可能是正在执行,也可能是在等待CPU为它分配执行时间。

  • 无限期等待(Waiting): 处于此状态的线程不会被分配CPU执行时间,它们要等待被其他线程显式地唤醒。
    以下方法会让线程陷入无限期的等待状态:

    • 没有设置Timeout参数的Object.wait()方法;
    • 没有设置Timeout参数的Thread.join()方法;
    • LockSupport.park()方法。
  • 限期等待(Timed Waiting): 处于这种状态的线程也不会被分配CPU执行时间,不过无须等待被其他线程显式地唤醒,在一定时间之后它们会由系统自动唤醒。
    以下方法会让线程陷入限期的等待状态:

    • Thread.sleep()方法;
    • 设置了Timeout参数的Object.wait()方法;
    • 设置了Timeout参数的Thread.join()方法;
    • LockSupport.parkNanos()方法;
    • LockSupport.parkUntil()方法。
  • 阻塞(Blocked): 线程被阻塞了,等待获取一个排他锁,这个时间将在另一个线程放弃这个锁的时候发生。
    在程序等待进入同步区域的时候,线程将进入这种状态。

  • 结束(Terminated): 已终止线程的线程状态,线程已经执行结束。

    定义线程任务 —— Runnable

    要想定义任务,只需实现Runnable接口并编写run()方法

    任务的run()方法通常会有某种形式的循环,使得任务一直运行下去直到不再需要,所以要设定跳出循环的条件(比如循环最后使用某个FLAG)。

    使用线程池管理多线程 —— Executor

    几种不同的Executor

    • CachedThreadPool
    • FixedThreadPool
    • SingleThreadPool

    它们的创建都是调用同一个构造器方法ExecutorThreadPoll(***),区别只是参数不同。

    public ThreadPoolExecutor(int corePoolSize,

    int maximumPoolSize,
    long keepAliveTime,
    TimeUnit unit,
    BlockingQueue<Runnable> workQueue,
    ThreadFactory threadFactory,
    RejectedExecutionHandler handler)

    使用何种类型?

    CachedThreadPool在程序执行过程中通常会创建与所需数量相同的线程,然后在它回收旧线程时停止创建新线程,
    因此它是合理的Executor的首选。只有当这种方式会引发问题时,我们才需要切换到FixedThreadPool。

    SingleThreadExecutor就像是线程数量为1 的FixedThreadPool。这个适用于你希望在另一个线程中连续运行的任何任务(长期存活的任务)。
    如果向SingleThreadExecutor提交了多个任务,那么这些任务将排队,每个任务都会在下一个任务开始之前运行结束,所有的任务将使用相同的线程。
    使用SingleThreadExecutor,可以确保任一时刻在任何线程中都只有唯一的任务在运行。此种方式可是你不需要在共享资源上同步(虽然有时候更好的解决方案是在资源商同步)。