Java Thread的一点总结

Java Thread的一点总结

线程适合处理不能在主线程上运行的长时间任务或者重复任务,或者使用多线程来实现并发。

创建线程

线程表示一条单独的执行流,它有自己的程序执行计数器,有自己的栈。在 Java 中创建线程有两种方式:

  • 继承 Thread 类
  • 实现 Runnable 接口

继承 Thread

@Slf4j
public class SimpleThread extends Thread{

    private final String message;

    public SimpleThread(String message) {
        this.message = message;
    }

    @Override
    public void run() {
        log.info(message);
    }
}

SimpleThread 继承了 Thread,并重写了 run 方法。 run 方法的方法签名是固定的,public,没有参数,没有返回值,不能抛出异常。run 方法类似单线程程序中的 main 方法,线程从 run 方法的第一条语句开始执行直到结束。

定义了这个类不代表代码就会开始执行,线程需要被启动,启动需要先创建一个 SimpleThread 对象,然后调用 Thread 的 start 方法

public class SingleThreadExample {
    public static void main(String[] args) {
        SimpleThread simpleThread = new SimpleThread("Hello World");
        simpleThread.start();
    }
}

在 main 方法中创建了一个线程对象,并调用了其 start 方法,调用 start 方法后,SimpleThread 的 run 方法就会开始执行。

实现 Runnable 接口与之类似,这里不再赘述。

线程的生命周期

Life cycle of a Thread in Java

一个线程从被创建开始,就会有自己的一个生命周期。

java.lang.Thread类包含了一个静态的 State 枚举,它定义了其潜在的状态。在任何给定的时间点上,线程只能处于这些状态的其中一种:

  • NEW 一个新创建但是还未执行的线程
  • RUNNABLE 正在执行或者等待资源分配后准备执行
  • BLOCKED 等待获得 a monitor lock 以进入或者重新进入一个同步块/方法
  • WAITING 在没有任何时间限制的情况下,等待其他线程执行一个特定的动作
  • TIMED_WAITING 等待其他线程在指定时间内执行一个特定的动作
  • TERMINATED 已完成执行

RUNNABLE 不代表 CPU 一定在执行该线程的代码,可能正在执行也可能在等待操作系统分配时间片,只是它没有在等待其他条件。

多线程同步

如果多个线程只关注于自己的任务,那么多线程和单线程程序都差不多,但是如果多线程之间涉及到需要访问同一个对象,那么线程之间就需要同步,以免发生意外的结果。

监视器(Monitor)是一种同步机制,允许线程之间:

  • 相互排斥(mutual exclusion)通过锁(locks),在某个时间点上只有一个线程可以执行该方法。
  • 合作(cooperation)使用 wait-set 机制使线程等待某些条件得到满足的能力

为什么被叫做 monitor 呢?Because it monitors how threads access some resources.

监视器提供三个主要功能给并发程序:

  • only one thread at a time has mutually exclusive access to a critical code section
  • threads running in a monitor could be blocked while they’re waiting for certain conditions to be met
  • one thread can notify other threads when conditions they’re waiting on are met

Java 实现 Monitors

关键部分(critical section)是指被不同的线程访问的相同数据的代码区域。

在 java 中,使用 synchronized 关键词标记关键部分(critical sections)

synchronized 可以标记类的实例方法,此时 synchronized 保护的是当前实例对象(this)。

synchronized 可以标记类的静态方法,此时 synchronized 保护的是类对象本身。

synchronized 也可以只标记一部分代码(synchronized statements)。

java 实现监视器的类比

直接放原文:

Java’s implementation of a monitor mechanism relies on two concepts – the entry set and the wait set. In literature, authors use a building and exclusive room analogy to represent the monitor mechanism. In this analogy, only one person can be present in an exclusive room at a time.

So, in this analogy:

  • the monitor is a building that contains two rooms and a hallway
  • the synchronized resource is the “exclusive room”
  • wait set is a “waiting room”
  • entry set is a “hallway”
  • threads are people who want to get to the exclusive room

When the person wants to enter the exclusive room, he first goes to the hallway (the entry set) where he waits for a scheduler. Therefore, the scheduler will pick the person and send him to the exclusive room.

Schedulers in JVMs use a priority-based scheduling algorithm. In case two threads have the same priority, the JVM uses the FIFO approach.

Hence, when the scheduler picks the person, he enters the exclusive room. It could be that some specific situation is happening in this room, so that person needs to go out and wait for the exclusive room to become available again. Therefore, that person will end up in the waiting room (the wait set). Consequently, the scheduler will schedule this person to enter an exclusive room later.

Also, it’s important to mention the steps that threads go through during this process, using the same analogy:

  • entering the building – entering the monitor
  • entering the exclusive room – acquiring the monitor
  • being in the exclusive room – owning the monitor
  • leaving the exclusive room – releasing the monitor
  • leaving the building – exiting the monitor.

Luckily, Java does most of the work in the background, and we don’t need to write semaphores when we’re dealing with multi-threaded applications. Therefore, the only thing we need to do is wrap our critical section with the synchronized keywords and it momentarily becomes a monitor region .

synchronized 实现的是监视器的相互排斥特性,wait/notify 则是实现监视器的合作特性。

线程同步(合作)

Java 的根父类是 Object,Java 在 Object 类而非 Thread 类中定义了一些线程协作的基本方法,使得每个对象都可以调用这些方法,这些方法有两类,一类是 wait,另一类是 notify 。

public final void wait() throws InterruptedException
public final native void wait(long timeout) throws InterruptedException;

wait 实际上做了什么呢?它在等待什么?每个对象都有一把锁和等待队列,一个线程在进入 synchronized 代码块时,会尝试获取锁,如果获取不到则会把当前线程加入到等待队列中,除了用于锁的等待队列,每个对象还有另一个等待队列,表示条件队列,该队列用于线程间的协作。调用 wait 就会把当前线程放到条件队列上并阻塞,表示当前线程执行不下去了,它需要等待一个条件,这条件它自己改变不了,需要其他线程改变。当其他线程改变了条件后,应该调用 Object 的 notify 方法:

public final native void notify();
public final native void notifyAll();

notify 做的事情是从条件队列中选一个线程,将其从队列中移除并唤醒,notifyAll 是移除条件队列中的所有线程并全部唤醒。

其实 Thread 类中也有使线程等待一段时间的静态方法 sleep()

sleepwait都会使线程 TIMED_WAITING ,但是 wait 会让线程释放对象锁,而 sleep 不会释放对象锁。

另外 wait/notify 方法只能在 synchronized 代码块内被调用,如果调用 wait/notify 时,当前线程没有持有对象锁,则会抛出 java.lang.IllegalMonitor-StateException 异常。

Executors 线程池

类结构关系

Executor 接口

Executor 管理多个异步任务的执行,而无需程序员显式地管理线程的生命周期。这里的异步是指多个任务的执行互不干扰,不需要进行同步操作。

ExecutorService 框架

ExecutorService 实现了线程池(Thread Pool)设计模式(也称为复制的工作者或工作者-团队模式),并负责我们上面提到的线程管理,此外它还增加了一些非常有用的功能,如线程可重用性和任务队列。

ExecutorService 继承自 Executor 接口,ExecutorService 提供了管理终止的方法,以及可为跟踪一个或多个异步任务执行状况而生成 Future 的方法。 可以关闭 ExecutorService,这将导致其停止接受新任务。关闭后,执行程序将最后终止,这时没有任务在执行,也没有任务在等待执行,并且无法提交新任务。

ScheduledExecutorService 接口

ScheduledExecutorService继承自ExecutorService接口,可安排在给定的延迟后运行或定期执行的命令。


参考资料:

How to Start a Thread in Java | Baeldung

Life Cycle of a Thread in Java | Baeldung

Guide to the Synchronized Keyword in Java | Baeldung

wait and notify() Methods in Java | Baeldung

A Guide to the Java ExecutorService | Baeldung

What is a Monitor in Computer Science? | Baeldung on Computer Science

《Java 编程的逻辑》第五部分

JUC - 类汇总和学习指南 | Java 全栈知识体系 (pdai.tech)