多线程基础

前言

最近在准备面试,所以重新复习下多线程基础方面的知识,后续还会跟进JUC包下常用的类等。多线程基础方面知识也好久没看了,这边学习了廖雪峰老师的课,在这做下笔记,下文大多都是引用廖雪峰老师的笔记,记录一些平时比较模糊的地方。

多线程基础

特别注意:直接调用Thread实例的run()方法是无效的。
可以对线程设置优先级Thread.setPriority(int n) //1~10, 默认值5。优先级高的线程被操作系统调度的优先级较高,操作系统对高优先级线程可能调度更频繁,但我们绝不能通过设置优先级来确保高优先级的线程一定会先执行。


线程状态

在Java程序中,一个线程对象只能调用一次start()方法来启动新线程,并在新线程中执行run()方法。一旦run()方法执行完毕,线程就结束了。因此Java线程的状态有下面几种

  1. New:新创建的线程,尚未执行。
  2. Runnable:运行中的线程,正在执行run()方法的Java代码。
  3. Block:运行中的线程,因为某些操作被阻塞而挂起。
  4. Waiting:运行中的线程,因为某些操作在等待中。
  5. Timed Waiting:运行中的线程,因为执行sleep()方法正在计时等待。
  6. Terminated:线程已终止,因为run()

当线程启动后,他可以在RunnableBlockWaitingTimed Waiting这几个状态之间切换,直到最后变成Terminated状态线程终止。

线程终止的原因有:

  1. 线程正常终止:run()方法执行到return语句返回;
  2. 线程以外终止:run()方法因为未捕获的异常导致线程终止;
  3. 对某个线程的Thread实例调用stop()方法强制终止(强烈不推荐使用)。

一个线程还可以等待另一个线程知道运行结束。例如main线程在启动a线程后,可以通过a.join()等待a线程结束后再继续运行。

如果a线程已经结束,对实例a调用a.join()会立刻返回。此外,join(long)的重载方法也可以指定一个等待时间,超过等待时间后就不再继续等待。

1
2
3
4
5
6
7
8
9
10
11
public class JoinTest {
public static void main(String[] args) throws InterruptedException {
Thread a = new Thread(() -> {
System.out.println("thead-a");
});
System.out.println("start");
a.start();
a.join();
System.out.println("end");
}
}
1
2
3
start
hello
end

线程中断

中断一个线程非常简单,只需要在其他线程中对目标线程调用interrupt()方法,目标线程需要反复检测自身状态是否是interrupted状态,如果是,就立刻结束运行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Main {
public static void main(String[] args) throws InterruptedException {
Thread t = new MyThread();
t.start();
Thread.sleep(1); // 暂停1毫秒
t.interrupt(); // 中断t线程
t.join(); // 等待t线程结束
System.out.println("end");
}
}

class MyThread extends Thread {
public void run() {
int n = 0;
while (!isInterrupted()) {
n ++;
System.out.println(n + " hello!");
}
}
}
1
2
1 hello!
end

上述代码中,main线程通过调用t.interrupt()方法中断t线程,但是要注意,interrupt()方法仅仅向t线程发出了“中断请求”,至于t线程是否能立刻响应,要看具体代码。而t线程的while循环会检测isInterrupted(),所以上述代码能正确响应interrupt()请求,使得自身立刻结束运行run()方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Main {
public static void main(String[] args) throws InterruptedException {
HelloThread t = new HelloThread();
t.start();
Thread.sleep(1);
t.running = false; // 标志位置为false
}
}

class HelloThread extends Thread {
public volatile boolean running = true;
public void run() {
int n = 0;
while (running) {
n ++;
System.out.println(n + " hello!");
}
System.out.println("end!");
}
}
1
2
3
4
1 hello!
2 hello!
3 hello!
end!

中断线程的第二种方法就是,通过标记位来进行中断。
注意到HelloThread的标志位boolean running是一个线程间共享的变量。线程间共享变量需要使用volatile关键字标记,确保每个线程都能读取到更新后的变量值。

为什么要对线程间共享的变量用关键字volatile声明?这涉及到Java的内存模型。在Java虚拟机中,变量的值保存在主内存中,但是,当线程访问变量时,它会先获取一个副本,并保存在自己的工作内存中。如果线程修改了变量的值,虚拟机会在某个时刻把修改后的值回写到主内存,但是,这个时间是不确定的!

volatile关键字的主要就是通知JVM:

  1. 每次访问变量时,总是获取主内存的最新值。
  2. 每次修改变量后,立刻回写到主内存中。

volatile关键字解决的是可见性问题:当一个线程修改了某个共享变量的值,其他线程能够立刻看到修改后的值。

如果我们去掉volatile关键字,运行上述程序,发现效果和带volatile差不多,这是因为在x86的架构下,JVM回写主内存的速度非常快,但是,换成ARM的架构,就会有显著的延迟。


守护线程

Java程序入口是main线程,main线程又能启动其他线程,只有当所有线程都运行结束的时候,JVM才会退出,如果一个线程没有退出,JVM进程就不会退出,所以,必须保证所有线程都能及时结束。

当一个死循环的线程被创建,说名这个线程不能被结束。那JVM进程就无法结束,但谁负责结束这个线程呢。
我们可以将这个线程设置为守护线程,因为所有非守护线程执行完毕之后,无论有没有守护线程,虚拟机都会自动退出。

1
2
3
Thread a = new TestThread();
a.setDaemon(true);
a.start();

我们可以在线程start()前设置setDaemon(true)可以将该线程标志为守护线程。

特别注意:守护线程不能持有需要关闭的资源,因为JVM退出时,守护线程没有任何机会来关闭资源,很有可能导致数据丢失。