在 Java 编程中,多线程 是实现并发编程的重要手段。本文将详细介绍 Java 中创建线程的四种方式,并通过一个“买电影票”的实际案例,演示不加同步与加同步时的不同效果,帮助你理解多线程中的线程安全问题及解决方案。
为什么需要多线程?以一个CPU处理四个任务为例子:
1. 创建线程的四种方式
Java 提供了多种创建线程的方式,以下是常见的四种:
1.1 继承 Thread 类
通过继承 Thread
类并重写 run()
方法来创建线程。
class MyThread extends Thread {
@Override
public void run() {
System.out.println("继承 Thread 类创建线程");
}
}
// 使用
MyThread t = new MyThread();
t.start(); // 启动线程
1.2 实现 Runnable 接口
实现 Runnable
接口并通过 Thread
构造器传入该对象创建线程,这是推荐方式之一,便于资源共享。
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("实现 Runnable 接口创建线程");
}
}
// 使用
Thread t = new Thread(new MyRunnable());
t.start();
1.3 实现 Callable 接口(有返回值)
与 Runnable
不同,Callable
可以返回执行结果,并且支持抛出异常。
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
return "Callable 返回结果";
}
}
// 使用
Callable<String> callable = new MyCallable();
FutureTask<String> task = new FutureTask<>(callable);
Thread t = new Thread(task);
t.start();
System.out.println(task.get()); // 输出:Callable 返回结果
1.4 线程池创建(推荐使用)
使用 ExecutorService
创建线程池,提高线程复用效率,适用于并发量大的场景。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
ExecutorService executor = Executors.newFixedThreadPool(5); // 创建固定大小线程池
executor.submit(() -> System.out.println("线程池中的任务"));
executor.shutdown(); // 关闭线程池
2. 多线程实现买电影票案例(不加同步操作)
2.1 案例描述
模拟电影院售票系统,多个窗口同时售票,总票数为100张。
class Ticket implements Runnable {
private int ticket = 100;
@Override
public void run() {
while (true) {
if (ticket <= 0) break;
try {
Thread.sleep(10); // 模拟延迟
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "卖出了第" + ticket-- + "张票");
}
}
}
// 测试类
public class TestTicket {
public static void main(String[] args) {
Ticket ticket = new Ticket();
new Thread(ticket, "窗口A").start();
new Thread(ticket, "窗口B").start();
new Thread(ticket, "窗口C").start();
}
}
2.2 存在的问题
由于多个线程共享同一个 ticket
变量,没有进行同步控制,会出现如下问题:
- 重复卖票:同一张票被多个线程卖出;
- 超卖问题:出现负数票号;
- 数据不一致:最终输出的结果不可预测。
3. 多线程实现买电影票案例(加同步操作)
为了解决上述线程安全问题,我们需要对关键代码块进行同步控制。
3.1 synchronized 实现案例
使用 synchronized
关键字确保每次只有一个线程进入卖票逻辑。
class SynchronizedTicket implements Runnable {
private int ticket = 100;
@Override
public void run() {
while (true) {
synchronized (this) {
if (ticket <= 0) break;
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "卖出了第" + ticket-- + "张票");
}
}
}
}
// 测试类
public class TestSynchronizedTicket {
public static void main(String[] args) {
SynchronizedTicket ticket = new SynchronizedTicket();
new Thread(ticket, "窗口A").start();
new Thread(ticket, "窗口B").start();
new Thread(ticket, "窗口C").start();
}
}
✅ 优点:
- 实现简单;
- JVM 自动管理锁的获取和释放。
❌ 缺点:
- 锁粒度大,性能可能受影响;
- 不支持尝试获取锁、超时等高级功能。
3.2 Lock 接口实现案例
使用 ReentrantLock
(可重入锁)可以更灵活地控制锁,如尝试加锁、设置超时时间等。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class LockTicket implements Runnable {
private int ticket = 100;
private final Lock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
lock.lock(); // 获取锁
try {
if (ticket <= 0) break;
Thread.sleep(10);
System.out.println(Thread.currentThread().getName() + "卖出了第" + ticket-- + "张票");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock(); // 释放锁
}
}
}
}
// 测试类
public class TestLockTicket {
public static void main(String[] args) {
LockTicket ticket = new LockTicket();
new Thread(ticket, "窗口A").start();
new Thread(ticket, "窗口B").start();
new Thread(ticket, "窗口C").start();
}
}
✅ 优点:
- 更灵活,支持尝试加锁、超时、中断等;
- 显式控制锁的获取与释放,适合复杂场景。
❌ 缺点:
- 需要手动加锁和解锁,容易忘记或错误使用。
✅ 总结
特性 | synchronized | Lock |
---|---|---|
使用方式 | 关键字 | 接口 |
锁自动释放 | 是 | 否(需手动释放) |
尝试加锁 | 不支持 | 支持 |
超时机制 | 不支持 | 支持 |
中断响应 | 不支持 | 支持 |
📌 结语
多线程是 Java 并发编程的核心内容,掌握线程的创建方式和同步机制对于开发高并发程序至关重要。本文通过“买电影票”的案例详细演示了线程安全问题及其解决方法,希望对你深入理解 Java 多线程有所帮助。