Java 线程

并发处理的广泛应用是人类“压榨”计算机运算能力的最有力武器。但是如果“管理“不好,高并发也是人类被计算机反杀的武器。

并发分为多进程并发和多线程并发。

进程 VS 线程

一个应用程序可以包含多个进程,一个进程可以包含多个线程。

进程是系统进行资源分配的基本单位。进程之间的地址空间相互独立,不同进程之间的资源是不共享的。

  • 优点:使多个程序可并发执行,提高系统的资源利用率和吞吐量。
  • 缺点:系统创建进程的开销比线程大,多进程间通信复杂。

线程是系统进行任务调度的基本单元(按代码顺序执行下来,执行完毕就结束)。它是进程内相对独立的可执行单元,与父进程中的其它兄弟线程共享该父进程所拥有的全部资源(代码空间和全局变量),但拥有独立的栈(即局部变量对于线程来说是私有的)。

  • 优点:减少程序在并发执行时所付出的时空开销,提高操作系统的并发性能。系统创建线程的开销比进程小。

线程的分类

线程可分为 CPU 线程 和 操作系统线程:

  • CPU 线程:多核 CPU 的每个核各自独立运行,因此每个核一个线程。(「四核八线程」:CPU 硬件级别支持一核多线程。)
  • 操作系统线程:操作系统通过时间分片方式,把 CPU 中的多个任务随机交替执行。

线程又可分为 守护线程、非守护线程(用户线程)。守护线程是守护用户线程的线程,即在程序运行时为其他线程提供一种服务。如:垃圾回收线程。

线程的调度

线程调度是指系统为线程分配处理器使用权的过程,主要调度方式有两种,分别是【协同式线程调度】和 【抢占式线程调度】。

协同式:

  • 优点:实现简单,而且由于线程要把自己的事情干完后才会进行线程切换,切换操作对线程自己是可知的,所以没有什么线程同步的问题。
  • 缺点:线程执行时间不可控制,甚至如果一个线程编写有问题,一直不告知系统进行线程切换,那么程序就会一直阻塞在那里。

抢占式:每个线程将由系统来分配执行时间,线程的切换不由线程本身来决定(在Java中,Thread.yield()可以让出执行时间,但是要获取执行时间的话,线程本身是没有什么办法的)。

Java 使用的线程调度方式就是抢占式调度。每个线程都有机会获得 CPU 的资源以便进行自身的线程操作;当线程使用 CPU 资源的时间结束时,即使线程没有完成自己的全部操作,JVM 也会中断当前线程的执行,把 CPU 资源的使用权切换给队列中下一个等待的线程。被中断的线程将等待 CPU 资源的下一次轮回,然后从中断处继续执行。

Java 虚拟机(JVM)中的线程调度器负责管理线程,并根据以下规则进行调度:

a. 根据线程优先级(由高到低),将 CPU 资源分配给各线程

b. 具备相同优先级的线程以轮流的方式获取 CPU 资源

比如: 存在 A、B、C、D 四个线程,其中:A 和 B 的优先级高于 C 和 D(A、B 同级,C、D 同级),那么 JVM 将先以轮流的方式调度 A、B,直到 A、B 线程结束,再以轮流的方式调度 C、D。

由于各平台线程优先级的差异以及优先级可能会被系统自行改变等原因, Java 线程的优先级是不靠谱的,不能通过优先级来完全准确的判断一组线程,哪个被先执行。

Java 中线程的 5 种状态及转换

Java 定义了 5 种线程状态,在任意一个时间点,一个线程只能是其中的一种状态,这 5 种状态分别如下:

  • NEW:还没有启动的线程。
  • RUNNABLE:处于此状态的线程有可能正在运行,也有可能正在等待着 CPU 为它分配执行时间。因此这个状态包括了操作系统线程状态中的 Running 和 Ready。
  • BLOCKED:处于此状态的线程被阻塞了。当前线程要么正在等待监视器锁,以进入同步块或同步方法;要么因为调用了 wait() 方法,等待重新进入同步块/方法。
  • WAITING:处于这种状态的线程不会被分配 CPU 执行时间,要等待被其他线程显式地唤醒。以下方法会让线程陷入无限期的等待状态:1. 没有设置 Timeout 参数的 Object.wait() 方法;2.没有设置 Timeout 参数的 Thread.join() 方法;3. LockSupport.park() 方法。
  • TIMED_WAITING:处于这种状态的线程也不会被分配 CPU 执行时间,不过无须等待被其他线程显式地唤醒,在一定时间之后会由系统自动唤醒。以下方法会让线程进入限期等待状态:1. Thread.sleep() 方法;2. 设置了 Timeout 参数的 Object.wait() 方法;3. 设置了 Timeout 参数的 Thread.join() 方法;4. LockSupport.parkNanos() 方法;5. LockSupport.parkUntil() 方法。
  • TERMINATED:已终止线程的线程状态,线程已经结束执行。

阻塞状态和等待状态看上去很像,有什么区别呢?

阻塞状态在等待获取一个排他锁;而等待状态则是在等待一段时间或被唤醒。

5 种线程状态的转换关系如下图:

Java 中线程的实现

对于 Sun JDK 来说,它的 Windows 版与 Linux 版都是使用一对一的线程模型实现的,一条 Java 线程就映射到一条轻量级进程之中,因为 Windows 和 Linux系统提供的线程模型就是一对一的。

而在 Solaris 平台中,由于操作系统的线程特性可以同时支持一对一及多对多的线程模型,因此在 Solaris 版的 JDK 中也对应提供了两个平台专有的虚拟机参数来明确指定虚拟机使用哪种线程模型。

线程联合 Thread.join()

线程 A 在占有 CPU 资源期间,通过调用线程 B 的 join() 方法中断自身线程执行,然后运行联合它的线程 B,直到线程 B 执行完毕后线程 A 再重新排队等待 CPU 资源,这个过程称为线程 A 联合线程 B。

Thread 除了提供 join() 方法之外,还提供了 join(long millis) 和 join(long millis, int nanos) 两个具备超时特性的方法。这两个超时方法表示,如果线程在给定的超时时间里没有终止,那么将会从该超时方法中返回。

线程中断

许多声明抛出 InterruptedException 的方法(例如 Thread. sleep(longmillis))在抛出 InterruptedException 之前,Java 虚拟机会先将该线程的中断标识位清除,然后抛出 InterruptedException,此时调用 isInterrupted() 方法将会返回 false。

废弃的 suspend、resume 和stop 方法

以 suspend() 方法为例,在调用后,线程不会释放已经占有的资源(比如锁),而是占有着资源进入睡眠状态,这样容易引发死锁问题。同样,stop() 方法在终结一个线程时不会保证线程的资源正常释放,通常是没有给予线程完成资源释放工作的机会,因此会导致程序可能工作在不确定状态下。

正因为这些方法带来的副作用,这些方法才被标注为不建议使用的废弃方法,而暂停和恢复操作可以用【等待/通知机制】来替代。

等待/通知机制

方法描述
notify()通知一个在该对象上等待的线程,使其从wait() 方法返回,返回的前提是该线程获取到了对象的锁。
notifyAll()通知所有等待在该对象上的线程。
wait()调用该方法的线程进入 WAITING 状态,只有等待另外线程的通知或中断才能返回,需要注意:调用 wait() 方法后,当前线程会释放该对象的锁
wait(long)超过等待一段时间,这里的参数时间是毫秒,也就是等待长达 n 毫秒,如果没有被通知就超时返回
wait(long, int)对于超时时间更细粒度的控制,可以达到纳秒。

标准的 wait 使用方式:

Object obj = new Object();
synchronized (obj) { // 1. 获取锁
    while(条件不符合) { // 2. 条件不满足,wait(),被通知后仍要检查条件。
        obj.wait();
    }
    3. 条件满足时对应的处理逻辑
}

标准的 notify/notifyAll 使用方式:

Object obj = new Object();
synchronized (obj) { // 1. 获取锁
    2. 改变条件,使等待中的线程满足检查条件
    obj.notify()/notifyAll(); // 3. 最好使用 notifyAll 通知等待在对象上的所有线程。
}

注意事项:

1) 使用 wait()、notify() 和 notifyAll() 时需要先对调用对象加锁。
2) 调用 wait() 方法后,线程状态由 RUNNING 变为 WAITING,并将当前线程放置到对象的等待队列。
3) notify() 或 notifyAll() 方法调用后,等待线程依旧不会从 wait() 返回,需要调用 notify() 或 notifyAll() 的线程释放锁之后,等待线程才有机会从 wait() 返回。
4) notify()方法将等待队列中的一个等待线程从等待队列中移到同步队列中,而notifyAll()方法则是将等待队列中所有的线程全部移到同步队列,被移动的线程状态由 WAITING 变为 BLOCKED。
5) 从 wait() 方法返回的前提是获得了调用对象的锁。

从上述细节中可以看到,等待/通知机制依托于同步机制,其目的就是确保等待线程从 wait() 方法返回时能够感知到通知线程对变量做出的修改。

如 Android 中的 HandlerThread 类中 getLooper() 和 run() 方法就是一个很好的等待/通知机制的示例:

Looper mLooper;

public Looper getLooper() {
    synchronized (this) {
        while (isAlive() && mLooper == null) {
            try {
                wait();
            } catch (InterruptedException e) {
            }
        }
    }
    return mLooper;
}

public void run() {
    ...
    synchronized (this) {
        mLooper = Looper.myLooper();
        notifyAll();
    }
    ...
}

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注