线程
一. 概念
线程(Thread)是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。一个进程可以包含多个线程,这些线程共享该进程的资源(如内存空间、文件描述符等),但同时拥有各自独立的执行栈、程序计数器和寄存器集合。
线程的核心特征
与进程的关系
- 进程是资源分配的基本单位,线程是调度执行的基本单位。
- 一个进程至少包含一个线程(称为主线程),也可创建多个线程(称为子线程)。
- 进程间资源相互独立,线程间资源共享(因此需注意线程安全问题,如竞态条件)。
举例说明
- 打开一个浏览器(进程),其中 “网页渲染”“下载文件”“播放音频” 等功能可由不同线程同时执行,它们共享浏览器的内存和网络连接。
二. 创建线程的四种方式
1. 继承Thread类并重写run()方法
步骤:
- 定义类继承java.lang.Thread类
- 重写run()方法(线程执行的核心逻辑)
- 创建线程对象并调用start()方法启动线程(而非直接调用run())
示例代码:
package com.itheima.ReStudy2;
public class Demo1 {
public static void main(String[] args) {
Thread t = new MyThread();
t.start();
}
}
class MyThread extends Thread{
@Override
public void run() {
System.out.println("恭喜你,创建线程成功");
}
}
特点:
- 实现简单,但受限于 Java 单继承特性,灵活性较低。
2. 实现Runnable接口
步骤:
- 定义类实现java.lang.Runnable接口
- 实现run()方法(线程执行逻辑)
- 创建Thread对象,将Runnable实例作为参数传入
- 调用Thread对象的start()方法启动线程
示例代码:
package com.itheima.ReStudy2;
public class Demo2 {
public static void main(String[] args) {
Thread t = new Thread(new MyThread2());
t.start();
}
}
class MyThread2 implements Runnable{
@Override
public void run() {
System.out.println("恭喜你,通过Runnable接口成功实现了一个线程");
}
}
特点:
- 避免单继承限制,可同时实现其他接口,推荐优先使用。
3. 实现Callable接口(带返回值的线程)
步骤:
- 定义类实现java.util.concurrent.Callable<V>接口(V为返回值类型)
- 实现call()方法(线程逻辑,可返回结果并抛出异常)
- 创建FutureTask<V>对象包装Callable实例(用于获取返回值)
- 创建Thread对象传入FutureTask,调用start()启动
- 通过FutureTask的get()方法获取线程执行结果
示例代码:
package com.itheima.ReStudy2;
import java.util.Objects;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class DEmo3Callable {
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask f = new FutureTask<Object>(new MyThread3());
Thread t = new Thread(f);
t.start();
String a = (String) f.get();
// 需要通过FutureTask获取Callable的返回值
System.out.println(a);
}
}
class MyThread3 implements Callable<Object>{
@Override
public Object call() throws Exception {
String a = "执行Callable成功 返回了一个String字符串!";
return a;
}
}
特点:
- 支持返回执行结果,可抛出异常,适用于需要线程执行结果的场景(如并行计算)。
核心区别总结
继承Thread | 简单 | 无 | 低(单继承) | 简单线程任务 |
实现Runnable | 中等 | 无 | 高 | 多线程共享资源或需多实现 |
实现Callable | 较高 | 有 | 高 | 需要线程执行结果或异常处理 |
实际开发中,Runnable和Callable因灵活性更常用,尤其是配合线程池使用时。
4.线程池
在 Java 中,线程池是管理线程生命周期的重要机制,能够有效避免频繁创建 / 销毁线程的性能开销。你提到的应该是ThreadPoolExecutor(核心实现类)和Executors(工具类),下面详细介绍:
4.1 核心实现类:ThreadPoolExecutor
ThreadPoolExecutor是线程池的核心实现类,位于java.util.concurrent包下,通过灵活配置参数可满足不同场景需求。
核心构造参数
public ThreadPoolExecutor(
int corePoolSize, // 核心线程数(始终存活的线程数)
int maximumPoolSize, // 最大线程数(线程池允许的最大线程数)
long keepAliveTime, // 非核心线程空闲超时时间
TimeUnit unit, // 超时时间单位(如TimeUnit.SECONDS)
BlockingQueue<Runnable> workQueue, // 任务等待队列(存放未执行的任务)
ThreadFactory threadFactory, // 线程工厂(用于创建线程)
RejectedExecutionHandler handler // 拒绝策略(任务满时的处理方式)
)
常用拒绝策略
示例代码:
package com.itheima.ReStudy2;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class Demo4ThreadPoolExecutor {
public static void main(String[] args) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(
3,//核心线程数
5, //最大线程数
10,//线程空闲时间
TimeUnit.SECONDS,//销毁时间单位
new ArrayBlockingQueue<>(4),//任务队列
Executors.defaultThreadFactory(),//线程工厂
new ThreadPoolExecutor.AbortPolicy()//拒绝策略
);
for (int i = 0; i < 15; i++) {
executor.execute(new MyThread4());
System.out.println("当前线程数:"+executor.getPoolSize());
System.out.println("当前排队线程数:"+executor.getQueue().size());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
executor.shutdown();
}
}
class MyThread4 implements Runnable{
public void run()
{
System.out.println(Thread.currentThread().getName()+"执行了");
}
}
4.2 工具类:Executors
Executors提供了便捷的静态方法,可快速创建预配置的线程池(底层仍基于ThreadPoolExecutor),适合简单场景。但阿里巴巴开发手册不推荐使用,因默认参数可能导致资源耗尽(如无界队列)。
常用方法
Executors.newFixedThreadPool(int nThreads)
- 固定大小的线程池(核心线程数 = 最大线程数)
- 队列使用LinkedBlockingQueue(无界,可能堆积大量任务导致 OOM
ExecutorService fixedPool = Executors.newFixedThreadPool(5); // 固定5个线程
Executors.newCachedThreadPool()
- 可缓存的线程池(核心线程数 = 0,最大线程数 = Integer.MAX_VALUE)
- 空闲线程 60 秒后销毁,适合短期任务
- 风险:任务过多时可能创建大量线程导致 OOM
ExecutorService cachedPool = Executors.newCachedThreadPool();
Executors.newSingleThreadExecutor()
- 单线程池(核心线程数 = 最大线程数 = 1)
- 所有任务按顺序执行,适合需要串行执行的场景
ExecutorService singlePool = Executors.newSingleThreadExecutor();
Executors.newScheduledThreadPool(int corePoolSize)
- 定时任务线程池,支持延迟执行或周期性执行
ScheduledExecutorService scheduledPool = Executors.newScheduledThreadPool(3);
// 延迟1秒后执行
scheduledPool.schedule(() -> System.out.println("延迟执行"), 1, TimeUnit.SECONDS);
// 延迟2秒后,每3秒执行一次
scheduledPool.scheduleAtFixedRate(() -> System.out.println("周期执行"), 2, 3, TimeUnit.SECONDS);
4.3 线程池使用流程
4.4 核心区别与建议
ThreadPoolExecutor | 参数可控,灵活适配场景 | 配置复杂 | 生产环境,需精细控制资源 |
Executors工具类 | 快速创建,代码简洁 | 默认参数有潜在风险(如 OOM) | 简单场景或测试环境 |
最佳实践:生产环境中推荐直接使用ThreadPoolExecutor,根据业务需求合理设置核心线程数、队列大小和拒绝策略,避免资源耗尽风险。
三. 多线程并发安全解决
在多线程环境中,当多个线程同时操作共享资源时,可能会导致数据不一致、逻辑错误等问题,这就是并发安全问题。
1. 并发安全问题的产生条件
示例:不安全的多线程操作
public class UnsafeThreadDemo {
private static int count = 0; // 共享资源
public static void main(String[] args) throws InterruptedException {
// 创建1000个线程,每个线程对count加100次
Runnable task = () -> {
for (int i = 0; i < 100; i++) {
count++; // 非原子操作,存在安全问题
}
};
Thread[] threads = new Thread[1000];
for (int i = 0; i < 1000; i++) {
threads[i] = new Thread(task);
threads[i].start();
}
// 等待所有线程执行完毕
for (int i = 0; i < 1000; i++) {
threads[i].join();
}
System.out.println("最终结果:" + count); // 预期100000,实际往往小于该值
}
}
问题原因:多个线程同时读取count的值,修改后再写回(由于线程执行并发机制),导致部分增量操作被覆盖。
二、并发安全问题的解决方案
1. 同步锁(synchronized代码块)
通过 synchronized关键字锁定共享资源,确保同一时间只有一个线程能执行临界区代码。
语法:
synchronized (锁对象) { // 操作共享资源的代码(临界区) }
示例:
public class SynchronizedBlockDemo {
private static int count = 0;
private static final Object lock = new Object(); // 锁对象(推荐使用专用对象)
public static void main(String[] args) throws InterruptedException {
Runnable task = () -> {
for (int i = 0; i < 100; i++) {
synchronized (lock) { // 锁定临界区
count++;
}
}
};
// 启动线程逻辑同上…
Thread[] threads = new Thread[1000];
for (int i = 0; i < 1000; i++) {
threads[i] = new Thread(task);
threads[i].start();
}
for (int i = 0; i < 1000; i++) {
threads[i].join();
}
System.out.println("最终结果:" + count); // 正确输出100000
}
}
特点:
- 锁对象可以是任意对象,但多个线程必须使用同一个锁对象才能保证同步
- 推荐使用private static final Object作为锁对象,避免锁对象被修改
2. 同步方法(synchronized方法)
将 synchronized关键字修饰在方法上,此时锁对象为:
- 非静态方法:当前对象(this)
- 静态方法:当前类的Class对象
示例:
public class SynchronizedMethodDemo {
private static int count = 0;
// 静态同步方法(锁对象为SynchronizedMethodDemo.class)
private static synchronized void increment() {
count++;
}
public static void main(String[] args) throws InterruptedException {
Runnable task = () -> {
for (int i = 0; i < 100; i++) {
increment(); // 调用同步方法
}
};
// 启动线程逻辑同上…
Thread[] threads = new Thread[1000];
for (int i = 0; i < 1000; i++) {
threads[i] = new Thread(task);
threads[i].start();
}
for (int i = 0; i < 1000; i++) {
threads[i].join();
}
System.out.println("最终结果:" + count); // 正确输出100000
}
}
Lock相比synchronized的优势:
- 可中断锁:支持tryLock(long time, TimeUnit unit)超时获取锁,避免死锁
- 可尝试获取锁:tryLock()方法尝试获取锁,失败则立即返回
- 公平锁:ReentrantLock可通过构造参数new ReentrantLock(true)实现公平锁(按请求顺序获取锁)
- 可分离锁:配合Condition实现更灵活的线程通信
3.三种方案的对比
同步代码块 | synchronized(锁) | 中等 | 较好 | 需锁定代码片段时 |
同步方法 | synchronized修饰方法 | 低(粒度粗) | 一般 | 简单场景,整个方法需同步时 |
Lock锁 | 显式lock()/unlock() | 高 | 较好(竞争激烈时更优) | 复杂场景(超时、公平锁等) |
最佳实践:
- 简单场景优先使用synchronized(代码简洁,JVM 优化更好)
- 复杂场景(如需要超时控制、公平锁)使用Lock
- 尽量缩小同步范围(只锁必要的代码),减少线程阻塞时间
评论前必须登录!
注册