多线程是 Java 最基本的并发模型。而后面的 IO、网络均依赖该模型。毕竟,多线程是 Java 实现多任务的基础。
线程创建
-
继承
Thread
类/** * Thread 实体类 */ public class FirstThread extends Thread { @Override public void run() { System.out.println("Hello, Thread 01!"); } } /** * 入口 */ public class Application { public static void main(String[] args) { FirstThread t = new FirstThread(); t.start(); } }
优点:可使用
this
访问当前线程缺点:在单继承的 Java 中,继承了 Thread 类后便不能继承其他类
-
实现
Runnable
接口/** * Runnable 实体类 */ public class SecondRunnable implements Runnable { @Override public void run() { System.out.println("Hello, Runnable 02!"); } } /** * 入口 */ public class Application { public static void main(String[] args) { Thread t = new Thread(new SecondRunnable()); t.run(); } }
优点:可继承其他类
缺点:只能使用
Thread.currentThread()
方法来访问当前线程 -
实现
Callable
接口/** * Callable 实体类 */ public class ThirdCallable implements Callable<String> { @Override public String call() throws Exception { System.out.println("Hello, Callable 03!"); return "Callable result"; } } /** * 入口 */ public class Application { public static void main(String[] args) { ThirdCallable task = new ThirdCallable(); FutureTask<String> futureTask = new FutureTask<>(task); Thread t = new Thread(futureTask); t.start(); try { String result = futureTask.get(); System.out.println("The result is: " + result); } catch (ExecutionException e) { e.printStackTrace(); } catch (InterruptedException e) { e.printStackTrace(); } } }
优点:可继承其他类;可直接获得线程的运行返回值
缺点:只能使用
Thread.currentThread()
方法来访问当前线程;须与FutureTask
类结合使用,创建过程较为复杂 -
引入
Executors
线程池/** * 任务实体类 */ public class Task implements Runnable { private int id; public Task(int id) { this.id = id; } @Override public void run() { System.out.println("Hello, Task " + id + "!"); } } /** * 入口 */ public class Application { public static void main(String[] args) { // 创建固定十个大小的线程池 ExecutorService executor = Executors.newFixedThreadPool(10); for (int i = 0; i < 100; i++) { // 将 Runnable 实例提交到线程池执行 executor.submit(new Task(i)); } // 彻底关闭线程池 executor.shutdown(); } }
优点:拥有 Runnable 的全部优点;线程的创建细节交由线程池实现,实现线程的重复利用,大大提高了程序的性能;线程池可预先创建线程,从而可以快速响应并发请求;线程池控制了线程的最大数量,从而避免线程过多创建导致系统崩溃的情况
缺点:拥有 Runnable 的全部缺点;增加了程序的复杂度,也增加了排错的难度
线程启动与停止
线程启动
通过上一小节的内容,我们知道通过 Thread
创建的线程,必须使用 start()
实例方法来启动线程;而通过 Executors
创建的线程池,则只需要把 Runnable
实例提交给 ExecutorService
实例即可,无需额外调用启动方法。
线程停止
首先应该说明:run()
方法执行完毕后,线程会自然地停止。只有在该方法内部出现诸如 while (true) { ... }
这样的代码时,线程才会一直执行下去。
-
返回停止法
线程实例在外部调用
interrupt()
方法为线程打上中断状态后,在run()
方法内部可以通过isInterrupted()
判断当前线程的中断状态,如果为true
则执行return
,也可达到中断线程的效果。/** * Thread 实体类 */ public class FinalThread extends Thread { @Override public void run() { System.out.println("Start!"); while (!this.isInterrupted()) { System.out.println("Still running..."); try { Thread.sleep(100); } catch (InterruptedException e) { System.out.println("End!"); return; // 或者 break; 也可以 } } } } /** * 入口 */ public class Application { public static void main(String[] args) throws InterruptedException { FinalThread t = new FinalThread(); t.start(); Thread.sleep(1000); t.interrupt(); } }
-
标记停止法
线程实体类定义一个布尔属性
running
(使用volatile
修饰),用来标记当前线程是否退出。而run()
方法内部的业务代码放置 于while (running) { ... }
代码块内。当外部设置了running
为false
后,线程不再执行下一轮循环,从而达到停止线程的效果。/** * Thread 实体类 */ public class FinalThread extends Thread { public volatile boolean running = true; @Override public void run() { System.out.println("Start!"); while (running) { System.out.println("Still running..."); try { Thread.sleep(100); } catch (InterruptedException e) { break; } } System.out.println("End!"); } } /** * 入口 */ public class Application { public static void main(String[] args) throws InterruptedException { FinalThread t = new FinalThread(); t.start(); Thread.sleep(1000); t.running = false; } }
-
暴力停止法
直接调用
stop()
实例方法终止后续代码的执行,即使后续代码是finally
块。此方法已过期,不再推荐使用。/** * Thread 实体类 */ public class FinalThread extends Thread { @Override public void run() { System.out.println("Start!"); while (true) { System.out.println("Still running..."); } } } /** * 入口 */ public class Application { public static void main(String[] args) throws InterruptedException { FinalThread t = new FinalThread(); t.start(); Thread.sleep(50); t.stop(); t.join(); // 等待线程 t 结束后再执行后续代码 System.out.println("End!"); } }
线程状态
状态就是生命周期中某一阶段的特征。下列状态图的 t
表示线程实例,并且双向的 t
都是同一个引用。
-
NEW - 线程已创建,但未调用
start()
方法graph LR A[NEW] -- " t.start() " --> B[RUNNABLE] style A fill:#A9DEF9 style B fill:#D3F8DA
-
RUNNABLE - 线程已调用
start()
方法,但操作系统未调度该线程请注意:
- RUNNABLE 其实还可以细分为 READY 与 RUNNING,但由于这两个状态完全是由操作系统调度决定,所以这里不做更多介绍。
-
BLOCKED - 因为某些操作或外部原因而阻塞,被操作系统挂起
graph LR B[RUNNABLE] -- " 执行 synchronized 方法或块等待锁 " --> C[BLOCKED] C[BLOCKED] -- " 获得锁 " --> B[RUNNABLE] style B fill:#D3F8DA style C fill:#EDE7B1
-
WAITING - 等待中,需要另一线程执行特定的方法来唤醒
请注意:
- 调用
obj.wait()
或t.join()
或LockSupport.park()
,是调用该方法的线程进入 WAITING 状态,而非t
进入等待状态! obj.wait()
的具体作用是释放obj
的同步锁,当前线程进入等待状态;而t.join()
则是当前线程进入等待状态,直到t
线程执行完毕。obj.notify()
通常是从等待池中把最早等待obj
锁的那个线程移至锁竞争池。而obj.notifyAll()
则是把等待池中全部的线程都移至锁竞争池。如果等待池没有等待obj
锁的线程,则调用此方法没有任何效果。- 如果
t
本身已结束,则调用t.join()
会立即返回,并继续执行后续代码。 LockSupport.park()
只会令当前进程进入阻塞状态,但不会释放持有的锁。
graph LR B[RUNNABLE] -- " obj.wait() " --> D[WAITING] B[RUNNABLE] -- " t.join() " --> D[WAITING] B[RUNNABLE] -- " LockSupport.park() " --> D[WAITING] D[WAITING] -- " LockSupport.unpark(thread) " --> B[RUNNABLE] D[WAITING] -- " t 内部的 run() 执行完毕 " --> B[RUNNABLE] D[WAITING] -- " obj.notify() 或 obj.notifyAll() " --> B[RUNNABLE] style B fill:#D3F8DA style D fill:#E3D4E7
- 调用
-
TIMED_WAITING - 等待中,但超过给定时间后会自动返回就绪状态
请注意:
LockSupport.parkNanos(long)
的参数是指阻塞等待的最大时长,单位为毫秒;LockSupport.parkUntil(long)
的参数是指阻塞等待的最久时间点,即时间戳。
graph LR B[RUNNABLE] -- " obj.wait(long) " --> E[TIMED_WAITING] B[RUNNABLE] -- " t.join(long) " --> E[TIMED_WAITING] B[RUNNABLE] -- " LockSupport.parkNanos(long) 或 LockSupport.parkUntil(long) " --> E[TIMED_WAITING] E[TIMED_WAITING] -- " LockSupport.unpark(thread) " --> B[RUNNABLE] E[TIMED_WAITING] -- " t 内部的 run() 执行完毕或超时 " --> B[RUNNABLE] E[TIMED_WAITING] -- " obj.notify() 或 obj.notifyAll() " --> B[RUNNABLE] style B fill:#D3F8DA style E fill:#EAD0EE
-
TERMINATED - 线程的
run()
方法已执行结束并返回graph LR B[RUNNABLE] -- " run() 结束 " --> F[TERMINATED] style B fill:#D3F8DA style F fill:#F9CECD
守护线程
用途:某些线程在启动后需要无限循环,但当非守护线程全部结束后,JVM 需要无感知退出。
示例:
/**
* Thread 实体类
*/
public class ForeverThread extends Thread {
@Override
public void run() {
System.out.println("Start!");
while (true) {
System.out.println("Still running...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
break;
}
}
}
}
/**
* 入口
*/
public class Application {
public static void main(String[] args) throws InterruptedException {
Thread t = new ForeverThread();
t.setDaemon(true); // 只需要在 start() 方法调用前执行该方法即可
t.start();
// 执行到这里后,t 还是结束了
}
}
线程安全
如果一个类被设计为允许多线程正确访问,我们就称该类是线程安全的。而一个非线程安全的类,则由于 Java 的线程可以直接修改线程外变量的值,在多个线程同时访问时容易出现竞态现象。要消除该现象,实现多线程编程时需要做到以下三点:
- 原子性 - 提供互斥访问,即同一时刻只能由一个线程对数据进行一组操作,其他线程不能打扰
- atomic 系列类
/** * 实体类 */ public class AtomicCounter { // JVM 使用 CAS 机制确保无锁同步 private AtomicInteger n = new AtomicInteger(0); public void increment() { // 自加并返回,相当于 ++n n.incrementAndGet(); } public void decrement() { // 自减并返回,相当于 --n n.decrementAndGet(); } public int getValue() { return n.get(); } } /** * 入口 */ public class Application { public static void main(String[] args) throws InterruptedException { AtomicCounter counter = new AtomicCounter(); Thread t1 = new Thread(() -> { for (int i = 0; i < 100; i++) { counter.increment(); } }); Thread t2 = new Thread(() -> { for (int i = 0; i < 100; i++) { counter.increment(); } }); t1.start(); t2.start(); t1.join(); t2.join(); // 将输出:Result = 200 System.out.println("Result = " + counter.getValue()); } }
- synchronized 关键字
/** * 实体类 */ public class SynchronizedCounter { private int n = 0; // 被加锁的,永远是对象。当前被加锁的,就是实例本身 public synchronized void increment() { ++n; } public synchronized void decrement() { --n; } public synchronized int getValue() { return n; } } /** * 入口 */ public class Application { public static void main(String[] args) throws InterruptedException { SynchronizedCounter counter = new SynchronizedCounter(); Thread t1 = new Thread(() -> { for (int i = 0; i < 100; i++) { counter.increment(); } }); Thread t2 = new Thread(() -> { for (int i = 0; i < 100; i++) { counter.increment(); } }); t1.start(); t2.start(); t1.join(); t2.join(); // 将输出:Result = 200 System.out.println("Result = " + counter.getValue()); } }
- atomic 系列类
- 可见性 - 一个线程修改了共享变量,其他线程能立即感知该变量已被修改
- volatile 关键字
/** * 实体类 */ public class VolatileEntity { // volatile 关键字保证了访问 flag 的有序性(阻止了指令的重排) private volatile boolean flag = false; public boolean getFlag() { return flag; } public void setFlag(boolean flag) { this.flag = flag; } } /** * 入口 */ public class Application { public static void main(String[] args) { VolatileEntity entity = new VolatileEntity(); Thread t1 = new Thread(() -> { System.out.println("t1 is starting..."); while (!entity.getFlag()); System.out.println("t1 is ended."); }); Thread t2 = new Thread(() -> { try { Thread.sleep(1000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } entity.setFlag(true); }); t1.start(); t2.start(); } }
- synchronized 关键字
/** * 实体类 */ public class SynchronizedEntity { private boolean flag = false; // synchronized 确保了线程间的可见性(当某线程进入本方法,它会读取共享变量的最新值) public synchronized boolean getFlag() { return flag; } // synchronized 确保了线程间的可见性(当某线程退出本方法,它会将共享变量的更新值写回主存) public synchronized void setFlag(boolean flag) { this.flag = flag; } } /** * 入口 */ public class Application { public static void main(String[] args) { SynchronizedEntity entity = new SynchronizedEntity(); Thread t1 = new Thread(() -> { System.out.println("t1 is starting..."); while (!entity.getFlag()); System.out.println("t1 is ended."); }); Thread t2 = new Thread(() -> { try { Thread.sleep(1000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } entity.setFlag(true); }); t1.start(); t2.start(); } }
- volatile 关键字
- 有序性 - 确保重排序后的执行结果与重排序前一致
-
happens-before 原则
具体内容请查看 【后端之旅】二、Java 基础概念(下篇)之 Java 并发编程
-
线程池
在前面的线程创建部分,我们已经知道线程池为多个线程提供了一种统一的管理方式。也知道了线程的主要好处包括:
- 降低系统资源消耗
- 控制并发数量以防止服务器过载
- 便于统一管理和维护线程
线程池作为管理多个线程的载体,与线程一样,也有自身的生命周期,即不同的状态:
- RUNNING —— 线程池创建后默认处于此状态,能接受新提交的任务,并且也能处理阻塞队列中的任务。
- SHUTDOWN —— 调用
shutdown()
方法后进入此状态,不再接受新提交的任务,但可以处理存量任务。 - STOP —— 调用
shutdownNow()
方法后变为此状态,不仅不接收新任务,还会尝试中断正在执行的任务,并且不会处理尚未开始执行的任务。 - TIDYING —— 当所有的任务都已经终止并且 workerCount(活动工作线程数)为 0 时,线程池将转换到此状态,并触发
terminated()
钩子方法。 - TERMINATED ——
terminated()
方法执行完毕后,线程池最终进入此状态,表明线程池已经彻底关闭,无法再进行任何操作。
前面的代码中,我们使用了定长线程池。事实上,线程池的种类非常多,下面将介绍最常用的 5 种:
-
定长线程池
创建指定数量的线程,当所有线程全部处于活动状态时,再提交的任务会在队列中等待,直到有线程可用。特点是被创建线程会一直存在,直到线程池调用了
shutdown()
。ExecutorService es = Executors.newFixedThreadPool(10);
-
单个线程池
线程池只会用唯一的工作线程来执行任务。会确保在任何情况下都不会有超过一个任务处于活动状态。主要特点该线程池无法再通过其他方法修改线程的数量,这是与
newFixedThreadPool(1)
不同的地方。ExecutorService es = Executors.newSingleThreadExecutor();
-
无限线程池
扩容大小为 (int) 的最大值,即可理解为无限大。而具体大小则根据传递进去的线程数量决定。当然,调用
execute()
时会优先重用以前构造的线程。而被构造或释放的线程,默认 60 秒不适用即被终止和移除。ExecutorService es = Executors.newCachedThreadPool();
-
计划线程池
创建指定数量的线程,任务的运行则可以指定在延迟时间后执行,或者定期执行。而任务的添加,与其他线程池使用
submit()
方法不一样的是,计划线程池使用schedule()
方法(这样才可以设置时间参数)。空闲的线程会进行保留,直到线程池调用了shutdown()
。ScheduledExecutorService es = Executors.newScheduledThreadPool(10);
-
并行线程池
获取当前可用的线程数量进行创建作为并行级别。并行级别决定了同一时刻最多有几个线程在执行,不传参则默认为 CPU 核心数目。主要用途是处理可并行化且计算密集型的任务。当某个线程执行完自己队列中的任务后,它会尝试从其他线程的队列中“窃取”任务来执行,从而实现了负载均衡。
// Runtime.getRuntime().availableProcessors() 可查看本机 CPU 核心数目 ExecutorService es = Executors.newWorkStealingPool();
ThreadLocal
如果把线程看作一个语句块,那么,在这个块中声明局部变量就是最基本的需求。Java 使用 ThreadLocal
来将一个类的属性声明为线程的局部变量。
可把 ThreadLocal
看成一个全局的 Map<Thread, Object>
,即每个线程内部获取 ThreadLocal
变量时,永远是以 Thread
自身作为 key。而各个线程的 ThreadLocal
关联的 Object
实例互不干扰。
/**
* 抽象控制类
*/
public abstract class AbstractController implements AutoCloseable {
// request 和 response 只在所在线程可访问
// 并且被所有当前实例的方法及继承实例的方法所共享
protected ThreadLocal<HttpServletRequest> request = new ThreadLocal<>();
protected ThreadLocal<HttpServletResponse> response = new ThreadLocal<>();
public void setRequestAndResponse(HttpServletRequest request, HttpServletResponse response) {
this.request.set(request);
this.response.set(response);
}
@Override
public void close() throws Exception {
request.remove();
response.remove();
}
}
/**
* 业务控制类
*/
public class HomeController extends AbstractController {
public void getHomePage() throws IOException {
String name = request.get().getParameter("name");
String pass = request.get().getParameter("pass");
PrintWriter out = response.get().getWriter();
String result = "You cannot access home page!";
if (name.equals("ego") && pass.equals("ego")) {
result = "This is the home page!";
}
out.println(result);
out.close();
response.get().flushBuffer();
}
}
/**
* 模拟访问
*/
public class Application {
public static void main(String[] args) {
try (HomeController hc = new HomeController()) {
// TODO
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
线程销毁后,ThreadLocal
关联的实例会自动被内存回收。然而,由于我们使用线程通常都是通过创建线程池的方式。而线程在关闭后并没有释放,而是放回到线程池中了。因此,我们必须在线程关闭时清除 ThreadLocal 关联的实例,以避免数据被下一个任务读取到:
/**
* 任务实体类
*/
public class SimpleTask implements AutoCloseable {
static final ThreadLocal<String> ctx = new ThreadLocal<>();
public SimpleTask(String str) {
ctx.set(str);
}
public static String currentCtx() {
return ctx.get();
}
@Override
public void close() throws Exception {
ctx.remove();
}
}
/**
* 模拟请求入口
*/
public class Application {
public static void main(String[] args) {
MockHttpServletRequest mockRequest = new MockHttpServletRequest();
MockHttpServletResponse mockResponse = new MockHttpServletResponse();
mockRequest.addParameter("name", "ego");
mockRequest.addParameter("pass", "ego");
try (HomeController hc = new HomeController()) {
hc.setRequestAndResponse(mockRequest, mockResponse);
hc.getHomePage();
System.out.println(mockResponse.getContentAsString());
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
小结
本文简单地介绍了一些关于 Java 多线程的基础知识,包括线程的完整生命周期及其附带的各种状态,也包括线程的各种注意事项,最后聊了一些关于线程池和 ThreadLocal 的简单知识。内容不多,希望能帮助到入门的你。后续有机会,会更深入地探讨线程在计算机中的本质。