进程与线程

最后一次更新时间:Sunday, November 8th 2020, AM

 

进程间通信

  1. 匿名管道:
    用于具有亲缘关系(父子、兄弟)的进程之间通信。

  2. 有名管道:
    FIFO,任意两个进程。

  3. 信号:
    用于通知某个进程发生了某事件。

  4. 消息队列:
    FIFO,也可以随即查询,更灵活。
    克服了信号信息承载量少的缺点。
    克服了管道只支持字节流,缓冲区大小受限的缺点。

  5. 信号量:
    是一个计数器,用于多进程访问共享数据,帮助进程间同步。

  6. 共享内存:
    依赖于同步操作,最有用的进程间通信方式。

  7. 套接字:
    Client与Server的进程之间进行通信

 

 

 

线程间同步

  1. 互斥量(MuteX):
    采用互斥对象机制(把对象加上互斥锁),拥有互斥对象的线程才能访问。例如Java的Synchronized关键字及其各种锁。

  2. 信号量:
    允许同一时刻,多个线程访问统一资源。但需控制最大线程数量。

  3. 事件(Event):
    wait() / notify():通过通知操作,来保持多线程同步,还能实现优先级比较。

 

 

 

进程的调度算法

  1. 先到先服务(FCFS):
    顾名思义,不解释。

  2. 短作业优先(SJF):
    顾名思义,不解释。

  3. 时间片轮转:
    最古老、最公平、最简单、使用最广。

  4. 多级反馈队列:
    高优先级 && 短作业 的进程优先处理。公认效果较好,Unix用的就是这种调度算法。

  5. 优先级调度:
    顾名思义,不解释。

 

 

 

OS内存管理

OS内存管理的内容

分配、回收、逻辑地址与物理地址的转换。

OS内存管理的机制

  • 连续分配

    • 块式管理:
      非常古老,把内存分为几个固定大小的块,每个块只存放一个进程。
       
  • 非连续分配

    • 页式管理:
      把内存划分为一页一页,页较小,比块式分配粒度更大,减少了碎片。通过页表中的映射,来管理逻辑与物理地址的转换。

    • 段式管理:
      粒度更小,进一步减少了碎片。每个段都定义了信息。

    • 段页式管理:
      内存先分成段,再分成页,并提高了运行时的安全性。
       

分页与分段
相同处:提高内存利用率,减少了碎片。离散存储,但每个段/页都是连续的。
不同处:页的大小是固定的,段的大小取决于程序而变化。分页满足OS的性能需求,分段满足用户的动态需求。

 

 

 

CPU寻址(虚拟寻址)

CPU将虚拟地址翻译为物理地址。这个过程需要CPU中的内存管理单元。

若无虚拟地址的存在(早期OS)的影响:程序可访问任意内存,不安全。且容易造成数据覆盖,很难同时运行多个程序。

 

 

 

虚拟内存

可以让程序拥有超出物理内存实际大小的空间,且为每个进程提供一致的、私有的空间。管理高效,减少出错。

 

 

 

局部性原理

  • 时间局部性:执行了一条指令,通常不久后会再次执行。
  • 空间局部性:访问了一个数据,通常不久后会再次访问。
  •  

 

 

并发与并行

  • 并发:同一时间段,多任务执行。
  • 并行;同一时间点,多任务执行。
    并行是并发的一个子集,着重于“同时”

 

 

 

线程的生命周期

 

 

 

上下文切换

一个线程的时间片用完,回到 READY就绪态
把CPU让给其它线程使用
切换之前,先保存一下自己的状态
切换回来再加载这个状态

上下文切换可能是OS中最耗时的操作

 

 

 

死锁

产生死锁需要满足四个条件

  1. 互斥:该资源一次只能被一个线程占用
  2. 请求和保持:阻塞时,不释放现有资源
  3. 不剥夺:资源不能被剥夺,只能自己用完释放
  4. 形成循环:若干线程形成循环等待

 

 

 

死锁的原因

  1. 资源不足
  2. 资源分配不合理
  3. 进程顺序不合理

 

 

 

sleep() 和 wait()

  1. 都可以暂停线程
  2. sleep()不释放锁,wait()释放
  3. wait()常用于线程间通信,sleep()只是暂停线程
  4. wait()需要notify()唤醒,sleep()超时自动唤醒

 

 

 

调用start()执行run() 与 直接调用run()

start()是把线程转换为RUNNABLE状态,CPU自动运行,是真正的多线程。
run()只是一个普通调用,顺序执行的一个普通方法。

 

 

 

ThreadLocal 线程局部变量

创建一个ThreadLocal变量,每个访问该变量的线程都会保存它的本地副本。
get()获取其默认值,set()修改其本地副本的值。

 

 

 

创建线程的四个方法

  1. 继承Thread类
  2. 实现Runnable接口
  3. 实现Callable接口
  4. 从线程池中获取

 

 

 

终止线程

  1. 设置一个flag(退出标志),运行完自行终止。
  2. 使用interrupt()抛出异常,终止线程。
  3. 使用stop()强行终止。(不安全,已废弃,不推荐)

 

 

 

如何保证线程安全

  1. 对非安全的代码加锁
  2. 使用线程安全的类
  3. 多线程并发时,把线程共享的变量,改为方法级的局部变量

 

 

 

守护线程(Daemon Thread)

又名 服务/精灵/后台 线程
用来保持JVM运行,优先级较低。

 

 

 

线程池

线程池的作用(优点)

  1. 限制线程的数量,不会因线程过多导致系统性能下降。
  2. 不需要频繁创建/销毁,节省系统开销。
  3. 可以统一进行管理。

线程池的组成

  • 管理器:创建、管理线程池。
  • 工作线程:池中的线程,无任务时处于等待态,循环使用。
  • 任务接口:每个任务必须实现的接口,以供工作线程调度。其规定了任务入口、任务完成后的收尾工作,任务的状态。
  • 任务队列:存放未处理的任务(缓冲区)

线程池的分类

  1. newFixedThreadPool:指定工作线程的数量。
  2. newCachedThreadPool:工作线程数量可变,空闲时(默认1分钟)则终止一个线程。
  3. newSingleThreadExecutor:只有一个工作线程,若意外终止,会自动重新创建。
  4. newSchduleThreadPool:工作线程数量固定,且支持周期性任务。