文章目录
一、线程
1.1创建线程的方式
在Java中,线程的创建方式主要有四种:继承Thread类、实现Runnable接口、实现Callable接口以及使用线程池(ExecutorService)。下面我将分别介绍这四种方式的优缺点。
1. 继承Thread类
class MyThread extends Thread {
@Override
public void run() {
System.out.println("Thread is running: " + Thread.currentThread().getName());
}
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start(); // 启动线程
}
}
优点:
- 简单直观:通过继承Thread类并重写run()方法,可以很容易地创建线程。
- 代码量少:无需额外创建接口或类。
缺点:
- 单继承限制:由于Java采用单继承机制,如果一个类已经继承了其他类,则无法再继承Thread类。
- 线程与任务耦合度高:线程和任务高度耦合,不能复用任务。
2. 实现Runnable接口
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("Runnable is running: " + Thread.currentThread().getName());
}
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start(); // 启动线程
}
}
优点:
- 避免单继承限制:一个类可以实现多个接口,因此不会受到Java单继承的限制。
- 线程与任务解耦:Runnable接口将线程的任务和线程本身分开,使得线程和任务可以解耦,任务可以被多个线程共享。
- 灵活性高:设计更加灵活,符合面向对象的设计思想。
缺点:
- 需要通过Thread类启动线程:略显繁琐,但这也是设计上的一种解耦,使得任务和线程的管理更加清晰。
3. 实现Callable接口
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
return "Callable result: " + Thread.currentThread().getName();
}
public static void main(String[] args) throws InterruptedException, ExecutionException {
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<String> future = executor.submit(new MyCallable());
String result = future.get(); // 获取Callable执行结果
System.out.println(result);
executor.shutdown();
}
}
优点:
- 支持返回值:Callable接口允许线程执行任务并返回结果,而Runnable则不支持返回值。
- 可以抛出异常:Callable执行时可以抛出异常,便于处理任务执行中的错误。
缺点:
- 需要结合ExecutorService使用:增加了代码复杂度,但ExecutorService提供了更好的线程管理机制。
- 阻塞问题:通过Future获取结果时,必须等待线程执行完毕,存在一定的阻塞。
4. 使用线程池(ExecutorService)
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
class MyTask implements Runnable {
@Override
public void run() {
System.out.println("Task is running: " + Thread.currentThread().getName());
}
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(3); // 创建一个固定大小的线程池
for (int i = 0; i < 5; i++) {
executorService.submit(new MyTask()); // 提交任务给线程池
}
executorService.shutdown(); // 关闭线程池,不再接受新任务,但会继续执行已提交的任务
}
}
优点:
- 高效管理线程:线程池能有效管理和复用线程,避免频繁创建和销毁线程的开销。
- 处理高并发:适合高并发场景,能够高效地处理大量并发任务。
- 配置灵活:线程池提供了丰富的配置选项,可以灵活调整线程池大小,优化资源使用。
缺点:
- 配置复杂:需要合理配置线程池的大小,不当的配置可能导致线程池资源浪费或性能瓶颈。
总结
选择哪种线程创建方式取决于具体的应用场景和需求。对于简单的任务,可以选择继承Thread类;对于需要复用任务或避免单继承限制的场景,可以选择实现Runnable接口;对于需要返回值或处理异常的任务,可以选择实现Callable接口;对于高并发或需要管理大量线程的场景,使用线程池(ExecutorService)是最佳选择。
1.2 start()和run()的区别
start()
方法用于启动一个新线程,并异步地调用该线程的run()
方法。run()
方法包含了线程要执行的代码,它是线程实际执行的地方。- 直接调用
run()
方法(而不是通过start()
方法)会在当前线程中同步执行代码,而不是在新的线程中执行。
因此,在创建和启动线程时,应该使用start()
方法,而不是直接调用run()
方法。这是编写多线程Java程序时的关键区别。
ps:直接调用run()
就相当于调用普通的同步方法,不涉及异步执行。
二、线程池
1. 线程池重要参数
线程池的重要参数通常指的是Java中ThreadPoolExecutor
类的构造函数中所使用的参数,这些参数用于配置线程池的行为,有助于高效地管理和复用线程,减少创建和销毁线程的开销。以下是线程池的几个重要参数:
-
corePoolSize(核心线程数):
- 作用:线程池中保持活动的最小线程数,即使这些线程处于空闲状态,线程池也会保持它们的存在,直到任务完成或线程池关闭。
- 用法:如果提交的任务数大于核心线程数,线程池会根据需要增加线程数,但不会超过
maximumPoolSize
。
-
maximumPoolSize(最大线程数):
- 作用:线程池中允许的最大线程数。线程池的线程数不会超过这个值。
- 用法:在队列已满的情况下,线程池会创建新的线程来处理任务,直到线程池的线程数达到这个值。
-
keepAliveTime(空闲线程保持存活的时间):
- 作用:超过这个时间没有任务执行的空闲线程会被销毁。
- 用法:如果线程池的线程数超过
corePoolSize
,且空闲线程超过keepAliveTime
,则这些空闲线程会被终止。
-
unit(keepAliveTime的时间单位):
- 作用:指定
keepAliveTime
的单位。 - 用法:可以是秒(
TimeUnit.SECONDS
)、毫秒(TimeUnit.MILLISECONDS
)等。
- 作用:指定
-
workQueue(任务队列):
- 作用:用于保存等待执行的任务。线程池通过队列来存储提交的任务,等待有空闲线程时执行。
- 用法:如果队列已满并且线程池中线程数小于
maximumPoolSize
,线程池会创建新的线程来处理任务。如果线程池中的线程数已经达到最大值,则根据拒绝策略处理任务。常见的队列类型有:LinkedBlockingQueue
:队列大小可以无限制,任务会被加入到队列中等待。ArrayBlockingQueue
:队列大小有上限,任务会被限制在队列大小内,超过上限则拒绝任务。SynchronousQueue
:每个任务都需要有一个空闲线程来执行,队列的容量为0。每提交一个任务就必须等待一个线程来接收它。
-
handler(拒绝策略):
- 作用:当线程池中的线程数已经达到最大线程数,且任务队列也已满时,如何处理新提交的任务。
- 常见策略:
AbortPolicy
:默认策略,抛出RejectedExecutionException
异常。CallerRunsPolicy
:由调用者线程执行该任务,避免任务丢失。DiscardPolicy
:丢弃当前任务,不抛出异常。DiscardOldestPolicy
:丢弃队列中最旧的任务,并尝试提交当前任务。
-
threadFactory(线程工厂):
- 作用:用于创建新线程的工厂。通过线程工厂,用户可以定制线程的创建方式,如线程的名称、优先级等。
- 默认值:
Executors.defaultThreadFactory()
,即默认的线程工厂。
-
allowCoreThreadTimeOut(是否允许核心线程超时):
- 作用:是否允许核心线程在空闲时被终止。默认情况下,核心线程会一直保持活动状态,直到线程池关闭。
- 用法:如果设置为
true
,即使是核心线程,也会在超出空闲时间后被终止。
ThreadPoolExecutor executor2 = new ThreadPoolExecutor(5,
10,
1000, TimeUnit.MILLISECONDS,
new ArrayBlockingQueue<>(10),
new UtilityElf.DefaultThreadFactory("test", false),
new ThreadPoolExecutor.AbortPolicy());
这些参数共同决定了线程池的性能和行为,通过合理配置这些参数,可以优化线程池的性能,适应不同的应用场景。
2. 线程池常用任务队列详解
在Java线程池中,任务队列有多种常见类型,每种类型都有其特定的用途和代码实现方式。以下是几种主要的任务队列类型及其代码示例:
1. 无界队列(Unbounded Queue) - LinkedBlockingQueue
无界队列理论上可以容纳无限数量的任务,但实际上受限于JVM的内存。当内存耗尽时,会抛出OutOfMemoryError
。
原理:LinkedBlockingQueue
队列容量可以指定容量也可以不指定容量。指定时为有界队列,不指定容量时其实代码中给了默认长度(Integer.MAX_VALUE=231-1)
// 无界队列构造函数
public LinkedBlockingDeque() {
this(Integer.MAX_VALUE);
}
import java.util.concurrent.*;
public class UnboundedQueueExample {
public static void main(String[] args) {
// 创建一个无界队列
BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();
// 创建一个线程池,使用无界队列
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, // 核心线程数
4, // 最大线程数
60L, TimeUnit.SECONDS, // 空闲线程保持存活的时间及单位
queue
);
// 提交任务给线程池
for (int i = 0; i < 10; i++) {
executor.submit(() -> {
System.out.println(Thread.currentThread().getName() + " is executing task.");
});
}
// 关闭线程池
executor.shutdown();
}
}
2. 有界队列(Bounded Queue) - ArrayBlockingQueue
有界队列有一个固定的容量限制。当队列满时,尝试添加新任务的操作会被阻塞,直到队列中有空间可用。
import java.util.concurrent.*;
public class BoundedQueueExample {
public static void main(String[] args) {
// 创建一个有界队列,容量为5
BlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(5);
// 创建一个线程池,使用有界队列
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, // 核心线程数
4, // 最大线程数
60L, TimeUnit.SECONDS, // 空闲线程保持存活的时间及单位
queue
);
// 提交任务给线程池
for (int i = 0; i < 10; i++) {
executor.submit(() -> {
System.out.println(Thread.currentThread().getName() + " is executing task.");
});
}
// 关闭线程池
executor.shutdown();
}
}
3. 直接提交队列(SynchronousQueue)
直接提交队列不存储任务,而是将每个任务直接提交给线程处理。它相当于一个通道,将任务直接传递给线程。
import java.util.concurrent.*;
public class SynchronousQueueExample {
public static void main(String[] args) {
// 创建一个直接提交队列
BlockingQueue<Runnable> queue = new SynchronousQueue<>();
// 创建一个线程池,使用直接提交队列
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, // 核心线程数
4, // 最大线程数
0L, TimeUnit.MILLISECONDS, // 空闲线程保持存活的时间(这里设置为0,因为队列不存储任务)
queue,
new ThreadPoolExecutor.AbortPolicy() // 拒绝策略(当线程池和队列都满时)
);
// 提交任务给线程池
for (int i = 0; i < 10; i++) {
executor.submit(() -> {
System.out.println(Thread.currentThread().getName() + " is executing task.");
});
}
// 关闭线程池
executor.shutdown();
}
}
4. 优先级队列(PriorityBlockingQueue)–实际执行顺序并非严格按照优先级
优先级队列支持按照任务的优先级顺序来处理任务。任务需要实现Comparable
接口或提供Comparator
来定义优先级。
import java.util.concurrent.*;
class PriorityTask implements Runnable, Comparable<PriorityTask> {
private int priority;
public PriorityTask(int priority) {
this.priority = priority;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " is executing task with priority " + priority);
}
@Override
public int compareTo(PriorityTask other) {
return Integer.compare(this.priority, other.priority);
}
}
public class PriorityQueueExample {
public static void main(String[] args) {
// 创建一个优先级队列
BlockingQueue<Runnable> queue = new PriorityBlockingQueue<>();
// 创建一个线程池,使用优先级队列
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, // 核心线程数
4, // 最大线程数
60L, TimeUnit.SECONDS, // 空闲线程保持存活的时间及单位
queue
);
// 提交不同优先级的任务给线程池
executor.execute(new PriorityTask(1));
executor.execute(new PriorityTask(5));
executor.execute(new PriorityTask(3));
// 关闭线程池
executor.shutdown();
}
}
-
尽管你使用了
PriorityBlockingQueue
作为线程池的任务队列,但任务的执行顺序仍然可能不是严格按照优先级来的。这通常是由以下几个原因导致的:- 线程并发性:
- 线程池中的线程是并发执行的,这意味着多个线程可能会同时从队列中取出任务并执行。尽管
PriorityBlockingQueue
保证了队列中任务的优先级顺序,但多个线程同时执行时,任务的实际执行顺序可能会受到线程调度、CPU资源分配等因素的影响。
- 线程池中的线程是并发执行的,这意味着多个线程可能会同时从队列中取出任务并执行。尽管
- 任务执行时间:
- 如果不同优先级的任务执行时间差异很大,高优先级的任务可能会在低优先级的任务之后开始执行,但因为它执行得快,所以可能会在低优先级的任务之前完成。这给人一种错觉,好像任务的执行顺序不是按照优先级来的。
- 线程池配置:
- 线程池的核心线程数和最大线程数设置也会影响任务的执行顺序。如果核心线程数很少,而任务数量很多,那么即使队列中的任务是按照优先级排序的,也可能因为线程不足而导致任务等待执行。
- PriorityBlockingQueue的特性:
PriorityBlockingQueue
是一个无界的优先级队列,它基于堆结构实现。虽然它能够保证每次出队的都是优先级最高的任务,但在多线程环境下,多个线程可能会同时尝试出队操作,这可能导致一些竞争条件,进而影响任务的执行顺序。
- 任务提交顺序与优先级的关系:
- 你提交的任务顺序是1, 2, 3,但如果这些任务的优先级不是按照数字大小来设置的(比如数字越大优先级越低),那么执行顺序自然就不会是1, 2, 3。确保你的
PriorityTask
类的compareTo
方法正确实现了优先级的比较逻辑。
- 你提交的任务顺序是1, 2, 3,但如果这些任务的优先级不是按照数字大小来设置的(比如数字越大优先级越低),那么执行顺序自然就不会是1, 2, 3。确保你的
- 线程池状态管理:
- 线程池在管理其内部线程和任务时,可能会因为一些内部状态管理(如线程正在执行其他任务、线程处于空闲状态但尚未被唤醒等)而导致任务执行顺序的偏差。
为了改善这种情况,你可以尝试以下方法:
- 确保
PriorityTask
类的compareTo
方法正确实现了优先级的比较逻辑。 - 调整线程池的配置,如增加核心线程数和最大线程数,以减少任务等待执行的时间。
- 如果任务执行时间差异很大,考虑使用同步机制(使用同步工具如
CountDownLatch
、Semaphore
等)或调整任务的设计来确保高优先级的任务能够优先执行。 - 监控线程池的性能指标,如活跃线程数、任务队列大小等,以便及时发现并解决问题。
请注意,尽管
PriorityBlockingQueue
提供了优先级排序的功能,但在多线程环境下,它并不能保证任务的严格顺序执行。如果你需要严格的顺序执行,可能需要考虑使用其他同步机制或调整应用程序的设计。 - 线程并发性: