【多线程笔记】基础
概念
信号
有时候你需要让线程处于等待状态,直到接收其他线程发来的消息,这就叫发送信号(signaling)
最简单的发送信号的方式就是使用ManualResetEvent。调用它的WaitOne()方法阻塞线程,调用Set()方法开启信号
临界区
一段代码内如果存在对共享资源的多线程读写操作,那么称这段代码为临界区。如:
static int counter = 0;
static void increment()
{// 临界区
counter++;
}
static void decrement()
{// 临界区
counter--;
}
竞争条件
当两个或两个以上的线程访问共享数据,并且尝试同时改变它时,就发生争用的情况。它们所依赖的那部分共享数据,叫做竞争条件。
数据争用是竞争条件中的一种,出现竞争条件可能会导致内存(数据)损坏或者出现不确定性的行为。
线程同步
如果有 N 个线程都会执行某个操作,当一个线程正在执行这个操作时,其它线程都必须依次等待,这就是线程同步。
多线程环境下出现竞争条件,通常是没有执行正确的同步而导致的。
阻塞
阻塞状态指线程处于等待状态。当线程处于阻塞状态时,会尽可能少占用 CPU 时间。
当线程从运行状态(Runing)变为阻塞状态时(WaitSleepJoin),操作系统就会将此线程占用的 CPU 时间片分配给别的线程。当线程恢复运行状态时(Runing),操作系统会重新分配 CPU 时间片。
分配 CPU 时间片时,会出现上下文切换。
内核模式、用户模式?
只有操作系统才能切换线程、挂起线程,因此阻塞线程是由操作系统处理的,这种方式被称为内核模式(kernel-mode)。
Sleep()
、Join()
等,都是使用内核模式来阻塞线程,实现线程同步(等待)。
内核模式实现线程等待时,出现上下文切换。这适合等待时间比较长的操作,这样会减少大量的 CPU 时间损耗。
如果线程只需要等待非常微小的时间,阻塞线程带来的上下文切换代价会比较大,这时我们可以使用自旋,来实现线程同步,这一方法称为用户模式(user-mode)。
对象锁
它采用互斥的方式让同一时刻至多只有一个线程持有对象锁,其他线程如果想获取这个锁就会阻塞住,这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换
线程的五个状态
新增状态:线程刚创建Thread t = new Thread();
就绪状态:线程执行start方法、sleep时间到、io方法返回、获得同步锁、收到通知
运行状态:当就绪状态的线程获得CPU资源
阻塞状态:当线程执行sleep、wait方法、IO阻塞、等待同步锁、等待通知
死亡状态:执行完成或异常
线程、栈、栈帧
线程启动,虚拟机为其分配一块栈内存
每个栈由多个栈帧(Frame)组成
每个栈帧对应每次方法调用占用的内存
每个线程只有一个活动栈帧,对应当前正在执行的方法
程序运行
线程上下文切换(Thread Context Switch)
因为以下一些原因导致 cpu 不再执行当前的线程,转而执行另一个线程的代码
- 线程的 cpu 时间片用完(每个线程轮流执行,看前面并行的概念)
- 垃圾回收
- 有更高优先级的线程需要运行
- 线程自己调用了 sleep、yield、wait、join、park、synchronized、lock 等方法
当 Context Switch 发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,Java 中对应的概念 就是程序计数器(Program Counter Register),它的作用是记住下一条 jvm 指令的执行地址,是线程私有的