实现 Runnable 接口
public class Ticket implements Runnable {
private int totalTickets = 10; // 总票数
@Override
public void run () {
while (true) {
synchronized (this) { // 同步锁,保证同一时间只有一个线程抢票
if (totalTickets <= 0) break;
System.out.println (Thread.currentThread ().getName () + "抢到第" + totalTickets + "张票");
totalTickets--;
}
try {
Thread.sleep (100); // 模拟抢票延迟
} catch (InterruptedException e) {
e.printStackTrace ();
}
}
}
public static void main (String [] args) {
Ticket ticket = new Ticket ();
// 创建 3 个线程模拟 3 个用户抢票
new Thread (ticket, "用户 A").start ();
new Thread (ticket, "用户 B").start ();
new Thread (ticket, "用户 C").start ();
}
}

实现 callable 接口(有返回值可抛出异常)
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
public class TicketCallable implements Callable<String> {
private int totalTickets = 10;
@Override
public String call () throws Exception {
while (true) {
synchronized (this) {
if (totalTickets <= 0) {
return Thread.currentThread ().getName () + "没抢到票,票已售罄";
}
String result = Thread.currentThread ().getName () + "抢到第" + totalTickets + "张票";
totalTickets--;
Thread.sleep (100);
return result; // 返回抢票结果
}
}
}
public static void main (String [] args) throws Exception {
TicketCallable ticket = new TicketCallable ();
// 用 FutureTask 包装 Callable,才能传给 Thread
FutureTask<String> task1 = new FutureTask<>(ticket);
FutureTask<String> task2 = new FutureTask<>(ticket);
FutureTask<String> task3 = new FutureTask<>(ticket);
new Thread (task1, "用户 A").start ();
new Thread (task2, "用户 B").start ();
new Thread (task3, "用户 C").start ();
// 获取每个线程的抢票结果
System.out.println (task1.get ());
System.out.println (task2.get ());
System.out.println (task3.get ());
}
}

这里打印顺序不是按照 10-9-8,是和调度顺序有关,同一时刻只有 1 个线程进入同步代码块,抢到了票,但是没有来得及打印,cpu 的时间片就用完了。
这背后其实是FutureTask的工作机制在起作用。当我们提交Callable任务时,每个任务都会被包装成一个FutureTask。FutureTask在执行时,虽然内部也会竞争锁,但它还有一个额外的步骤,就是要保存任务的返回结果。这个保存结果的操作,加上FutureTask本身的状态管理,会引入一些微小的延迟。这些延迟会让各个线程的执行节奏变得更不一致,从而增加了打印顺序混乱的概率。
线程池
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class TicketThreadPool {
private int totalTickets = 10;
public static void main (String [] args) {
TicketThreadPool ticket = new TicketThreadPool ();
// 创建一个固定大小为 3 的线程池,对应 3 个抢票用户
ExecutorService executor = Executors.newFixedThreadPool (3);
// 提交 3 个抢票任务给线程池
for (int i = 0; i < 3; i++) {
executor.submit (() -> {
while (true) {
synchronized (ticket) {
if (ticket.totalTickets <= 0) break;
System.out.println (Thread.currentThread ().getName () + "抢到第" + ticket.totalTickets + "张票");
ticket.totalTickets--;
}
try {
Thread.sleep (100);
} catch (InterruptedException e) {
e.printStackTrace ();
}
}
});
}
// 关闭线程池
executor.shutdown ();
}
}

业务逻辑和线程池解耦,
比如以后修改抢票逻辑,线程池的代码就不用修改。在实际生产中直接在一个服务类或者工具类里,直接创建线程池即可,嵌入到合适的业务流程中就行。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class TicketWithThreadPool {
public static void main (String [] args) {
// 创建 Ticket 实例,包含抢票逻辑
Ticket ticketTask = new Ticket ();
// 创建固定大小为 3 的线程池
ExecutorService executor = Executors.newFixedThreadPool (3);
// 提交 3 个抢票任务,注意这里提交的是同一个 Ticket 实例
// 因为所有线程需要共享同一份票数,所以不能 new 三个 Ticket
for (int i = 0; i < 3; i++) {
executor.submit (ticketTask);
}
// 任务提交完毕后关闭线程池
executor.shutdown ();
}
}

阿里军规-推荐使用 newFixedThreadPool
阿里巴巴的 Java 开发手册里确实有相关的建议。它之所以推荐用newFixedThreadPool这类封装好的方法,而不是让大家直接去 new ThreadPoolExecutor传一堆参数,主要就是为了避免踩坑。因为线程池的核心参数,比如核心线程数、最大线程数、存活时间、阻塞队列类型等,组合起来非常复杂,一旦设置不当,就可能导致内存泄漏、线程耗尽或者任务堆积等问题。对于大部分业务场景来说,newFixedThreadPool已经足够用了,它的线程数量固定,不会无限创建线程,相对更安全。