目录
3. 继承Thread类并覆写run方法,通过共享变量获取结果
一、多线程实现方式
Java多线程编程是Java并发编程的核心部分,它允许程序同时执行多个任务。在Java中,实现多线程主要有两种方式:继承Thread
类和实现Runnable
接口。下面,我将分别给出这两种方式的示例。
1. 继承Thread
类
通过继承Thread
类来创建线程是最基本的一种方式。你需要创建一个扩展自Thread
类的子类,并重写其run()
方法。然后,你可以创建该子类的实例来创建新的线程。
// MyThread.java
class MyThread extends Thread {
public void run() {
System.out.println("线程运行中: " + Thread.currentThread().getName());
}
}public class ThreadExample {
public static void main(String[] args) {
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();t1.start(); // 启动线程
t2.start(); // 启动另一个线程
}
}
2. 实现Runnable
接口
另一种创建线程的方式是实现Runnable
接口。你需要创建一个实现了Runnable
接口的类的实例,这个实例将被用作Thread
对象的构造器参数。然后,通过调用Thread
对象的start()
方法来启动线程。
// MyRunnable.java
class MyRunnable implements Runnable {
public void run() {
System.out.println("线程运行中: " + Thread.currentThread().getName());
}
}public class RunnableExample {
public static void main(String[] args) {
Thread t1 = new Thread(new MyRunnable());
Thread t2 = new Thread(new MyRunnable());t1.start(); // 启动线程
t2.start(); // 启动另一个线程
}
}
3. 线程同步示例
在多线程编程中,线程同步是一个重要的概念,它用于控制多个线程对共享资源的访问。下面是一个简单的使用synchronized
关键字进行线程同步的示例。
class Counter {
private int count = 0;public synchronized void increment() {
count++;
}public synchronized int getCount() {
return count;
}
}public class CounterThread extends Thread {
private final Counter counter;public CounterThread(Counter counter) {
this.counter = counter;
}public void run() {
for (int i = 0; i < 10000; i++) {
counter.increment();
}
}public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Thread t1 = new CounterThread(counter);
Thread t2 = new CounterThread(counter);t1.start();
t2.start();t1.join();
t2.join();System.out.println("Final count: " + counter.getCount());
}
}
在这个例子中,Counter
类中的increment()
和getCount()
方法都被synchronized
关键字修饰,这确保了当一个线程正在执行这些方法时,其他线程不能同时执行这些方法,从而保证了count
变量的线程安全。
二、阿里线程池规范
阿里线程池规范主要涉及线程池的创建、配置、使用以及避免的一些常见问题。
1.线程池的作用
线程池能够充分合理地协调利用CPU、内存、I/O等系统资源,通过复用线程、控制最大并发数、实现定时和周期性任务执行、任务队列缓存策略和拒绝机制等功能,提高系统性能和稳定性。
2.不推荐使用Executors直接创建线程池
阿里巴巴开发手册中明确规定,不建议使用Executors
类直接创建线程池,原因如下:
-
FixedThreadPool和SingleThreadPool:这两个线程池允许的请求队列长度为
Integer.MAX_VALUE
,可能会堆积大量的请求,从而导致OOM
(内存溢出)。 -
CachedThreadPool:允许创建的线程数量为
Integer.MAX_VALUE
,可能会创建大量的线程,同样会导致OOM
。
3.推荐使用ThreadPoolExecutor创建线程池
通过ThreadPoolExecutor
的构造方法,可以明确地设置线程池的参数,从而避免上述风险。ThreadPoolExecutor
的构造方法包含七个主要参数:
-
corePoolSize(核心线程数):线程池中会维护一个最小的线程数量,即使这些线程处理空闲状态,他们也不会被销毁,除非设置了
allowCoreThreadTimeOut
。 -
maximumPoolSize(最大线程数):线程池允许的最大线程数,必须大于等于1,且大于等于
corePoolSize
。 -
keepAliveTime(空闲线程存活时间):当线程数大于
corePoolSize
时,这是多余空闲线程在终止前等待新任务的最长时间。 -
unit(时间单位):
keepAliveTime
的时间单位。 -
workQueue(任务队列):用于存放待执行的任务的阻塞队列。
可选值 描述 ArrayBlockingQueue 基于数组的阻塞队列,需要指定队列的大小。当任务队列满时,会尝试创建新线程,如果线程数达到 maximumPoolSize
,则执行拒绝策略。LinkedBlockingQueue 基于链表的阻塞队列,如果不指定大小,默认大小为 Integer.MAX_VALUE
。这意味着如果线程数达到corePoolSize
,新任务将一直在队列中等待,除非线程池被关闭或线程池中的线程数达到maximumPoolSize
且队列也满。SynchronousQueue 一个不存储元素的阻塞队列,每个插入操作必须等待另一个线程的对应移除操作,否则插入操作将一直阻塞。这种队列适合传递性任务,即生产者线程生成一个任务后,必须立即由消费者线程处理,不存在中间状态的任务。 PriorityBlockingQueue 一个支持优先级排序的阻塞队列,可以根据任务的优先级来执行任务。 -
threadFactory(线程工厂):用于创建新线程。
可选值 描述 Executors.defaultThreadFactory() 默认的线程工厂,创建的线程属于同一个 ThreadGroup
,且线程名遵循一定的命名规则。自定义 ThreadFactory
通过实现 ThreadFactory
接口,可以自定义线程的创建过程,包括设置线程的优先级、守护线程状态、线程名等。 -
handler(拒绝策略):当任务队列已满,且线程数达到
maximumPoolSize
时,对新任务执行的拒绝策略。可选值 描述 ThreadPoolExecutor.AbortPolicy() 默认策略,直接抛出 RejectedExecutionException
异常。ThreadPoolExecutor.CallerRunsPolicy() 用调用者所在的线程来执行任务。 ThreadPoolExecutor.DiscardPolicy() 忽略新任务,不抛出异常。 ThreadPoolExecutor.DiscardOldestPolicy() 丢弃队列中最近的一个任务,并执行当前任务。 RejectedExecutionHandler
接口,可以自定义拒绝策略的行为。
4.线程池的参数配置建议
-
核心线程数:根据系统可用的核心处理器数量进行配置,一般推荐为
CPU内核数 * 2 + 1
(对于IO密集型应用)或CPU内核数 + 1
(对于CPU密集型应用)。 -
最大线程数:一般设置为大于或等于核心线程数,具体值根据系统负载和业务需求进行调整。
-
任务队列:推荐使用有界队列,如
ArrayBlockingQueue
,以避免无限制的任务堆积导致的内存溢出。 -
拒绝策略:根据业务需求选择合适的拒绝策略,如
CallerRunsPolicy
(调用者运行策略)、DiscardPolicy
(直接丢弃任务)、DiscardOldestPolicy
(丢弃队列中最老的任务)等。
5.线程池的生命周期管理
线程池的生命周期管理包括创建、运行、关闭等阶段。在应用程序结束或不再需要线程池时,应正确关闭线程池,以释放系统资源。
6.创建线程池示例
基于Java标准库java.util.concurrent
中的ThreadPoolExecutor
类,结合阿里巴巴推荐的实践来创建一个线程池
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ThreadPoolExecutor.AbortPolicy;
import java.util.concurrent.ThreadPoolExecutor.CallerRunsPolicy;public class AlibabaThreadPoolExample {
public static void main(String[] args) {
// 核心线程数
int corePoolSize = 5;
// 最大线程数
int maximumPoolSize = 10;
// 非核心线程空闲存活时间
long keepAliveTime = 60L;
// 时间单位
TimeUnit unit = TimeUnit.SECONDS;
// 任务队列ArrayBlockingQueue
,它是基于数组的阻塞队列,有界队列有助于防止资源耗尽
ArrayBlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(100);
// 线程池拒绝策略:这里采用了CallerRunsPolicy
,即如果线程池已满,则直接在调用者线程中运行被拒绝的任务。这是一种简单的反馈机制,能够减缓任务提交速度,但可能会影响到调用者线程的性能。根据实际需要,可以选择其他的拒绝策略,如AbortPolicy
(直接抛出异常)、DiscardPolicy
(丢弃无法处理的任务)、DiscardOldestPolicy
(丢弃队列头部的任务,尝试再次提交当前任务)。
RejectedExecutionHandler handler = new ThreadPoolExecutor.CallerRunsPolicy();// 创建线程池
ThreadPoolExecutor executor = new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
keepAliveTime,
unit,
workQueue,
handler);// 提交任务
for (int i = 0; i < 120; i++) {
final int taskId = i;
executor.execute(() -> {
// 模拟任务执行
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("任务 " + taskId + " 执行完成");
});
}// 关闭线程池
// 注意:这里为了示例不关闭线程池,实际使用中需要根据情况合理关闭
// executor.shutdown();
// 等待所有任务完成
// try {
// if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
// executor.shutdownNow();
// }
// } catch (InterruptedException e) {
// executor.shutdownNow();
// }
}
}
7.线程池总结
阿里线程池规范的核心在于避免使用Executors
类直接创建线程池,而是通过ThreadPoolExecutor
的构造方法明确设置线程池的参数,以规避资源耗尽的风险。同时,合理的参数配置和生命周期管理也是保障线程池高效稳定运行的关键。
三、获取线程执行结果
1. 使用Callable
和Future
Callable
接口类似于Runnable
,但它可以返回一个结果,并且可以抛出一个异常。你可以将Callable
任务提交给ExecutorService
,它会返回一个Future
对象。通过Future
对象,你可以检查计算是否完成,等待计算完成,并检索计算结果。
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<Integer> future = executor.submit(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
// 模拟耗时任务
Thread.sleep(1000);
return 123;
}
});// 等待任务执行完成,并获取结果
try {
System.out.println(future.get()); // 输出:123
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}executor.shutdown();
2. 使用CompletableFuture
CompletableFuture
是Java 8引入的,提供了更为强大的异步编程能力。它实现了Future
和CompletionStage
接口,允许你以非阻塞的方式等待异步操作的完成,并且可以添加回调函数来处理结果或异常。
CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(() -> {
// 模拟耗时任务
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return 123;
});//非阻塞的方式等待异步操作的完成
completableFuture.thenAccept(result -> System.out.println(result)); // 输出:123
// 或者阻塞等待结果
try {
System.out.println(completableFuture.get()); // 输出:123
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
3. 继承Thread
类并覆写run
方法,通过共享变量获取结果
这不是一个推荐的做法,因为它违背了线程之间的解耦原则,并且容易引发竞态条件(race condition)和同步问题。但如果你的应用场景确实需要这么做,你可以通过定义一个共享变量来存储线程的执行结果,并使用适当的同步机制(如synchronized
、volatile
或Lock
)来确保线程安全。
4. 使用ForkJoinPool
ForkJoinPool
是Java 7引入的,用于执行可以分解为更小任务的并行算法。它特别适用于递归任务分解。ForkJoinPool
中的任务通过RecursiveAction
(无返回值)或RecursiveTask<V>
(有返回值)来提交。通过RecursiveTask
,你可以获取到子任务的结果。
ForkJoinPool pool = ForkJoinPool.commonPool();
ForkJoinTask<Integer> task = pool.submit(() -> {
// 递归或并行计算逻辑
return 123;
});try {
System.out.println(task.get()); // 输出:123
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
5.获取线程结果总结
在Java中,获取线程执行结果的主要方式是使用Callable
和Future
,以及Java 8引入的CompletableFuture
。这两种方式都提供了丰富的API来支持异步编程和结果处理。继承Thread
类并通过共享变量获取结果的方式虽然可行,但通常不推荐使用,因为它容易导致线程间的耦合和同步问题。ForkJoinPool
则适用于需要递归分解任务的场景。