并发相关理论
文章目录
相关概念
- 死锁
- 活锁
那是不是所有的代码都需要认真分析一遍是否存在这三个问题呢?当然不是,其实只有一种情况需要:存在共享数据并且该数据会发生变化,通俗地讲就是有多个线程会同时读写同一数据。那如果能够做到不共享数据或者数据状态不发生变化,不就能够保证线程的安全性了嘛。有不少技术方案都是基于这个理论的,例如线程本地存储(Thread Local Storage,TLS)、不变模式等等,后面我会详细介绍相关的技术方案是如何在 Java 语言中实现的。
但是,现实生活中,必须共享会发生变化的数据,这样的应用场景还是很多的。
但有时线程虽然没有发生阻塞,但仍然会存在执行不下去的情况,这就是所谓的“活锁”。可以类比现实世界里的例子,路人甲从左手边出门,路人乙从右手边进门,两人为了不相撞,互相谦让,路人甲让路走右手边,路人乙也让路走左手边,结果是两人又相撞了。这种情况,基本上谦让几次就解决了,因为人会交流啊。可是如果这种情况发生在编程世界了,就有可能会一直没完没了地“谦让”下去,成为没有发生阻塞但依然执行不下去的“活锁”。
解决“活锁”的方案很简单,谦让时,尝试等待一个随机的时间就可以了。例如上面的那个例子,路人甲走左手边发现前面有人,并不是立刻换到右手边,而是等待一个随机的时间后,再换到右手边;同样,路人乙也不是立刻切换路线,也是等待一个随机的时间再切换。由于路人甲和路人乙等待的时间是随机的,所以同时相撞后再次相撞的概率就很低了。“等待一个随机时间”的方案虽然很简单,却非常有效,Raft 这样知名的分布式一致性算法中也用到了它。
线程的几种状态
线程是一个动态执行的过程,它也有一个从产生到死亡的过程,在 Java 中一个线程完整的生命周期一共包含以下五种状态:
新建状态(New)
当使用 new
关键字和 Thread
类或其子类创建一个线程对象后,那么线程就进入了新建状态
,此时它和其它的 Java 对象一样,仅仅由 JVM 分配了内存,并初始化其成员变量值,它会一直保持这个状态直到调用该对象的 start
方法。
就绪状态(Runnable)
当线程对象调用了 start
方法之后,该线程就进入了就绪状态。就绪状态的线程会放在一个就绪队列中,等待 JVM 里的调度器进行调度。处于就绪状态的线程,随时可能被 CPU 调度执行。
运行状态(Running)
如果就绪状态的执行被 CPU 调度执行,就可以执行 run
方法,此时线程就处于线程状态。处于运行状态的线程最复杂,它可以变为阻塞状态
、就绪状态
和死亡状态
。需要注意一点,线程变为运行状态
之前的状态只能是就绪状态
。
阻塞状态(Blocked)
线程变为阻塞状态是因为某种原因放弃 CPU 的使用权,暂时停止运行,如果执行了 sleep
、suspend
等方法,释放了所占用的资源之后,线程就从运行状态
进入阻塞状态
。等待睡眠时间结束或者获得设备资源之可以重新进入就绪状态
。阻塞可以分为以下三种:
- 等待阻塞 处于
运行状态
的线程调用wait
方法,会使线程进入等待阻塞状态
- 同步阻塞 当线程获取
synchronized
同步锁因为同步锁被其他线程占用而失败后,会使线程进入同步阻塞
- 其它阻塞 通过调用线程的
sleep
或join
发出了 I/O 请求时,线程就会进入到阻塞状态。当sleep
状态超时,join
等待线程终止或超时,或者 I/O 处理完毕,线程重新回到就绪状态
。
死亡状态(Dead)
一个处于运行状态
的线程执行完了 run
方法或者因为其它终止条件发生时,线程就会进入到死亡状态
,该线程结束生命周期。
以上线程各种状态的流转用一张图表示如下:
方法 | 描述 |
---|---|
public final native void notify() | 唤醒在此对象监视器上等待的单个线程,使其进入就绪状态 |
public final native void notifyAll() | 唤醒在此对象监视器上等待的所有线程,使其进入就绪状态 |
public final void wait() | 让当前线程处于·等待阻塞状态 ,直到其他线程调用此对象的notify 方法或notifyAll 方法,当前线程被唤醒,会释放它所持有的锁 |
public final native void wait(long timeout) | 让当前线程处于·等待阻塞状态 ,直到其他线程调用此对象的notify 方法或notifyAll 方法,当前线程被唤醒 |
public final void wait(long timeout, int nanos) | 让当前线程处于·等待阻塞状态 ,直到其他线程调用此对象的notify 方法或notifyAll 方法或者其他某个线程中断当前线程,或者已超过某个实际时间量,当前线程被唤醒 |
另一类是 Thread
类定义的方法,如下所示:
方法 | 描述 |
---|---|
public static native void yield() | 暂停当前正在执行的线程对象,并执行其他线程,yield 方法不会释放锁 |
public static native void sleep(long millis) | 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),sleep 方法不会释放锁 |
public final void join() | 当某个程序执行流中调用其他线程的 join 方法时,调用线程将被阻塞,直到被 join 的线程执行完毕 |
public void interrupt() | 用于中断本线程,这个方法被调用时,会立即将线程的中断标志设置为 true |
public static boolean interrupted() | Thread 类的一个静态方法,它返回一个布尔类型指明当前线程是否已经被中断,interrupted 方法除了返回中断标记之外,它还会清除中断标记(即将中断标记设为 false ) |
public boolean isInterrupted() | Thread 类的一个实例方法,它返回一个布尔类型指明当前线程是否已经被中断,isInterrupted 方法仅仅返回中断标记,不会清楚终端标记 |
线程的优先级
每一个 Java 线程都有一个优先级,这样有助于操作系统确定线程的调度顺序。Java 线程的优先级是一个整数,其取值范围是 1(Thread.MIN_PRIORITY )~ 10(Thread.MAX_PRIORITY )
。默认情况下,每一个线程都会分配一个优先级NORM_PRIORITY(5)
。具有较高优先级的线程对程序更重要,并且应该在低优先级的线程之前分配处理器资源,Thread
类提供了 setPriority
和 getPriority
方法来更改和获取线程优先级(需要注意的是: 线程优先级不能保证线程执行的顺序,而且非常依赖于平台)。
文章作者 LYR
上次更新 2021-08-17