进程与线程

进程

  • 程序由指令和数据组成,但是这些指令要运行,数据要读写,就必须将指令加载至 CPU,数据加载至内存。在指令运行过程中还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理IO的;
  • 当一个程序被执行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程;
  • 进程还可以视为程序的一个实例。大部分程序可以同时运行多个实例进程。

线程

  • 一个进程内可以有N个线程;
  • 一个线程就是一个指令流,将指令流中一条条指令以一定的顺序交给 CPU 执行;
  • Java 中,线程作为最小调度单位,进程作为资源分配的最小单位。

进程与线程对比

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

为什么使用多线程

  • 提升操作系统CPU的利用率
  • 更快更高效的响应用户

    守护线程

    在Java中有两类线程:User Thread(用户线程)、Daemon Thread(守护线程)

只要当前JVM实例中尚存在任何一个非守护线程没有结束,守护线程就全部工作;
只有当最后一个非守护线程结束时,守护线程随着JVM一同结束工作。

Daemon的作用是为其他线程的运行提供便利服务,守护线程最典型的应用就是 GC (垃圾回收器),它就是一个很称职的守护者。

线程分为内核态、用户态

  • 内核态:CPU可以访问内存所有数据,,包括外围设备,例如硬盘,、网卡、 CPU也可以将自己从一个程序切换到另一个程序;
  • 用户态:只能受限的访问内存, 且不允许访问外围设备。占用CPU的能力被剥夺,CPU资源可以被其他程序获取;
    • 管理开销小:创建、销毁不需要系统调用
    • 切换成本低:用户空间程序可以自己维护,不需要走操作系统调度。

synchronized 是 JVM 层面上的锁的实现,可以解决可见性、顺序性和原子性。

查看进程线程的方法

Windows

  • 查看进程:tasklist
  • 杀死进程:taskkill /T /F /PID <PID>,/T 表示终止指定的进程和由它启用的子进程,/F 表示强制执行

    Linux

  • 查看所有进程:ps -ef

  • 查看某个进程:ps -fT -p
  • 杀死进程:kill -9
  • 按大写H切换时候显示线程:top
  • 查看某个进程的所有线程:top -H -p

    Java

  • 查看所有 java 进程:jps

  • 查看某个 Java 进程 的所有线程状态:jstack
  • 来查看某个 Java 进程中线程的运行情况(图形化界面):jconsole

    并行与并发概念

    并发:是同一时间应对多件事情的能力; 并行:是同一时间动手做多件事情的能力;

同步与异步

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

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

线程运行原理

栈与栈针

Java Virtual Machine Statcks (Java 虚拟机栈) 堆栈(stack)按照FILO(First In Last Out,后进先出)的原则存储数据。

JVM 是有堆、栈、方法区等组成,其中栈内存是在每个线程启动后,虚拟机在栈内存中分配一块栈内存;

  • 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存;
  • 每一个线程只有一个活动栈帧,对应着正在执行的那个方法;

线程的上下文切换

由于以下原因导致 CPU 不再执行当前的线程,转而执行另一个线程的代码

  • 线程的 CPU 时间片用完了
  • 垃圾回收
  • 有更高优先级的线程需要运行;
  • 线程自己调用了sleep、yield、wait、join、park、synchronized、lock等方法程序;

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

  • 状态包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回方法等;
  • Context Switch 频繁发生会影响性能;

CPU与缓存行

因为CPU与内存的速度差异很大,需要靠预读数据至缓存来提升效率。 而缓存以缓存行为单位,每个缓存对应着一块内存,一般是 64byte; 缓存的加入会造成数据副本的产生,即同一份数据会缓存在不同核心的缓存行中,CPU要保证数据的一致性,如果某个CPU核心更改了数据,其他CPU核心对应的整个缓存行必须失效。

并发解决思路

  1. 共享模型
  2. 非共享模型

synchronized 实际上是利用对象所保证了临界区代码的原则性,保证临界区代码对外是不可分割的,不会被线程切换所打断;
**

  1. // synchronized 锁普通方法,相当于锁住的是this对象
  2. public synchronized void methodA(){
  3. // 方法体
  4. }
  5. // 锁住的是this对象
  6. public void methodAA(){
  7. synchronized (this){
  8. // 方法体
  9. }
  10. }
  11. // synchronized 锁静态方法,相当于锁住的是类对象
  12. public synchronized static void methodA(){
  13. // 方法体
  14. }
  15. // 锁住的是类对象
  16. public static void methodAA(){
  17. synchronized (Application.class){
  18. // 方法体
  19. }
  20. }

变量的线程安全分析

成员变量和静态变量是否线程安全?

  • 如果他们没有共享,则线程安全
  • 如果他们被共享了,根据他们的状态是否能够被改变
  1. 如果只读,则线程安全
  2. 如果有读写,则这段代码是临界区,需要考虑线程安全

    局部变量是否线程安全

  • 局部变量是线程安全的
  • 但是局部变量引用的对象则未必:
  1. 如果该对象没有逃离方法的作用访问,则线程安全;
  2. 如果该对象逃离方法的作用范围,需要考虑线程安全;

    常见的线程安全的类

  3. String

  4. Integer
  5. StringBuffer
  6. Random
  7. Vector
  8. Hashtable
  9. java.util.concurrent 包下的类

    共享模型

    共享问题
    synchronized
    线程安全分析
    Monitor
    wait/notify
    线程状态转换
    活跃性
    lock

    共享模型之不可变类

  • 不可变类的使用
  • 不可变类的设计
  • 无状态类的设计

    两种加锁方式

  1. JVM层面上 synchronized 的关键字
  2. API 层面上 lock 锁、J.U.C