相关概念

  1. 死锁
  2. 活锁

那是不是所有的代码都需要认真分析一遍是否存在这三个问题呢?当然不是,其实只有一种情况需要:存在共享数据并且该数据会发生变化,通俗地讲就是有多个线程会同时读写同一数据。那如果能够做到不共享数据或者数据状态不发生变化,不就能够保证线程的安全性了嘛。有不少技术方案都是基于这个理论的,例如线程本地存储(Thread Local Storage,TLS)、不变模式等等,后面我会详细介绍相关的技术方案是如何在 Java 语言中实现的。

但是,现实生活中,必须共享会发生变化的数据,这样的应用场景还是很多的。

有时线程虽然没有发生阻塞,但仍然会存在执行不下去的情况,这就是所谓的“活锁”。可以类比现实世界里的例子,路人甲从左手边出门,路人乙从右手边进门,两人为了不相撞,互相谦让,路人甲让路走右手边,路人乙也让路走左手边,结果是两人又相撞了。这种情况,基本上谦让几次就解决了,因为人会交流啊。可是如果这种情况发生在编程世界了,就有可能会一直没完没了地“谦让”下去,成为没有发生阻塞但依然执行不下去的“活锁”。

解决“活锁”的方案很简单,谦让时,尝试等待一个随机的时间就可以了。例如上面的那个例子,路人甲走左手边发现前面有人,并不是立刻换到右手边,而是等待一个随机的时间后,再换到右手边;同样,路人乙也不是立刻切换路线,也是等待一个随机的时间再切换。由于路人甲和路人乙等待的时间是随机的,所以同时相撞后再次相撞的概率就很低了。“等待一个随机时间”的方案虽然很简单,却非常有效,Raft 这样知名的分布式一致性算法中也用到了它。

线程的几种状态

线程是一个动态执行的过程,它也有一个从产生到死亡的过程,在 Java 中一个线程完整的生命周期一共包含以下五种状态: 新建状态(New) 当使用 new 关键字和 Thread 类或其子类创建一个线程对象后,那么线程就进入了新建状态,此时它和其它的 Java 对象一样,仅仅由 JVM 分配了内存,并初始化其成员变量值,它会一直保持这个状态直到调用该对象的 start 方法。

就绪状态(Runnable) 当线程对象调用了 start 方法之后,该线程就进入了就绪状态。就绪状态的线程会放在一个就绪队列中,等待 JVM 里的调度器进行调度。处于就绪状态的线程,随时可能被 CPU 调度执行。

运行状态(Running) 如果就绪状态的执行被 CPU 调度执行,就可以执行 run 方法,此时线程就处于线程状态。处于运行状态的线程最复杂,它可以变为阻塞状态就绪状态死亡状态。需要注意一点,线程变为运行状态之前的状态只能是就绪状态

阻塞状态(Blocked) 线程变为阻塞状态是因为某种原因放弃 CPU 的使用权,暂时停止运行,如果执行了 sleepsuspend 等方法,释放了所占用的资源之后,线程就从运行状态进入阻塞状态。等待睡眠时间结束或者获得设备资源之可以重新进入就绪状态。阻塞可以分为以下三种:

  1. 等待阻塞 处于运行状态的线程调用wait方法,会使线程进入等待阻塞状态
  2. 同步阻塞 当线程获取 synchronized 同步锁因为同步锁被其他线程占用而失败后,会使线程进入同步阻塞
  3. 其它阻塞 通过调用线程的sleep join发出了 I/O 请求时,线程就会进入到阻塞状态。当sleep状态超时,join等待线程终止或超时,或者 I/O 处理完毕,线程重新回到就绪状态

死亡状态(Dead) 一个处于运行状态的线程执行完了 run 方法或者因为其它终止条件发生时,线程就会进入到死亡状态,该线程结束生命周期。 以上线程各种状态的流转用一张图表示如下:

thread-state-transfer.png

方法 描述
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 类提供了 setPrioritygetPriority 方法来更改和获取线程优先级(需要注意的是: 线程优先级不能保证线程执行的顺序,而且非常依赖于平台)。