Java-线程

线程和状态机

线程和线程任务

线程任务区别于线程,可以理解为线程需要执行的逻辑,类似 Thread 中要执行的 Runnable。

前台线程(用户线程)和后台线程(守护线程)

jvm 不区分主线程和用户线程,各个线程是独立的,不同于 win32 的线程模型。jvm 把线程分为前台线程和后台线程,前台线程官方术语叫用户线程(User),后台线程则叫守护线程(Daemon),jvm 结束的条件是 所有的前台线程结束
Java 中线程分为两类,分别为 Daemon 线程(守护线程)和 User 线程(用户线程),类似于 Unix 中的用户进程和守护进程,实际上它们的区别不大,只是它们的含义不同,用户线程为用户服务,守护线程为其他线程服务。在 JVM 启动时候会调用 main 函数,main 函数所在的线程是一个用户线程,这个是我们可以看到的线程,其实 JVM 内部同时还启动了好多守护线程,比如垃圾回收线程(严格说属于 JVM 线程),并且在所有用户线程都退出后守护线程也一并退出。
守护线程和用户线程的区别:只有且仅有当最后一个用户线程结束后 JVM 会正常退出,而不管当前是否有守护线程。
正常构建的线程都是前台线程,可以在线程未开始前调用 Thread 类的 setDaemon(true)方法将线程改变为后台守护线程

1
t.setDaemon(true);

下面的例子区分了守护线程和用户线程的特点(注意不要使用 JUNIT 测试,因为 JUNIT 在主线程退出后会直接退出 JVM):

1
2
3
4
5
6
7
8
9
10
11
// 主线程退出后JVM不会退出
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
public void run() {
for(;;){}
}
});
//启动子线
thread.start();
System.out.print("main thread is over");
}

上面的例子证明主线程退出、而仍存在子线程运行时 JVM 是不会退出的。下面的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
// 主线程退出后不管守护线程是否仍在运行、直接退出JVM
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
public void run() {
for (; ; ) {
}
}
});
//启动子线
thread.setDaemon(true);
thread.start();
System.out.print("main thread is over");
}

因此,如果你想在主线程结束后 JVM 进程马上结束,那么创建线程的时候可以设置线程为守护线程,否则如果希望主线程结束后子线程继续工作,等子线程结束后在让 JVM 进程结束那么就设置子线程为用户线程。

守护线程原理

Java 中在 main 线程运行结束后,JVM 会自动启动一个叫做 DestroyJavaVM 线程,该线程会等待所有用户线程结束后终止 JVM 进程。
翻开 JVM 的代码,最终会调用到 JavaMain 这个函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
int JNICALL
JavaMain(void * _args)
{
...
//执行Java中的main函数
(*env)->CallStaticVoidMethod(env, mainClass, mainID, mainArgs);

//main函数返回值
ret = (*env)->ExceptionOccurred(env) == NULL ? 0 : 1;

//等待所有非守护线程结束,然后销毁JVM进程
LEAVE();
}

LEAVE 是 C 语言里面的一个宏定义,定义如下:

1
2
3
4
5
6
7
8
9
10
11
#define LEAVE() \
do { \
if ((*vm)->DetachCurrentThread(vm) != JNI_OK) { \
JLI_ReportErrorMessage(JVM_ERROR2); \
ret = 1; \
} \
if (JNI_TRUE) { \
(*vm)->DestroyJavaVM(vm); \
return ret; \
} \
} while (JNI_FALSE)

上面宏的作用实际是创建了一个名字叫做 DestroyJavaVM 的线程来等待所有用户线程结束。

线程上下文切换

在多线程编程中,线程个数一般都大于 CPU 个数,而每个 CPU 同一时刻只能被一个线程使用,为了让用户感觉多个线程是在同时执行,CPU 资源的分配采用了时间片轮转的策略,也就是给每个线程分配一个时间片,在时间片内占用 CPU 执行任务。当前线程的时间片使用完毕后当前就会处于就绪状态并让出 CPU 让其它线程占用,这就是上下文切换,从当前线程的上下文切换到了其它线程。
那么就有一个问题让出 CPU 的线程等下次轮到自己占有 CPU 时候如何知道之前运行到哪里了?所以在切换线程上下文时候需要保存当前线程的执行现场,当再次执行时候根据保存的执行现场信息恢复执行现场。

线程上下文切换时机

  • 当前线程的 CPU 时间片使用完毕处于就绪状态时候;
  • 当前线程被其它线程中断时候。

线程上下文切换开销问题

由于线程切换是有开销的,所以并不是开的线程越多越好,比如如果机器是 4 核心的,你开启了 100 个线程,那么同时执行的只有 4 个线程,这 100 个线程会来回切换线程上下文来共享这四个 CPU。

Java 如何执行线程任务

这个问题其实和一个经典面试题很像——如何创建线程?
其实 Java 中创建线程的方式只有一种,就是new Thread,其他的所谓创建线程都是指的如何调度线程,包括 Runnable、Future、Callable,及各种线程池 ExecutorService、ForkJoinPool 等。

Runnable 和 Thread

使用 Runnable 比直接使用 Thread 更加灵活:

  • Thread 继承的方式下,run()内获取当前线程可以直接使用 this,但是 Java 不支持多重继承,如果继承了 Thread 就不能再继承其他类了,且任务与代码没有分离,当多个线程执行一样的任务时需要实例化多个继承的线程类。
  • 实现 Runnable 的方式下,任务与代码分离,run()内获取当前线程必须使用 Thread.currentThread()。

Callable 和 Future

Java5 使用 Callable 来执行逻辑、返回线程执行结果,且支持声明抛出异常,Future 接口是 Callable 的执行器,可以获取 Callable 中 call()的返回值,它有一个实现类 FutureTask:

  • 可以提供给 Thread 调度,因为 FutureTask 实现了 Runnable 接口;
  • 控制关联的 Callable,比如 calcel()可以取消 Callable 任务、get()可以获取 call()方法的返回值(阻塞直到 call()返回)。

Java 如何实现多线程

Java 并不依靠 JVM 实现多线程,Thread 的start0()方法是一个 native 方法,这意味着线程的执行是平台相关的。
JVM 需要通过操作系统内核中的 TCB(Thread Control Block)模块来改变线程的状态,这一过程需要耗费一定的 CPU 资源。

Java 中在 main 线程运行结束后,JVM 会自动启动一个叫做 DestroyJavaVM 线程,该线程会等待所有用户线程结束后终止 JVM 进程,下面是 JVM 中的相关代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
int JNICALL
JavaMain(void * _args)
{
...
//执行Java中的main函数
(*env)->CallStaticVoidMethod(env, mainClass, mainID, mainArgs);

//main函数返回值
ret = (*env)->ExceptionOccurred(env) == NULL ? 0 : 1;

//等待所有非守护线程结束,然后销毁JVM进程
LEAVE();
}

LEAVE 是 C 语言里面的一个宏定义,定义如下:

1
2
3
4
5
6
7
8
9
10
11
#define LEAVE() \
do { \
if ((*vm)->DetachCurrentThread(vm) != JNI_OK) { \
JLI_ReportErrorMessage(JVM_ERROR2); \
ret = 1; \
} \
if (JNI_TRUE) { \
(*vm)->DestroyJavaVM(vm); \
return ret; \
} \
} while (JNI_FALSE)

上面宏的作用实际是创建了一个名字叫做 DestroyJavaVM 的线程来等待所有用户线程结束。

在 Tomcat 的 NIO 实现 NioEndpoint 中会开启一组接受线程用来接受用户的链接请求和一组处理线程负责具体处理用户请求,那么这些线程是用户线程还是守护线程呢?下面我们看下 NioEndpoint 的 startInternal 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public void startInternal() throws Exception {
if (!running) {
running = true;
paused = false;
...

//创建处理线程
pollers = new Poller[getPollerThreadCount()];
for (int i=0; i<pollers.length; i++) {
pollers[i] = new Poller();
Thread pollerThread = new Thread(pollers[i], getName() + "-ClientPoller-"+i);
pollerThread.setPriority(threadPriority);
pollerThread.setDaemon(true);//声明为守护线程
pollerThread.start();
}
//启动接受线程
startAcceptorThreads();
}
...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
protected final void startAcceptorThreads() {
int count = getAcceptorThreadCount();
acceptors = new Acceptor[count];

for (int i = 0; i < count; i++) {
acceptors[i] = createAcceptor();
String threadName = getName() + "-Acceptor-" + i;
acceptors[i].setThreadName(threadName);
Thread t = new Thread(acceptors[i], threadName);
t.setPriority(getAcceptorThreadPriority());
t.setDaemon(getDaemon());//设置是否为守护线程,默认为守护线程
t.start();
}
}

private boolean daemon = true;
public void setDaemon(boolean b) { daemon = b; }
public boolean getDaemon() { return daemon; }

如上代码也就是说默认情况下接受线程和处理线程都是守护线程,这意味着当 Tomact 收到 shutdown 命令后 Tomact 进程会马上消亡,而不会等处理线程处理完当前的请求。

有限状态机(FSM)

有限状态机也称为 FSM(Finite State Machine),是表示有限个状态以及在这些状态之间的转移和动作等行为的数学模型。FSM 可以把模型的多状态、多状态建的转换条件解耦。可以使维护变得容易,代码也更加具有可读性。
有限状态机的4要素

要素

状态机可归纳为 4 个要素:现态、条件、动作、次态。

  • 现态:指当前流程所处的状态,包括起始、中间、终结状态。
  • 条件:也可称为事件;当一个条件被满足时,将会触发一个动作并执行一次状态的迁移。
  • 动作:当条件满足后要执行的动作。动作执行完毕后,可以迁移到新的状态,也可以仍旧保持原状态。
  • 次态:当条件满足后要迁往的状态。“次态”是相对于“现态”而言的,“次态”一旦被激活,就转变成新的“现态”了。

状态

状态表示流程中的持久状态,流程图上的每一个圈代表一个状态。

  • 初始状态: 流程开始时的某一状态;
  • 中间状态: 流程中间过程的某一状态;
  • 终结状态: 流程完成时的某一状态。

使用建议:

  • 状态必须是一个持久状态,而不能是一个临时状态;
  • 终结状态不能是中间状态,不能继续进行流程流转;
  • 状态划分合理,不要把多个状态强制合并为一个状态;
  • 状态尽量精简,同一状态的不同情况可以用其它字段表示。

动作

动作的三要素:角色、现态、次态,流程图上的每一条线代表一个动作。

  • 角色: 谁发起的这个操作,可以是用户、定时任务等;
  • 现态: 触发动作时当前的状态,是执行动作的前提条件;
  • 次态: 完成动作后达到的状态,是执行动作的最终目标。

使用建议:

  • 每个动作执行前,必须检查当前状态和触发动作状态的一致性;
  • 状态机的状态更改,只能通过动作进行,其它操作都是不符合规范的;
  • 需要添加分布式锁保证动作的原子性,添加数据库事务保证数据的一致性;
  • 类似的动作(比如操作用户、请求参数、动作含义等)可以合并为一个动作,并根据动作执行结果转向不同的状态。

线程状态

线程状态流转

  • 创建(NEW):实例化线程对象,此时还没有调用 start 执行线程。
  • 就绪(RUNNABLE):调用了线程的 start,此时调度器还没来得及给线程分配时间片,线程还处于就绪队列中。
  • 运行(RUNNABLE):线程调度程序将处于就绪状态的线程设置为当前线程,此时线程就进入了运行状态,开始运行 run 函数当中的代码。
  • 阻塞:线程正在运行的时候,被暂停,通常是为了等待某个时间的发生(比如说某项资源就绪)之后再继续运行。sleep、suspend、wait 等方法都可以导致线程阻塞。阻塞分三种情况:
    1. 等待阻塞(WAITING):调用 wait,让线程等待某工作的完成后调用 notify 通知;
    2. 同步阻塞(BLOCKED):synchronized 获取监视器锁失败,进入同步阻塞状态,直到其他线程放开监视器锁;
    3. 其他阻塞(TIMED_WAITING):sleep、join 或发出了 IO 请求时,线程阻塞,直到 sleep 超时、join 等待线程终止 / 超时、或 IO 处理完毕,线程重新进入就绪状态。
  • 死亡(TERMINATED):一个线程的 run 方法执行结束或者调用 stop 方法后,该线程就会死亡。对于已经死亡的线程,无法再使用 start 方法令其进入就绪。

Java 线程状态和操作系统线程状态并不是一一映射的,这些状态是由 JVM 维护的,并不是调磁盘 IO 系统调用线程就进入 BLOCKED 状态。
Java 中线程没有 RUNNING 状态,RUNNABLE 映射了操作系统的 READY、RUNNING 和 WAITING 三个状态,实际上 RUNNABLE 表示线程正在 Java 虚拟机中执行,但它可能正在等待来自操作系统的其他资源,比如处理器、硬盘、网卡等。

线程状态模拟

  • RUNNABLE
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    @Test
    public void testInBlockedIOState() throws InterruptedException {
    Scanner in = new Scanner(System.in);
    // 创建一个名为“输入输出”的线程t
    Thread t = new Thread(new Runnable() {
    @Override
    public void run() {
    try {
    // 命令行中的阻塞读
    String input = in.nextLine();
    System.out.println(input);
    } catch (Exception e) {
    e.printStackTrace();
    } finally {
    if (in != null) {
    in.close();
    }
    }
    }
    }, "输入输出"); // 线程的名字

    // 启动
    t.start();

    // 确保run已经得到执行
    Thread.sleep(1000);

    // 状态为RUNNABLE
    assertThat(t.getState(), IsEqual.equalTo(State.RUNNABLE));
    }
    @Test
    public void testBlockedSocketState() throws Exception {
    Thread serverThread = new Thread(new Runnable() {
    @Override
    public void run() {
    ServerSocket serverSocket = null;
    try {
    serverSocket = new ServerSocket(10086);
    while (true) {
    // 阻塞的accept方法
    Socket socket = serverSocket.accept();
    }
    } catch (IOException e) {
    e.printStackTrace();
    } finally {
    try {
    serverSocket.close();
    } catch (IOException e) {
    e.printStackTrace();
    }
    }
    }
    }, "socket线程"); // 线程的名字
    serverThread.start();

    // 确保run已经得到执行
    Thread.sleep(500);

    // 状态为RUNNABLE
    assertThat(serverThread.getState(), IsEqual.equalTo(Thread.State.RUNNABLE));
    }
  • WAITING 状态
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    @Test
    public void testInWaiting() throws InterruptedException {
    Object lock = new Object();
    Thread t = new Thread(new Runnable() {
    @Override
    public void run() {
    try {
    synchronized (lock) {
    lock.wait();
    System.out.println("wait finished");
    }
    } catch (Exception e) {
    e.printStackTrace();
    }
    }
    }, "等待"); // 线程的名字

    // 启动
    t.start();

    // 确保run已经得到执行
    Thread.sleep(1000);

    // 状态为RUNNABLE
    assertThat(t.getState(), IsEqual.equalTo(State.WAITING));
    synchronized (lock) {
    lock.notify();
    }
    }

线程调度方法

Thread 类有许多方法用于调度线程:
sleep(睡眠) 使线程进入阻塞状态,直到睡眠时间结束
wait(等待) 使线程进入等待状态,直到别的线程调用锁定对象的 notify()或 notifyall()方法
yield(让步) 提醒线程调度器给别的线程分配更多时间
join(加入) 若调用自身的 join(),则等待其他线程终止;若调用了别的对象的 join(),则当前线程进入阻塞状态,直到另一个线程运行结束。
notify(唤醒) 唤醒此对象监视器上等待的一个线程
interrupt(中断) 中断使线程离开阻塞状态,并准备下一次运行
runstart:run 定义了线程的执行逻辑,是由用户定义的,是一个回调函数,当调用 start 方法后并没有立刻执行而是处于就绪状态,这个就绪状态是指该线程已经获取了除 CPU 资源外的其它资源,等获取 CPU 资源后才会真正处于运行状态,这个资源一般指由操作系统分配的 CPU 时间片,当 run 方法执行完毕后,该线程就处于终止状态了。

监视器锁

在具体探究线程状态流转前,首先需要明确一个监视器锁(monitor)的概念,每个对象(包括 Class)都持有一个 monitor 锁,因为每个对象都属于共享资源,多线程读写一个对象的属性,必然面临并发问题。

线程的终止

有三种方式:stop、interrupt 和设置条件变量。

  1. stop
    不推荐
  2. interrupt
    1
    t.interrupt();
  3. 设置条件变量
    创建线程任务
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    @Data
    public class StopTest implements Runnable {
    boolean isStop;

    @Override
    public void run() {
    int count = 0;
    while(! isStop) {
    System.out.println("running...");
    }
    System.out.println("stop...");
    }
    }
    当想要终止该线程时,设置条件变量为 true
    1
    2
    3
    4
    5
    StopTest s = new StopTest();
    Thread t = new Thread(s);
    t.start();
    Thread.sleep(100);
    s.setStop(true);

wait / notify 线程等待与通知

  • 在调用具体共享对象的 wait 或者 notify 系列函数前要先获取共享对象的锁;
  • notify 和 notifyAll 的区别;
  • 由于线程虚假唤醒的存在,一定要使用循环检查的方式。

当一个线程调用一个共享对象的 wait() 方法时候,调用线程会被阻塞挂起,直到下面几个事情之一发生才返回:

  1. 其它线程调用了该共享对象的 notify() 或者 notifyAll() 方法;
  2. 其它线程调用了该线程的 interrupt() 方法设置了该线程的中断标志,该线程会抛出 InterruptedException 异常返回。

wait / notify 方法签名:

  • void wait()
  • void wait(long timeout)
    该方法相比 wait() 方法多一个超时参数,不同在于如果一个线程调用了共享对象的该方法挂起后,如果没有在指定的 timeout ms 时间内被其它线程调用该共享变量的 notify() 或者 notifyAll() 方法唤醒,那么该函数还是会因为超时而返回。
    需要注意的是如果在调用该函数时候 timeout 传递了负数会抛出 IllegalArgumentException 异常。
  • void wait(long timeout, int nanos)
    内部是调用 wait(long timeout),如下代码:只是当 nanos>0 时候让参数一递增 1。
  • void notify()
    一个线程调用共享对象的 notify() 方法后,会唤醒一个在该共享变量上调用 wait 系列方法后被挂起的线程,一个共享变量上可能会有多个线程在等待,具体唤醒哪一个等待的线程是随机的。
    另外被唤醒的线程不能马上从 wait 返回继续执行,它必须获取了共享对象的监视器后才可以返回,也就是唤醒它的线程释放了共享变量上面的监视器锁后,被唤醒它的线程也不一定会获取到共享对象的监视器,这是因为该线程还需要和其它线程一块竞争该锁,只有该线程竞争到了该共享变量的监视器后才可以继续执行。
    类似 wait 系列方法,只有当前线程已经获取到了该共享变量的监视器锁后,才可以调用该共享变量的 notify() 方法,否者会抛出 IllegalMonitorStateException 异常。
  • void notifyAll()
    不同于 nofity() 方法在共享变量上调用一次就会唤醒在该共享变量上调用 wait 系列方法被挂起的一个线程,notifyAll() 则会唤醒所有在该共享变量上由于调用 wait 系列方法而被挂起的线程。

可以通过 synchronized 关键字获取监视器锁,需要注意,如果调用 wait() 方法的线程没有事先获取到该对象的监视器锁,则调用 wait() 方法时候调用线程会抛出 IllegalMonitorStateException 异常。

例 1 - 获取监视器锁:

1
2
3
4
5
6
7
8
9
// 使用同步代码块
synchronized(共享变量){
//doSomething
}

// 使用同步方法
synchronized void add(int a,int b){
//doSomething
}

另外需要注意的是一个线程可以从挂起状态变为可以运行状态(也就是被唤醒)即使该线程没有被其它线程调用 notify(),notifyAll() 进行通知,或者被中断,或者等待超时,这就是所谓的虚假唤醒
虽然虚假唤醒在应用实践中很少发生,但是还是需要防范于未然的,做法就是不停的去测试该线程被唤醒的条件是否满足,不满足则继续等待,也就是说在一个循环中去调用 wait() 方法进行防范,退出循环的条件是条件满足了唤醒该线程。

例 2 - 防止虚假唤醒:

1
2
3
4
5
synchronized (obj) {
while (条件不满足){
obj.wait();
}
}

如上代码为经典的调用共享变量 wait() 方法的实例,首先通过同步块获取 obj 上面的监视器锁,然后通过 while 循环内调用 obj 的 wait() 方法。

另外当一个线程调用了共享变量的 wait() 方法后该线程会被挂起,同时该线程会暂时释放对该共享变量监视器的持有,直到另外一个线程调用了共享变量的 notify() 或者 notifyAll() 方法才有可能会重新获取到该共享变量的监视器的持有权(这里说有可能,是因为考虑到多个线程第一次都调用了 wait() 方法,所以多个线程会竞争持有该共享变量的监视器)。

例 3 - 生产者消费者:
下面从生产者消费者例子来加深理解,如下面代码是一个生产者的例子,其中 queue 为共享变量,生产者线程在调用 queue 的 wait 方法前,通过使用 synchronized 关键字拿到了该共享变量 queue 的监视器,所以调用 wait() 方法才不会抛出 IllegalMonitorStateException 异常,如果当前队列没有空闲容量则会调用 queued 的 wait() 挂起当前线程,这里使用循环就是为了避免上面说的虚假唤醒问题,这里假如当前线程虚假唤醒了,但是队列还是没有空余容量的话,当前线程还是会调用 wait() 把自己挂起。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
//生产线程
synchronized (queue) {

//消费队列满,则等待队列空闲
while (queue.size() == MAX_SIZE) {
try {
//挂起当前线程,并释放通过同步块获取的queue上面的锁,让消费线程可以获取该锁,然后获取队列里面元素
queue.wait();
} catch (Exception ex) {
ex.printStackTrace();
}
}

//空闲则生成元素,并通知消费线程
queue.add(ele);
queue.notifyAll();
}
//消费线程
synchronized (queue) {

//消费队列为空
while (queue.size() == 0) {
try
//挂起当前线程,并释放通过同步块获取的queue上面的锁,让生产线程可以获取该锁,生产元素放入队列
queue.wait();
} catch (Exception ex) {
ex.printStackTrace();
}
}

//消费元素,并通知唤醒生产线程
queue.take();
queue.notifyAll();
}

借上述代码来说明下对调用共享变量 wait() 方法后当前线程会释放持有的共享变量的锁的理解。假如生产线程 A 首先通过 synchronized 获取到了 queue 上的锁,那么其它生产线程和所有消费线程都会被阻塞,线程 A 获取锁后发现当前队列已满会调用 queue.wait() 方法阻塞自己,然后会释放获取的 queue 上面的锁,这里考虑下为何要释放该锁?如果不释放,由于其它生产线程和所有消费线程已经被阻塞挂起,而线程 A 也被挂起,这就处于了死锁状态。这里线程 A 挂起自己后释放共享变量上面的锁就是为了打破死锁必要条件之一的持有并等待原则。关于死锁下面章节会有讲到,线程 A 释放锁后其它生产线程和所有消费线程中会有一个线程获取 queue 上的锁进而进入同步块,这就打破了死锁。

例 4 - InterruptedException:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class WaitNotifyInterupt {

static Object obj = new Object();

public static void main(String[] args) throws InterruptedException {

//创建线程
Thread threadA = new Thread(new Runnable() {
public void run() {
try {
System.out.println("---begin---");
//阻塞当前线程
obj.wait();
System.out.println("---end---");

} catch (InterruptedException e) {
e.printStackTrace();
}
}
});

threadA.start();

Thread.sleep(1000);

System.out.println("---begin interrupt threadA---");
threadA.interrupt();
System.out.println("---end interrupt threadA---");
}
}

输出:
---begin---
Exception in thread "Thread-0" java.lang.IllegalMonitorStateException
at java.lang.Object.wait(Native Method)
at java.lang.Object.wait(Object.java:502)
at com.tallate.localcache.WaitNotifyInterupt$1.run(WaitNotifyInterupt.java:15)
at java.lang.Thread.run(Thread.java:745)
---begin interrupt threadA---
---end interrupt threadA---

如上代码 threadA 调用了共享对象 obj 的 wait()方法后阻塞挂起了自己,然后主线程在休眠 1s 后中断了 threadA 线程,可知中断后 threadA 在 obj.wait() 处抛出了 java.lang.IllegalMonitorStateException 异常后返回后终止。

例 5 - notify() 和 notifyAll()的用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
private static volatile Object resourceA = new Object();

public static void main(String[] args) throws InterruptedException {

// 创建线程
Thread threadA = new Thread(new Runnable() {
public void run() {

// 获取resourceA共享资源的监视器锁
synchronized (resourceA) {

System.out.println("threadA get resourceA lock");
try {

System.out.println("threadA begin wait");
resourceA.wait();
System.out.println("threadA end wait");

} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});

// 创建线程
Thread threadB = new Thread(new Runnable() {
public void run() {

synchronized (resourceA) {
System.out.println("threadB get resourceA lock");
try {

System.out.println("threadB begin wait");
resourceA.wait();
System.out.println("threadB end wait");

} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

});

// 创建线程
Thread threadC = new Thread(new Runnable() {
public void run() {

synchronized (resourceA) {

System.out.println("threadC begin notify");
// 如果有多个线程在等待,则notify只能唤醒其中一个,而notifyAll能唤醒全部
// resourceA.notify();
resourceA.notifyAll();
}
}
});

// 启动线程
threadA.start();
threadB.start();

// 等待一会,让线程 A 和 B 全部执行到调用 wait 方法后在调用线程 C 的 notify 方法
Thread.sleep(1000);
threadC.start();

// 等待线程结束
threadA.join();
threadB.join();
threadC.join();
System.out.println("main over");
}

输出:
threadA get resourceA lock
threadA begin wait
threadB get resourceA lock
threadB begin wait
threadC begin notify
threadB end wait
threadA end wait
main over

从结果来看,这次线程调度器先调度了线程 A 占用 CPU 来运行,线程 A 首先获取了 resourceA 上的锁,然后调用 resourceA 的 wait()方法挂起当前线程并释放获取到的锁,然后线程 B 获取到 resourceA 上面的锁并调用了 resourceA 的 wait(),此时线程 B 也被阻塞挂起并释放 resourceA 上的锁。
注意线程 C 中的 notify 和 notifyAll,如果调用了 notify() 方法,则会激活 resourceA 的阻塞集合里面的一个线程,如果是 notifyAll 则会激活所有,只是线程 B 先获取到了 resourceA 上面的锁然后从 wait()方法返回,待 B 执行完毕后,线程 A 又获取到 resourceA 上面的锁,然后从 wait()方法返回,等 A 也执行完毕后,由主线程打印结果。

线程优先级

setPriority()/getPriority()
设置/获取线程优先级,Runnable 的多个线程中优先级高的会被线程调度器优先分配时间片

join 等待线程执行终止

在项目实践时候经常会遇到一个场景,就是需要等待某几件事情完成后才能继续往下执行,比如多个线程去加载资源,当多个线程全部加载完毕后在汇总处理,Thread 类中有个静态的 join 方法就可以做这个事情。
前面介绍的等待通知方法是属于 Object 类的,而 join 方法则是直接在 Thread 类里面提供的,下面简单介绍其使用方法:

例 1 - join 的简单示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public static void main(String[] args) throws InterruptedException {
Thread threadOne = new Thread(new Runnable() {

@Override
public void run() {

try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}

System.out.println("child threadOne over!");

}
});

Thread threadTwo = new Thread(new Runnable() {

@Override
public void run() {

try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}

System.out.println("child threadTwo over!");


}
});

//启动子线程
threadOne.start();
threadTwo.start();

System.out.println("wait all child thread over!");

//等待子线程执行完毕,返回
threadOne.join();
threadTwo.join();

System.out.println("all child thread over!");

}

例 2 - 线程 join()时被 interrupt 会抛出 InterruptedException:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
public static void main(String[] args) throws InterruptedException {

//线程one
Thread threadOne = new Thread(new Runnable() {

@Override
public void run() {

System.out.println("threadOne begin run!");
for (;;) {
}

}
});
//获取主线程
final Thread mainThread = Thread.currentThread();

//线程two
Thread threadTwo = new Thread(new Runnable() {

@Override
public void run() {
//休眠1s
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//中断主线程
mainThread.interrupt();

}
});

// 启动子线程
threadOne.start();

// 延迟1s启动线程
threadTwo.start();


try { //等待线程one执行结束
threadOne.join();

} catch(InterruptedException e){
System.out.println("main thread:" + e);
}
}

sleep 线程睡眠

  • sleep 会让调用线程暂时让出指定时间的 CPU 执行权;
  • 但是该线程所拥有的监视器资源,比如锁还是持有不让出的。

当一个执行中的线程调用了 Thread 的 sleep 方法后,调用线程会暂时让出指定时间的执行权,也就是这期间不参与 CPU 的调度,但是该线程所拥有的监视器资源,比如锁还是持有不让出的。当指定的睡眠时间到了该函数会正常返回,线程就处于就绪状态,然后参与 CPU 的调度,当获取到了 CPU 资源就可以继续运行了。
如果在睡眠期间其它线程调用了该线程的 interrupt() 方法中断了该线程,该线程会在调用 sleep 的地方抛出 InterruptedException 异常返回。

例 1 - 线程 sleep 时不会释放监视器锁:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class SynchronizedTest {

private static final Object lock = new Object();

public static void main(String[] args) {
Thread t1 = new Thread(() -> {
synchronized (lock) {
try {
System.out.println("-> t1");
Thread.sleep(10000);
System.out.println("<- t1");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});

Thread t2 = new Thread(() -> {
synchronized (lock) {
try {
System.out.println("-> t2");
Thread.sleep(10000);
System.out.println("<- t2");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});

t1.start();
t2.start();
}
}

输出:
-> t1
<- t1
-> t2
<- t2

要么是线程 1 先获取到锁,要么是线程 2,获取锁后调用 sleep 挂起,此时不会释放锁,体现到输出里就是二者不会出现交叉打印的情况。

interrupt 线程中断

  • 中断一个线程仅仅是设置了该线程的中断标志,也就是设置了线程里面的一个变量的值,本身是不能终止当前线程运行的。
  • 一般程序里面是检查这个标志的状态来判断是否需要终止当前线程。

Java 中线程中断是一种线程间协作模式,通过设置线程的中断标志并不能直接终止该线程的执行,而是需要被中断的线程根据中断状态自行处理。

  • void interrupt()
    中断线程,例如当线程 A 运行时,线程 B 可以调用线程 A 的 interrupt() 方法来设置线程 A 的中断标志为 true 并立即返回。interrupt 仅仅是设置标志,线程 A 并没有实际被中断,会继续往下执行。如果线程 A 因为调用了 wait 系列函数或者 join 方法或者 sleep 函数而被阻塞挂起,这时候线程 B 调用了线程 A 的 interrupt() 方法,线程 A 会在调用这些方法的地方抛出 InterruptedException 异常而返回。
  • boolean isInterrupted()
    检测当前线程是否被中断,如果是返回 true,否者返回 false。
  • boolean interrupted()
    检测当前线程是否被中断,如果是返回 true,否者返回 false,与 isInterrupted 不同的是该方法如果发现当前线程被中断后会清除中断标志。
    并且,该函数是 static 方法,可以通过 Thread 类直接调用。

例 1 - 使用 Interrupted 优雅退出:

1
2
3
4
5
6
7
8
9
10
11
12
13
public void run(){    
try{
....
//线程退出条件,需要额外判断线程是否被中断
while(!Thread.currentThread().isInterrupted() && more work to do) {
// do more work;
}
} catch (InterruptedException e) {
// thread was interrupted during sleep or wait
} finally {
// cleanup, if required
}
}

QA

  • 什么是线程?线程和进程的关系。
  • 线程几种状态之间的转换
  • 线程之间如何协调
  • 线程创建与运行,创建一个线程有哪几种方式?有何区别?
  • 线程安全问题
  • 线程通知与等待,多线程同步的基础设施。
  • 线程的虚假唤醒,以及如何避免。
  • 等待线程执行终止的 join 方法。想让主线程在子线程执行完毕后在做一点事情?
  • 让线程睡眠的 sleep 方法,sleep 的线程会释放持有的监视器锁?
  • 线程中断。中断一个线程,被中断的线程会自己终止?
  • 理解线程上下文切换。线程多了一定好?
  • 线程死锁,以及如何避免。
  • 守护线程与用户线程。当 main 函数执行完毕,但是还有用户线程存在的时候,JVM 进程会退出?
  1. 今有线程执行 synchronized(this),另一线程后到,他们分别属于什么状态?
  2. 今有线程调用 interrupt(),它属于什么状态?
  3. 当线程 wait()时,也可以通过 interrupt()中断,此时它不会马上抛出异常,而是会先获取锁,在得到锁后再抛出异常
  4. 什么情况下会发生虚假唤醒?
  5. stop 为什么被 Deprecated 了?