Java多线程:线程创建、安全、同步与线程池
目录
- Java多线程:线程创建、安全、同步与线程池
- 1. 什么是多线程?
- 2. 线程创建和启动
- 2.1 方式一:继承Thread类
- 2.2 方式二:实现Runnable接口
- 2.3 方式三:实现Callable接口(带返回值)
- 2.4 三种创建方式的对比
- 3. Thread的常用构造器和方法
- 3.1 常用构造器
- 3.2 常用方法及注意事项
- 4. 线程安全
- 4.1 什么是线程安全?
- 4.2 线程安全问题出现的原因
- 4.3 模拟线程安全问题(售票系统超卖)
- 5. 线程同步(解决线程安全问题)
- 5.1 认识线程同步
- 5.2 同步代码块(显式指定锁对象)
- 5.3 同步方法(隐式锁对象)
- 5.4 Lock锁(JDK 5+,显式锁)
- 6. 线程池
- 6.1 认识线程池
- 6.2 创建线程池:ThreadPoolExecutor
- 6.3 常用方法
- 6.4 线程池注意事项
- 6.4.1 什么时候创建临时线程?
- 6.4.2 什么时候拒绝新任务?
- 6.4.3 任务拒绝策略
- 6.4.4 参数选择配置公式
- 6.5 Executors工具类(不推荐)
- 7. 并发和并行的概念
Java多线程:线程创建、安全、同步与线程池
1. 什么是多线程?
多线程是指在一个程序中同时运行多个独立的执行流(线程),共享同一进程的资源(如内存空间),但各自拥有独立的执行栈和程序计数器。
- 生活类比:一家餐厅(进程)有多个服务员(线程)同时为顾客服务,共享餐厅的资源(厨房、餐具),但各自处理不同的订单。
- 核心优势:提高程序执行效率(如后台下载文件时不阻塞UI操作)、充分利用CPU资源。
2. 线程创建和启动
Java提供三种创建线程的方式,各有优缺点,适用于不同场景。
2.1 方式一:继承Thread类
步骤:
- 定义类继承
Thread
,重写run()
方法(线程执行体); - 创建线程对象,调用
start()
方法启动线程(注意:直接调用run()
不会启动新线程,只是普通方法调用)。
代码案例:
// 1. 自定义线程类,继承Thread
class MyThread extends Thread {// 2. 重写run()方法:线程要执行的任务@Overridepublic void run() {for (int i = 0; i < 3; i++) {// Thread.currentThread().getName():获取当前线程名称System.out.println(Thread.currentThread().getName() + "执行:" + i);}}
}public class ThreadDemo {public static void main(String[] args) {// 3. 创建线程对象MyThread t1 = new MyThread();MyThread t2 = new MyThread();// 4. 设置线程名称(可选)t1.setName("线程A");t2.setName("线程B");// 5. 启动线程(底层调用start0()本地方法,由JVM创建新线程并执行run())t1.start(); t2.start();// 注意:主线程任务应放在启动子线程之后,避免主线程先执行完System.out.println("主线程执行完毕");}
}
执行结果(线程调度顺序不确定,每次运行可能不同):
主线程执行完毕
线程A执行:0
线程B执行:0
线程A执行:1
线程B执行:1
线程A执行:2
线程B执行:2
注意事项:
- 不能多次调用
start()
:一个线程对象只能启动一次,重复调用会抛出IllegalThreadStateException
。 start()
vsrun()
:start()
会启动新线程并异步执行run()
;直接调用run()
会在当前线程同步执行,无多线程效果。
2.2 方式二:实现Runnable接口
步骤:
- 定义类实现
Runnable
接口,重写run()
方法; - 创建
Runnable
实现类对象,作为参数传入Thread
构造器; - 调用
Thread
对象的start()
方法启动线程。
优势:避免单继承限制(一个类可实现多个接口),适合多线程共享资源场景。
代码案例:
// 1. 实现Runnable接口
class MyRunnable implements Runnable {@Overridepublic void run() {for (int i = 0; i < 3; i++) {System.out.println(Thread.currentThread().getName() + "执行:" + i);}}
}public class RunnableDemo {public static void main(String[] args) {// 2. 创建Runnable对象(任务)MyRunnable task = new MyRunnable();// 3. 将任务交给Thread线程对象Thread t1 = new Thread(task, "线程C"); // 直接指定线程名称Thread t2 = new Thread(task); t2.setName("线程D");// 4. 启动线程t1.start();t2.start();}
}
简化写法:
- 匿名内部类:无需单独定义类,直接在
Thread
构造器中实现Runnable
。
Thread t = new Thread(new Runnable() {@Overridepublic void run() {System.out.println("匿名内部类线程执行");}}, "匿名线程");t.start();
- Lambda表达式(Java 8+,进一步简化):
Thread t = new Thread(() -> {System.out.println("Lambda线程执行");}, "Lambda线程");t.start();
2.3 方式三:实现Callable接口(带返回值)
步骤:
- 实现
Callable<T>
接口,重写call()
方法(有返回值,可抛出异常); - 创建
Callable
对象,包装为FutureTask
(实现RunnableFuture
接口,兼具Runnable
和Future
特性); - 将
FutureTask
传入Thread
构造器,调用start()
启动线程; - 通过
FutureTask.get()
获取返回值(会阻塞当前线程,直到任务完成)。
优势:可获取线程执行结果,可处理异常。
代码案例:
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;// 1. 实现Callable接口,泛型指定返回值类型
class MyCallable implements Callable<Integer> {private int num;public MyCallable(int num) {this.num = num;}// 2. 重写call()方法,返回计算结果@Overridepublic Integer call() throws Exception {int sum = 0;for (int i = 1; i <= num; i++) {sum += i;}return sum; // 返回1~num的和}
}public class CallableDemo {public static void main(String[] args) throws ExecutionException, InterruptedException {// 3. 创建Callable任务MyCallable task = new MyCallable(100);// 4. 包装为FutureTask(用于获取结果)FutureTask<Integer> futureTask = new FutureTask<>(task);// 5. 启动线程new Thread(futureTask, "求和线程").start();// 6. 获取结果(会阻塞,直到call()执行完毕)Integer result = futureTask.get();System.out.println("1~100的和为:" + result); // 输出:5050}
}
2.4 三种创建方式的对比
方式 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
继承Thread | 代码简单,直接使用this获取线程对象 | 单继承限制,任务与线程耦合 | 简单任务,无继承需求 |
实现Runnable | 无单继承限制,任务与线程解耦 | 无法直接获取返回值,不能抛出受检异常 | 多线程共享资源,复杂任务 |
实现Callable | 可获取返回值,可抛出异常 | 代码较复杂,需配合FutureTask | 需要获取线程执行结果的场景 |
3. Thread的常用构造器和方法
3.1 常用构造器
构造器 | 说明 |
---|---|
Thread() | 创建线程对象,默认名称(Thread-0,1…) |
Thread(String name) | 指定线程名称 |
Thread(Runnable target) | 将Runnable任务传入线程 |
Thread(Runnable target, String name) | 传入任务并指定线程名称 |
3.2 常用方法及注意事项
方法名 | 作用 | 注意事项 |
---|---|---|
void start() | 启动线程(异步执行run()) | 不可重复调用 |
void run() | 线程执行体 | 直接调用无多线程效果 |
String getName() | 获取线程名称 | - |
void setName(String name) | 设置线程名称 | 建议在start()前设置 |
static Thread currentThread() | 获取当前执行的线程对象 | 静态方法,可在任何地方调用 |
static void sleep(long millis) | 让当前线程休眠指定毫秒数 | 会抛出InterruptedException ,需处理 |
void join() | 等待该线程执行完毕后,当前线程再继续 | 需处理InterruptedException |
代码案例:
public class ThreadMethodDemo {public static void main(String[] args) throws InterruptedException {Thread t = new Thread(() -> {try {for (int i = 0; i < 3; i++) {System.out.println(Thread.currentThread().getName() + "执行:" + i);Thread.sleep(500); // 休眠500ms,模拟任务耗时}} catch (InterruptedException e) {e.printStackTrace();}}, "演示线程");t.start();System.out.println("等待演示线程执行完毕...");t.join(); // 主线程等待t线程执行完System.out.println("演示线程已结束,主线程继续");}
}
执行结果:
等待演示线程执行完毕...
演示线程执行:0
演示线程执行:1
演示线程执行:2
演示线程已结束,主线程继续
4. 线程安全
4.1 什么是线程安全?
当多个线程同时操作共享资源时,若无需额外同步操作就能保证结果正确,则称该资源是线程安全的。
- 共享资源:多个线程都能访问的变量、对象、文件等(如多线程售票系统中的“剩余票数”)。
4.2 线程安全问题出现的原因
- 多线程并发访问:多个线程同时操作共享资源;
- 共享资源修改:对共享资源进行非原子性操作(如
count++
实际分为“读取-修改-写入”三步); - 缺乏同步机制:未对共享资源的访问进行限制。
4.3 模拟线程安全问题(售票系统超卖)
场景:3个窗口同时售卖10张票,未加同步机制时出现超卖或重复售票。
class TicketSystem implements Runnable {private int ticketCount = 10; // 共享资源:总票数@Overridepublic void run() {while (true) {if (ticketCount > 0) {// 模拟售票耗时(放大线程安全问题)try { Thread.sleep(100); } catch (InterruptedException e) {}System.out.println(Thread.currentThread().getName() + "售出第" + ticketCount + "张票");ticketCount--; // 非原子操作:读取ticketCount -> 减1 -> 写回} else {break;}}}
}public class ThreadSafeDemo {public static void main(String[] args) {TicketSystem task = new TicketSystem();new Thread(task, "窗口A").start();new Thread(task, "窗口B").start();new Thread(task, "窗口C").start();}
}
问题结果(可能出现):
窗口A售出第10张票
窗口B售出第10张票 // 重复售票
窗口C售出第9张票
...
窗口A售出第1张票
窗口B售出第0张票 // 超卖(票数为负)
5. 线程同步(解决线程安全问题)
5.1 认识线程同步
线程同步:通过限制多个线程对共享资源的访问顺序,保证同一时刻只有一个线程操作资源,从而解决安全问题。核心是加锁:将共享资源的操作代码“锁住”,只有获得锁的线程才能执行。
5.2 同步代码块(显式指定锁对象)
语法:
synchronized (锁对象) {// 共享资源操作代码(临界区)
}
作用:同一时刻只有一个线程能进入同步代码块(需获取锁对象的“对象锁”)。
原理:锁对象是同步的关键,多个线程必须使用同一个锁对象才能保证同步效果。
解决售票问题案例:
class SafeTicketSystem implements Runnable {private int ticketCount = 10;private Object lock = new Object(); // 锁对象(必须是多个线程共享的对象)@Overridepublic void run() {while (true) {// 同步代码块:锁住共享资源操作synchronized (lock) { if (ticketCount > 0) {try { Thread.sleep(100); } catch (InterruptedException e) {}System.out.println(Thread.currentThread().getName() + "售出第" + ticketCount + "张票");ticketCount--;} else {break;}}}}
}
注意事项:
- 锁对象规范:
- 实例方法中:通常用
this
(当前对象)作为锁; - 静态方法中:必须用类对象(如
TicketSystem.class
)作为锁,因为静态方法属于类,不依赖实例。
- 实例方法中:通常用
5.3 同步方法(隐式锁对象)
语法:
修饰符 synchronized 返回值类型 方法名(参数) {// 共享资源操作代码
}
原理:
- 实例同步方法:锁对象为
this
(当前实例); - 静态同步方法:锁对象为类对象(如
Xxx.class
)。
与同步代码块的区别:
- 同步方法:锁住整个方法体,粒度较粗;
- 同步代码块:可只锁住关键代码,粒度更细,性能更好。
代码案例(同步方法解决售票问题):
class SafeTicketSystem2 implements Runnable {private int ticketCount = 10;// 同步实例方法:锁对象为this(当前Runnable实例)private synchronized void sellTicket() {if (ticketCount > 0) {try { Thread.sleep(100); } catch (InterruptedException e) {}System.out.println(Thread.currentThread().getName() + "售出第" + ticketCount + "张票");ticketCount--;}}@Overridepublic void run() {while (ticketCount > 0) {sellTicket(); // 调用同步方法}}
}
5.4 Lock锁(JDK 5+,显式锁)
介绍:java.util.concurrent.locks.Lock
接口,提供比synchronized
更灵活的锁定操作(如尝试获取锁、超时释放锁等)。常用实现类ReentrantLock
(可重入锁)。
常用方法:
void lock()
:获取锁(若未获取到则阻塞);void unlock()
:释放锁(必须在finally
中调用,确保锁释放);boolean tryLock()
:尝试获取锁(成功返回true,失败返回false,不阻塞)。
代码案例(Lock解决售票问题):
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;class SafeTicketSystem3 implements Runnable {private int ticketCount = 10;// 锁对象用final修饰,防止被篡改(重要!)private final Lock lock = new ReentrantLock(); @Overridepublic void run() {while (true) {lock.lock(); // 获取锁try {if (ticketCount > 0) {try { Thread.sleep(100); } catch (InterruptedException e) {}System.out.println(Thread.currentThread().getName() + "售出第" + ticketCount + "张票");ticketCount--;} else {break;}} finally {lock.unlock(); // 释放锁(必须在finally中,防止异常时锁未释放)}}}
}
注意事项:
- 必须将
unlock()
放在finally
中,否则若代码抛出异常,锁可能永远无法释放,导致死锁; - 锁对象建议用
final
修饰,防止在运行中被修改为其他对象,导致锁失效。
6. 线程池
6.1 认识线程池
线程池:管理一组预先创建的线程,用于重复执行多个任务,避免频繁创建/销毁线程的开销(线程创建和销毁需要消耗CPU和内存资源)。
不使用线程池的影响:
- 频繁创建线程:导致CPU资源浪费、内存占用过高;
- 无限制创建线程:可能引发
OutOfMemoryError
(OOM)。
线程池工作原理:
- 线程池初始化时创建核心线程;
- 任务提交时,优先使用核心线程执行;
- 核心线程满时,任务进入阻塞队列等待;
- 队列满时,创建临时线程(不超过最大线程数);
- 所有线程和队列都满时,触发任务拒绝策略;
- 临时线程空闲超过存活时间,自动销毁。
6.2 创建线程池:ThreadPoolExecutor
构造器及7个参数:
public ThreadPoolExecutor(int corePoolSize, // 核心线程数(常驻线程,即使空闲也不销毁)int maximumPoolSize, // 最大线程数(核心+临时线程的上限)long keepAliveTime, // 临时线程空闲存活时间TimeUnit unit, // 存活时间单位(如TimeUnit.SECONDS)BlockingQueue<Runnable> workQueue, // 任务阻塞队列(核心线程满时存放任务)ThreadFactory threadFactory, // 线程工厂(用于创建线程,可自定义线程名称)RejectedExecutionHandler handler // 任务拒绝策略(队列和线程都满时如何处理新任务)
)
参数解析案例(创建“工厂生产线程池”):
import java.util.concurrent.*;public class ThreadPoolDemo {public static void main(String[] args) {// 1. 创建线程池ThreadPoolExecutor pool = new ThreadPoolExecutor(2, // 核心线程数:2个(生产线固定工人)5, // 最大线程数:5个(最多临时加3个工人)3, // 临时线程存活时间:3秒TimeUnit.SECONDS,new ArrayBlockingQueue<>(3), // 队列容量:3个任务(超出核心线程的任务排队)Executors.defaultThreadFactory(), // 默认线程工厂new ThreadPoolExecutor.AbortPolicy() // 拒绝策略:直接抛出异常);// 2. 提交10个任务(模拟10个产品需要加工)for (int i = 1; i <= 10; i++) {int taskId = i;pool.submit(() -> {System.out.println(Thread.currentThread().getName() + "加工产品" + taskId);try { Thread.sleep(1000); } catch (InterruptedException e) {} // 模拟加工耗时});}// 3. 关闭线程池(不再接受新任务,等待现有任务执行完)pool.shutdown();}
}
6.3 常用方法
方法名 | 作用 |
---|---|
submit(Runnable task) | 提交Runnable任务(无返回值) |
submit(Callable<T> task) | 提交Callable任务(有返回值,返回Future) |
shutdown() | 平缓关闭线程池:不再接受新任务,等待现有任务完成 |
shutdownNow() | 立即关闭线程池:尝试中断所有任务,返回未执行的任务 |
6.4 线程池注意事项
6.4.1 什么时候创建临时线程?
当核心线程全部繁忙且阻塞队列已满时,才会创建临时线程(最多到maximumPoolSize
)。
6.4.2 什么时候拒绝新任务?
当核心线程满、队列满、临时线程满(达到maximumPoolSize
)时,新任务触发拒绝策略。
6.4.3 任务拒绝策略
拒绝策略 | 作用 |
---|---|
AbortPolicy (默认) | 直接抛出RejectedExecutionException |
CallerRunsPolicy | 由提交任务的线程(如主线程)自己执行 |
DiscardPolicy | 默默丢弃新任务,无任何提示 |
DiscardOldestPolicy | 丢弃队列中最旧的任务,尝试提交新任务 |
6.4.4 参数选择配置公式
- 核心线程数:
- CPU密集型任务(如计算):
核心线程数 = CPU核心数 + 1
(减少线程切换开销); - IO密集型任务(如文件读写、网络请求):
核心线程数 = CPU核心数 * 2
(IO操作时线程会阻塞,可多开线程提高利用率)。
- CPU密集型任务(如计算):
- 队列容量:根据任务提交频率和处理耗时调整,避免过大(导致OOM)或过小(频繁创建临时线程)。
6.5 Executors工具类(不推荐)
作用:提供快速创建线程池的静态方法,但存在严重弊端。
常用方法及弊端:
方法名 | 说明 | 弊端 |
---|---|---|
newFixedThreadPool(n) | 固定核心线程数(n),队列容量无上限 | 队列无限大,任务过多时导致OOM |
newCachedThreadPool() | 无核心线程,最大线程数无限,临时线程存活60秒 | 无限创建线程,导致OOM |
newSingleThreadExecutor() | 单线程池,队列容量无上限 | 队列无限大,导致OOM |
推荐做法:手动使用ThreadPoolExecutor
创建线程池,明确指定核心参数,避免OOM风险。
7. 并发和并行的概念
- 并发(Concurrency):多个任务在同一时间段内交替执行(宏观上同时,微观上交替)。
- 例:一个CPU核心“快速切换”处理多个任务(如边听歌边聊天)。
- 并行(Parallelism):多个任务在同一时刻同时执行(需多个CPU核心支持)。
- 例:两个CPU核心分别处理“听歌”和“聊天”任务,真正同时进行。
生活类比:
- 并发:一个厨师同时处理多个订单(切菜→炒菜→装盘,交替进行);
- 并行:多个厨师同时处理不同订单(各自独立工作,互不干扰)。