一. 线程状态

下图是Linux定义的七种状态。

image.png

As in every Unix flavour, in Linux a process can be in a number of states. It's easiest to observe it in tools like ps or top: it's usually in the column named S. The documentation of ps describes the possible values:

PROCESS STATE CODES
R running or runnable (on run queue)
D uninterruptible sleep (usually IO)
S interruptible sleep (waiting for an event to complete)
Z defunct/zombie, terminated but not reaped by its parent
T stopped, either by a job control signal or because
it is being traced
[...]

A process starts its life in an R "running" state and finishes after its parent reaps it from the Z "zombie" state.

六个状态定义: java.lang.Thread.State

  1. New: 尚未启动的线程的状态
  2. Runnable: 可运行线程的线程状态,等待CPU调度
  3. Blocked: 线程阻塞等待监视器锁定的线程状态,处于synchronized同步代码块或方法中被阻塞
  4. Waiting: 等待线程的线程状态。下列不带超时的方式:
    1. Object.wait
    2. Thread.join
    3. LockSupport.park
  5. Timed Waiting: 具有指定等待时间的等待线程的线程状态。
    1. Thread.sleep
    2. Object.wait
    3. Thread.join
    4. LockSupport.parkNanos
    5. LockSupport.parkUntil
  6. Terminated: 终止线程的线程状态,线程正常执行完成或出现异常。

二. 线程终止

stop: 强制终止,破坏线程安全

interrupt: 如果目标线程在调用Object class的wait、join、sleep方法时被阻塞,那么Interrupt会生效,该线程的中断状态会被清除,抛出InteruptedException异常。如果目标线程是被I/O或者NIO中的Channel所阻塞,同样,I/O操作会被中断或者返回特殊异常值。达到终止线程的目的。

三. CPU缓存和内存屏障

1. CPU性能优化手段——缓存

为了提高程序运行的性能,现代CPU在很多方面对程序进行了优化。

例如: CPU高速缓存。尽可能地避免处理器访问主内存的时间开销,处理器大多会利用缓存以提高性能。

2. 多级缓存
  • L1 Cache

    L1 Cache(一级缓存)是CPU第一层高速缓存,分为数据缓存和指令缓存。一般服务器CPU的L1缓存的容量在32~4096Kb。

  • L2 Cache

    由于L1级高速缓存容量的限制,为了再次提高CPU的运算速度,在CPU外部又放置了一层高速缓存,即二级缓存。

  • L3 Cache

    L3级缓存是为了进一步降低内存延迟,提升大数据量计算时处理器的性能。具有较大L3缓存的处理器提供更有效的文件系统缓存行为及较短消息和处理器队列长度。一般是多核共享一个L3缓存。

CPU在读取数据时,先在L1中寻找,再从L2寻找,再从L3寻找,然后是内存,再后是外存储器。

3. 缓存同步协议

多CPU读取同样的数据进行缓存,进行不同运算之后,最终写入主内存以哪个CPU为准?

在这种高速缓存回写的场景下,有一个缓存一致性协议多数CPU厂商对它进行了实现。

MESI协议,他规定每条缓存有一个状态位,同时定义了下面四个状态。

  • 修改态(Modified): 此cache行已被修改过(脏行),内容已不同于主存,为此cache专有。
  • 专有态(Exclusive): 此cache行内容同于主存,但不出现在其他cache中。
  • 共享态(Shared): 此cache行内容同于主存,但也出现在其他cache中。
  • 无效态(Invalid): 此cache行内容无效(空行)。

多处理器时,单个CPU对缓存中数据进行了改动,需要通知给其他CPU。

也就是意味着,CPU处理要控制自己的读写操作,还要监听其他CPU发出的通知,从而保证最终一致性。

4. CPU性能优化手段——运行时指令重排

指令重排的场景: 当CPU写缓存时发现缓存区块正在被其他CPU占用,为了提高CPU处理性能,可能将后面的读缓存命令优先执行。

x = 100; // 1. 将100写入x
y = z;	 // 2. 读取z的值
		 // 3. 将z值写入y

重排序后执行

1. 读取z的值
2. 将z值写入y
3. 将100写入x

并非随便重排,需要遵守as-if-serial语义,不管怎么重排(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。编译器、runtime和处理器都必须遵守as-if-serial语义。

编译器和处理器不会对存在数据依赖关系的操作做重排序

5. 问题
  1. CPU高速缓存下有一个问题:

    缓存中的数据与主内存的数据并不是实时同步的,各CPU(或CPU核心)间缓存的数据也不是实时同步。在同一个时间点,各CPU所看到同一内存地址的数据的值可能是不一致的。

  2. CPU执行指令重排序优化下有一个问题:

    虽然遵守了as-if-serial语义,但仅在但CPU自己执行的情况下能保证结果正确。多核多线程中,指令逻辑无法分辨因果关联,可能出现乱序执行,导致程序运行结果错误。

6. 内存屏障

处理器提供了两个内存屏障指令(Memory Barrier)用于解决上述两个问题:

  1. 写内存屏障(Store Memory Barrier): 在指令后插入Store Barrier,能让写入缓存中的最新数据更新写入主内存,让其他线程可见。强制写入主内存,这种显示调用,CPU就不会因为性能考虑而去对指令重排。
  2. 读内存屏障(Load Memory Barrier): 在指令前插入Load Barrier,可以让高速缓存中的数据失效,强制从 主内存加载数据。强制读取主内存,让CPU缓存与主内存保持一直,避免缓存导致的一致性问题。

四. 线程通信

通信的方式

想要实现多个线程之间的协同,如: 线程执行先后顺序、获取某个线程执行结果等等。

涉及到线程之间互相通信,分为下面四类:

  1. 文件共享

  2. 网络共享

  3. 共享变量

  4. jdk提供的线程协调API,suspend/resume、wait/notify、park/unpark。

  • 被弃用的suspend和resume

    作用: 调用suspend挂起目标线程,通过resume可以恢复线程执行。

    被弃用的主要原因是容易写出死锁代码。

  • wait/notify机制

    只能由同一对象锁的持有者线程调用,也就是写在同步代码块里面,否则会抛出IllegalMonitorStateException异常。

    wait方法导致当前线程等待,加入该对象的等待集合中,并且放弃当前持有的对象锁。notify/notifyAll方法唤醒一个或所有正在等待这个对象锁的线程。

    注意: 虽然wait会自动解锁,但是对顺序有要求,如果在notify被调用之后,才开始wait方法的调用,线程会永远处于WAITING状态。

  • park/unpark机制

    线程调用park则等待许可,unpark方法为指定线程提供许可(permit)。

    不要求park和unpark的调用顺序,多次调用unpark之后再调用park,线程会直接运行,但不会叠加,也就是说,连续多次调用park方法,第一次会拿到许可直接运行,后续调用会进入等待。

五. ThreadLocal与线程封闭

1. 线程封闭

多线程访问共享可变数据时,涉及到线程间数据同步的问题。并不是所有时候都要用到共享数据,所以就有了线程封闭的概念。

数据被封闭在各自的线程之中,就不需要同步,这种通过将数据封闭在线程中而避免使用同步的技术称为线程封闭。

2. ThreadLocal

ThreadLocal是Java里一种特殊的变量。它是一个线程级别变量,每个线程都有一个ThreadLocal,就是拥有一个独自的变量,竞争条件被消除了,在并发模式下是绝对安全的变量。

用法: ThreadLocal<T> var = new ThreadLocal<>();

会自动在每个线程上创建一个泛型T的副本,副本之间彼此独立,互不影响。可以用ThreadLocal存储一些参数,以便在线程多个方法中传递使用,代替参数传递。

3. 栈封闭

局部变量的固有属性之一就是封闭在线程中,他们位于执行线程的栈中,其他线程无法访问。

六. 线程池原理

1. 为什么要用线程池

线程池是不是越多越好?

  • 线程在Java中是一个对象,更是操作系统的资源,线程创建、销毁、上下文切换需要时间。如果创建时间加销毁时间大于执行任务时间,就很不合算。

  • Java对象占用堆空间,操作系统线程占用系统内存,根据JVM规范,一个线程默认最大栈大小1M,这个栈空间是需要从系统内存中分配的。线程过多,会消耗很多的内存。

  • 操作系统需要频繁切换上下文,影响性能。

线程池的出现,就是为了控制线程创建的数量。

2. 线程池原理——概念
  1. 线程池管理器: 用于创建并管理线程池,包括创建线程池,销毁线程池,添加新任务。
  2. 工作线程: 线程池中线程,在没有任务时处于等待状态,可以循环的执行任务。
  3. 任务接口: 每个任务必须实现的接口,以供工作线程调度任务的执行,它主要规定了任务的入口,任务执行完成后的收尾工作,任务的执行状态等。
  4. 任务队列: 用于存放没有处理的任务,提供一种缓冲机制。
3. 线程池API——接口定义和实现类
类型名称描述
接口Executor最上层的接口,定义了执行任务的方法execute
接口ExecutorSerivce继承了Executor接口,扩展了Callable、Future、关闭方法
接口ScheduledExecutorService继承了ExecutorService,增加了定时任务相关的方法
实现类ThreadPoolExecutor基础、标准的线程池实现
实现类ScheduledThreadPoolExecutor继承了ThreadPoolExecutor,实现了ScheduledExecutorService中相关定时任务的方法。
4. 线程池API——方法定义
ExecutorService
  1. awaitTermination(long timeout, TimeUnit unit): 检测ExecutorService是否已经关闭,直到所有任务完成执行,或超时发生,或当前线程被中断
  2. invokeAll(Collection<? extends Callable> tasks): 执行给定的任务集合,执行完毕后返回结果
  3. invokeAll(Collection<? extends Callable> tasks, long timeout, TimeUnit unit): 执行给定的任务集合,执行完毕或者超时后返回结果,其他任务终止。
  4. invokeAny(Collection<? extends Callable> tasks): 执行给定的任务,任意一个执行成功则返回结果,其他任务终止。
  5. invokeAny(Collection<? extends Callable> tasks, long timeout, TimeUnit unit): 执行给定的任务,任意一个执行成功或超时后则返回结果,其他任务终止。
  6. isShutdown(): 如果此线程池已关闭,则返回true。
  7. isTerminated(): 如关闭后所有任务都已完成,则返回true。
  8. shutdown(): 优雅关闭线程池,之前提交的任务将被执行,但不会接受新的任务。
  9. shutdownNow(): 尝试停止所有正在执行的任务,停止等待任务的处理,并返回等待执行任务的列表。
  10. submit(Callable task): 提交可运行任务以执行,并返回一个Future对象,执行结果为null。
  11. submit(Runnable task, T result): 提交可运行任务以执行,并返回Future,执行结果为传入的result。
ScheduledExecutorService
  1. schedule(Callable callable, long delay, TimeUnit unit): 创建并执行一个一次性任务,过了延迟时间就会被执行
  2. schedule(Runnable command, long delay, TimeUnit unit): 创建并执行一个一次性任务,过了延迟时间就会被执行
  3. scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit): 创建并执行一个周期性任务,过了给定的初始延迟时间,会第一次被执行,执行过程中发生了异常,任务就停止。一次任务执行时长超过了周期时间,下一次任务就会等到任务执行结束后立刻执行。
  4. scheduleWithFixedDelay(Runnable command, long initialDelay, long period, TimeUnit unit): 创建并执行一个周期性任务,过了给定的初始延迟时间,第一次被执行,后续以给定的周期时间执行,执行过程中发生了异常,任务就停止。一次任务执行时长超过了周期时间,下一次任务会在上一次任务结束的时间基础上计算延迟时间。
5. 线程池API——Executors工具类

我们可以自己实例化线程池,也可以用Executors创建线程池的工厂类,常用方法如下:

  • newFixedThreadPool(int nThreads): 创建一个固定大小、任务队列容量无界的线程池。核心线程数=最大线程数。
  • newCachedThreadPool(): 创建的是一个大小无界的缓冲线程池。它的任务队列是一个同步队列。任务加入到池中,如果池中有空闲线程,则利用空闲线程执行,如无则创建新线程执行。池中的线程空闲超过60秒,将被销毁释放。线程数随任务的多少变化,适用于执行耗时较小的异步任务。池的核心线程数=0,最大线程数=Integer.MAX_VALUE。
  • newSingleThreadExecutor(): 只有一个线程来执行无界队列的单一线程池,该线程池确保任务按加入顺序一个一个依次执行。当唯一的线程因任务异常终止时,将创建一个新的线程来继续执行后面的任务。与newFixedThreadPool(1)的区别在于,单一线程池的池大小在newSingleThreadExecutor方法中硬编码,不能在改变。
  • newScheduledThreadPool(int corePoolSize): 能定时执行任务的线程池。该池的核心线程数由参数指定,最大线程数=Integer.MAX_VALUE。
6. 线程池原理——任务execute过程

  1. 是否达到核心线程数量? 没达到,创建一个工作线程来执行任务。
  2. 工作队列是否已满?没满,则将新提交的任务存储在工作队列里。
  3. 是否达到线程池最大数量?没达到,则创建一个新的工作线程来执行任务。
  4. 最后,执行拒绝策略来处理这个任务。
7. 线程数量
  • 如何确定合适数量的线程?

    计算型任务: cpu数量的1~2倍

    IO型任务: 相对比计算型任务,需多一些线程,要根据具体的IO阻塞时长进行考量决定。

    如Tomcat中默认的最大线程数为200,也可以考虑根据需要在一个最大数量和最小数量之间自动增减线程数。

七. 线程可见性问题

1. JVM运行时数据区

线程独占: 每个线程都会有它独立的空间,随线程生命周期而创建和销毁

线程共享: 所有线程都能访问这块内存数据,随虚拟机或者GC而创建和销毁

2. 多线程中的问题
  1. 所见非所得
  2. 无法肉眼去检测程序准确性
  3. 不同的运行平台有不同表现
  4. 错误很难重现
3. 指令重排序

Java编程语言的语义允许编译器和微处理器执行优化,这些优化可以与不正确的同步代码交互,产生看似矛盾的行为。

thread-1thread-2
1: r2 = A3: r1 = B
2: B = 14: A = 2

重排序后

thread-1thread-2
B = 1r1 = B
r2 = A4: A = 2

八. 原子操作

九. 锁相关

  • 自旋锁: 为了不放弃CPU执行时间,循环的使用CAS技术对数据尝试更新,直到成功。

  • 悲观锁: 假定会发生并发冲突,同步所有对数据的相关操作,从读数据就开始上锁。

  • 乐观锁: 假定没有冲突,在修改数据时如果发现数据和之前获取的不一致,则读最新数据,修改后重试修改。

  • 独享锁(写): 给资源加上写锁,线程可以修改资源,其他线程不能再加锁(单写)

  • 共享锁(读): 给资源加上读锁后只能读不能改,其他线程也只能加读锁,不能加写锁(多读)

  • 可重入、不可重入锁: 线程拿到一把锁之后,可以自由进入同一把锁所同步的其他代码。

  • 公平、非公平锁: 争抢锁的顺序,如果是按先来后到,则为公平。

几种重要的锁实现: synchronized、ReentrantLock、ReentrantReadWriteLock

1. 同步关键字synchronized

属于最基本的线程通信机制,基于对象监视器实现的。

Java中每个对象都与一个监视器相关联,一个线程可以锁定或者解锁。

一次只有一个线程可以锁定监视器。

试图锁定该监视器的如何其他线程都会被阻塞,直到他们可以获得该监视器上的锁为止。

  • 特性: 可重入、独享、悲观锁
  • 锁的范围: 类锁、对象锁、锁消除、锁粗化

同步关键字,不仅是实现同步,根据JVM规定还能保证可见性(读取最新主内存数据,结束后写入主内存)

Q.E.D.


Talk is cheap, show me the code.