Java 线程基础
多用线程池少用 Thread 这个类,下面是各个版本的线程池:
- < Java 5:Thread、Runnable
- Java 5:Executor、Future、Callable
- Java 7:ForkJoin
- Java 8:CompletionStage、CompletionFuture
- Java 9:Flow
Java 中线程的生命周期和线程的状态可以理解为:在其生命周期中,线程会经历各种状态。
类似于下面这张图是最常见的:

使用 new Thread() 创建出来的线程就是 NEW 状态,这个时候线程并没有启动;当调用 start() 方法时,线程状态就会变成 RUNNABLE 状态。但是要注意,RUNNABLE 状态又分为两种,一种是准备运行(I/O 阻塞),另一种是运行。
当 t1 获得锁,t2 会等待 t1 释放锁,这个时候 t2 线程的状态就是 BLOCKED 阻塞状态。
当调用了线程的 Object#wait()、Thread#join() 或 LockSupport#park() 方法时,就会变成 WAITTING 状态;如果调用的是带有超时参数的就会变成 TIMED_WAITTING 状态。
线程执行结束就是 TERMINATED 状态。
在 Java 中线程一共就分为 6 种状态并且可以通过 state() 方法查看线程状态。
创建线程
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Thread run");
}
});
上面是 Java 创建一个线程的例子,下面代码展示了这个线程具体是怎么创建的,Thread 构造方法最终都会调用 init 方法。
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
/* 每一个线程必须有一个名字,默认的线程名称是:Thread- 加上线程ID。
线程ID 是存放在 threadInitNumber 变量中,每次创建 Thread 实例时,都会加1。*/
if (name == null) {
throw new NullPointerException("name cannot be null");
}
this.name = name;
/* 假设在线程 A 中,调用了 new Thread 创建了线程 B。
这里获取线程 A 是为了,将线程 A 的一些配置传递给线程 B。*/
Thread parent = currentThread();
SecurityManager security = System.getSecurityManager();
/* 每一个线程必须有一个线程组,如果没有指定则会先获取安全管理器中的线程组,否则再回去父级(线程A)线程组。 */
if (g == null) {
if (security != null) {
g = security.getThreadGroup();
}
if (g == null) {
g = parent.getThreadGroup();
}
}
/* 判断当前线程(线程A) 有没有操作线程组的权限,如果没有就会抛出 SecurityException。
因为 Thread#start() 方法会操作线程组。*/
g.checkAccess();
if (security != null) {
/* 主要用来判断子类有没有重写 getContextClassLoader 和 setContextClassLoader 方法,如果重写了则返回 true。*/
if (isCCLOverridden(getClass())) {
/* 既然重写了那么就判断一下有没有重写权限(enableContextClassLoaderOverride),如果没有则抛出 SecurityException。*/
security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
}
}
/* 将线程组中的 未启动线程 变量加 1。*/
g.addUnstarted();
this.group = g;
this.daemon = parent.isDaemon();
this.priority = parent.getPriority();
/* 如果重写了就调用子类的 getContextClassLoader() 方法,否则就直接获取父类的 ClassLoader 就可以了。*/
if (security == null || isCCLOverridden(parent.getClass()))
this.contextClassLoader = parent.getContextClassLoader();
else
this.contextClassLoader = parent.contextClassLoader;
/* 我也不清楚为什么要获取这个。*/
this.inheritedAccessControlContext = acc != null ? acc : AccessController.getContext();
/* 我也不清楚为什么要获取这个。*/
this.target = target;
/* 设置线程优先级。*/
setPriority(priority);
/* 继承父级的 ThreadLocal.ThreadLocalMap。
当然自己的线程数据可以放到 ThreadLocal.ThreadLocalMap threadLocals 中。*/
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
this.stackSize = stackSize;
tid = nextThreadID();
}
下面是简化的实例化流程:
- 设置线程名
- 如果没有指定线程组,则会先获取安全管理器中的线程组,否则再回去父级线程组
- 将
ThreadGroup#nUnstartedThreads变量加 1 - 设置当前线程实例的变量 daemon、priority 和 contextClassLoader 的值为父级的值。
- 关联 Runnable 实例
- 调用 setPriority0 本地方法设置优先级
- 判断需不需要传递(默认需要传递) ThreadLocal
- 设置线程栈大小
- 最后设置线程ID
终止一个线程
Thread#stop() 方法已经不推荐使用了,但可以使用 Thread#interrupt() 方法停止一个线程。
但是当调用目标线程的 interrupt() 方法后只会将目标线程打上中断标记,而目标线程并不会立即终止。也就是说,只是一个中断标记,JVM 不会强制要求终止线程。下面就是一个不会立即终止的例子:
public class ThreadWithoutIntterupt {
public static void main(String[] args) {
Thread t = new Thread(new PrintEvenNumbers());
t.start();
// 调用目标线程 t 的 interrupt() 方法。
t.interrupt();
}
}
class PrintEvenNumbers implements Runnable {
@Override
public void run() {
System.out.println("Printing even numbers till 20 :");
for (int i = 2; i <= 20; i = i + 2) {
System.out.println(i);
}
}
}
上面这个例子中,目标线程并没有立即终止,而是正常停止(打印完了所有偶数)。也就是说,终止目标线程的执行,必须要目标线程自己终止,这样才能保证一致性。目标线程可以使用 Thread#interrupted() 方法来判断是否要终止自身。将 PrintEvenNumbers 类改成如下:
class PrintEvenNumbers implements Runnable {
@Override
public void run() {
System.out.println("Printing even numbers till 20 :");
for (int i = 2; i <= 20; i = i + 2) {
System.out.println(i);
if (Thread.interrupted()) {
System.out.println("保证数据一致性,并终止线程。");
return;
}
}
}
}
值得注意的是:当调用
Thread.interrupted()方法时,会清除中断标记(也就是再次设置为 false),再次调用时该方法会返回 false。但是调用t1.isInterrupted()方法则不会清除中断标记。
还有一点就是无论是否先调用了目标线程的 interrupt() 方法,Object#wait(...) Thread#join(...) Thread#sleep(...) 都会抛出 InterruptedException 并且会清空 interrupt 状态(只要 interrupt 状态是 true,这三个方法就会抛出异常)。如果是 I/O 操作的话则会抛出 ClosedByInterruptException 异常。
class PrintEvenNumbers implements Runnable {
@Override
public void run() {
System.out.println("Printing even numbers till 20 :");
try {
Thread.sleep(10_000);
} catch (InterruptedException e) {
throw new InternalError("Thraed is interrupted");
}
}
}
值得注意的是:如果 interrupt() 方法在 start() 方法之前调用则无效。
join
在当前线程中调用目标线程的 join(...) 方法时,当前线程进入等待状态,直到目标的线程终止、 join 超时或其它线程调用了当前线程的 interrupt() 方法,当前线程才会继续执行。
public class Main {
public static void main(String[] args) throws InterruptedException {
Thread t2 = new SampleThread(1);
t2.start();
System.out.println("Invoking join");
t2.join();
System.out.println("Returned from join");
System.out.println(t2.isAlive());
}
}
class SampleThread extends Thread {
public int processingCount = 0;
SampleThread(int processingCount) {
this.processingCount = processingCount;
System.out.println("Thread Created");;
}
@Override
public void run() {
System.out.println("Thread " + this.getName() + " started");
while (processingCount > 0) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
System.out.println("Thread " + this.getName() + " interrupted");
}
processingCount--;
}
System.out.println("Thread " + this.getName() + " exiting");;
}
}
下面是一个带有超时时间的 join 代码示例:
Thread t2 = new SampleThread(10);
t2.start();
System.out.println("Invoking join");
t2.join(1000);
System.out.println("Returned from join");
System.out.println(t2.isAlive());
值得注意的是:join(...) 方法满足 Happens-before 关系。
wait 和 notify
当我们调用 wait() 方法时,会使当前线程进行等待并释放锁,直到其他的某个线程在同一对象上调用 notify()、notifyAll() 或 interrupt() 方法为止。
notify() 和 notifyAll() 方法的区别在于,前者只会随机唤醒其中一个等待的线程,而后者会唤醒所有等待的线程。注意:被唤醒的线程有可能不能立即执行,因为这些线程还会争夺锁。
值得注意的是:notify() 和 wait() 方法只能从同步方法或同步代码块中被调用。另外,wait 方法也可以设置超时时间,当设置超时时间为 wait(0) 时与调用 wait() 的作用是相同的。
下面是一个使用例子,首先创建一个 Data 类,该类由将要从 Sender 发送到 Receiver 的数据包组成。我们将使用 wait() 和 notifyAll() 在它们之间建立同步:
public class Data {
private String packet;
// True if receiver should wait
// False if sender should wait
private boolean transfer = true;
public synchronized void send(String packet) {
while (!transfer) {
try {
wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
Log.error("Thread interrupted", e);
}
}
transfer = false;
this.packet = packet;
notifyAll();
}
public synchronized String receive() {
while (transfer) {
try {
wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
Log.error("Thread interrupted", e);
}
}
transfer = true;
notifyAll();
return packet;
}
}
在 wait 写在 while 循环中是为了防止还没有满足条件的时候,线程就被唤醒了并得到错误的结果。
讨论
1. 为什么要弃用 stop()、suspend()、resume() 方法?
当调用 Thread#stop() 方法时会导致立即释放掉它持有的锁(这是不安全操作),并且还会抛出 ThreadDeath 异常。
Thread t1 = new Thread(() -> {});
t1.stop(); // t1 这个线程会立即释放掉它持有的锁
假设,同步代码块中一共有五行代码,t1 线程执行到第三行的时候就被 t2 线程调用了 t1.stop() 方法,这时 t1 会立即释放掉锁,那么 t2 所看到的数据就有可能会不一致(也可以说这些数据是被毁坏的)。
suspend() 和 resume() 方法必须要配合使用,如果 resume() 在 suspend() 之前执行会导致死锁,所以一定要保证顺序;还有就是 suspend() 会挂起线程,但是不会释放锁,所以也会导致死锁。
还有一点要注意,虽然可以使用 wait() 和 notify() 代替 suspend() 和 resume(),但是:
Thread t1 = new Thread(() -> {});
// 可以用在没有同步的地方使用
t1.suspend();
t1.resume();
synchronized (Object.class) {
// 必须要在同步的地方使用
Object.class.wait();
Object.class.notify();
}
这里有产生了另一个问题,为什么 wait 和 notify 要放到同步代码块或同步方法中。有关这个为题可以阅读 Java Monitor。
2. wait & join
wait 和 join 之间的一个最重要的区别就是 wait 要写在同步代码块或同步方法中,否则就会抛出 IllegalMonitorStateException。其他的就是用途不一样,wait 用于线程间的通信,而 join 可以控制线程执行顺序;再者他们两个都可以通过 interrupt 方法来中断,并且都有代超时参数的重载。
还有就是 join 方法也是靠 wait 实现的:
public final synchronized void join(long millis) throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (millis == 0) {
// 如果线程还在活动,则返回true。这里写入 while 循环,是为了防止没有满足条件就被唤醒了。
while (isAlive()) {
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
总结
对于 Thread 类中,主要方法 interrupt、wait、notify、notifyAll、join。
还有就是建议使用 TimeUnit sleep 代替 Thread sleep 方法
知识拓展-Runnable 接口
Runnable 这个接口可以理解成线程的任务接口,它里面只有一个无参的 run 方法,线程启动后就会执行这个方法中的代码。
还可以使,Thread 实例通过另一个 Thread 的构造方法进行传递。
知识拓展-FutureTask 配合 Thread
FutureTask 能够接收 Callable 类型的参数,用来处理又返回结果的情况:
// 创建任务对象
FutureTask<Integer> task = new FutureTask<>(() -> {
System.out.println("hello");
return 100;
});
// 参数1 是任务对象;参数2 是线程名字,推荐
new Thread(task, "tk").start();
// 主线程阻塞,同步等待 task 执行完毕的结果。
Integer result = task.get();
System.out.printf("结果是: %d\n", result);
知识拓展-主线程与守护线程
默认情况下,Java进程需要等待所有线程都运行结束,才会结束。有一种特殊的线程叫做守护线程,只要其它非守护线程运行结束了,就是守护线程的代码没有执行完,也会强制结束。
// 设置 t1 线程为守护线程,需要在 start 之前设置。
t1.setDaemon(true);
参考资料
Life Cycle of a Thread in Java
Java Thread Primitive Deprecation

浙公网安备 33010602011771号