实现多线程
在Java中实现多线程有3种方式,1是继承Tread类,2是实现Runnable接口,3如果需要返回值的话,可以实现Callable接口
1、继承Thread类
package org.fssx;
import java.io.*;
public class App{
public static void main( String[] args ) throws IOException {
MyThread t1 = new MyThread("Thread1");
MyThread t2 = new MyThread("Thread2");
t1.start();
t2.start();
}
}
class MyThread extends Thread{
private String name;
public MyThread(String name) {
this.name = name;
}
@Override
public void run() {
for(int i=0;i<5;i++){
System.out.println(this.name+"在运行"+i);
}
}
}
运行结果为:
Thread1在运行0
Thread2在运行0
Thread2在运行1
Thread2在运行2
Thread1在运行1
Thread2在运行3
Thread1在运行2
Thread2在运行4
Thread1在运行3
Thread1在运行4
2、实现Runnable接口
package org.fssx;
import java.io.*;
public class App{
public static void main( String[] args ) throws IOException {
MyThread thread1 = new MyThread("Thread1"); // 创建自定义多线程类
MyThread thread2 = new MyThread("Thread2"); //
Thread t1 = new Thread(thread1); // 将自定义类传入Thread构造方法,创建Thread类
Thread t2 = new Thread(thread2);
t1.start(); // 调用start方法,启动多线程
t2.start();
}
}
class MyThread implements Runnable{ // 实现Runnable接口
private String name;
public MyThread(String name) {
this.name = name;
}
@Override
public void run() { // 重写Runnable接口的run方法
for(int i=0;i<5;i++){
System.out.println(this.name+"在运行"+i);
}
}
}
运行结果为:
Thread2在运行0
Thread1在运行0
Thread2在运行1
Thread2在运行2
Thread2在运行3
Thread2在运行4
Thread1在运行1
Thread1在运行2
Thread1在运行3
Thread1在运行4
3、实现Callable接口
package org.fssx;
import java.io.*;
public class App{
public static void main( String[] args ) throws IOException {
FutureTask<Integer> futureTask = new FutureTask<>(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
return 123;
}
});
Thread thread = new Thread(futureTask);
thread.start();
try {
System.out.println(futureTask.get());
} catch (Exception e) {
e.printStackTrace();
}
}
}
知识点
1、run()和start()的关系:在程序中可以看到虽然我们重写的是run方法,但是调用的却是start方法,因为run只是类中的一个普通方法,调用run不会开启一个新的线程,而调用start,程序会开启一个新的线程,然后调用run方法执行程序。
2、Thread类和Runnable接口的关系:Thread类实现了Runnable接口,但是并没有完全重写Runnable接口的run方法,所以我们在继承Thread类的时候也需要重写run方法,并且如果一个类继承 Thread类,则不适合于多个线程共享资源,而实现了 Runnable 接口,就可以方便的实现资源的共享。
3、FutureTask 对象不能被多个线程同时执行:当你将 FutureTask 对象传递给多个 Thread 对象时,这些线程将尝试并发地执行同一个任务,这可能会导致不可预测的行为。然而,FutureTask 实际上是为单个执行而设计的,它内部的 call() 方法只能被调用一次。
4、FutureTask 的 get() 方法:get() 方法会阻塞调用它的线程,直到任务完成,并返回任务的结果。但是,由于 FutureTask 的 call() 方法只能被调用一次,一旦任务完成并返回了结果,后续的 get() 调用将立即返回相同的结果,而不会再次执行任务。但是,在你的代码中,你试图在任务完成后多次调用 get() 方法,这是没有问题的,因为它将返回相同的结果。然而,如果任务抛出异常,那么 get() 方法会重新抛出该异常。
线程的状态
要想实现多线程,必须在主线程中创建新的线程对象。任何线程一般具有5种状态,即创建,就绪,运行,阻塞,终止。下面分别介绍一下这几种状态:
- 创建状态 在程序中用构造方法创建了一个线程对象后,新的线程对象便处于新建状态,此时它已经有了相应的内存空间和其他资源,但还处于不可运行状态。新建一个线程对象可采用Thread
类的构造方法来实现,例如 “Thread thread=new Thread()”。 - 就绪状态 新建线程对象后,调用该线程的 start() 方法就可以启动线程。当线程启动时,线程进入就绪状态。此时,线程将进入线程队列排队,等待 CPU 服务,这表明它已经具备了运行条件。
- 运行状态 当就绪状态被调用并获得处理器资源时,线程就进入了运行状态。此时,自动调用该线程对象的 run() 方法。run() 方法定义该线程的操作和功能。
- 阻塞状态 一个正在执行的线程在某些特殊情况下,如被人为挂起或需要执行耗时的输入/输出操作,会让 CPU 暂时中止自己的执行,进入阻塞状态。在可执行状态下,如果调用sleep(),suspend(),wait()
等方法,线程都将进入阻塞状态,发生阻塞时线程不能进入排队队列,只有当引起阻塞的原因被消除后,线程才可以转入就绪状态。 - 死亡状态 线程调用 stop() 方法时或 run() 方法执行结束后,即处于死亡状态。处于死亡状态的线程不具有继续运行的能力。
Java 程序每次运行至少启动2个线程?
每当使用 Java 命令执行一个类时,实际上都会启动一个 JVM,每一个JVM实际上就是在操作系统中启动一个线程,Java 本身具备了垃圾的收集机制。所以在 Java 运行时至少会启动两个线程,一个是 main 线程,另外一个是垃圾收集线程。
线程的方法
// Thread.表示调用线程静态方法,t.表示调用实例方法
Thread.currentThread() // 获取当前线程对象
Thread.sleep(100) // 当前线程休眠1s钟
t.join() // 阻塞线程
t.start() // 启动线程
t.interrupt() // 中断线程
t.setDaemon(true) // 设置守护线程,只要当前JVM实例中尚存在任何一个非守护线程没有结束,
// 守护线程就全部工作;只有当最后一个非守护线程结束时,守护线程随着JVM一同结束工作。
t.setPriority(Thread.MIN_PRIORITY);
// 设置线程优先级,优先级最低MIN_PRIORITY,最高MAX_PRIORITY,普通NORM_PRIORITY,优先级只是
// 运行的可能性设置,并不绝对,优先级高的依然有可能慢于其他线程
Thread.currentThread().yield() // 当前线程让出CPU控制权
一个多线程的程序如果是通过 Runnable 接口实现的,则意味着类中的属性被多个线程共享,那么这样就会造成一种问题,如果这多个线程要操作同一个资源时就有可能出现资源同步问题。
// 同步对象
synchronized(同步对象){ // 这里可以使用this作为同步对象
需要同步的代码
}
// 同步方法
synchronized 方法返回值 方法名称(参数列表){
同步代码
}
当一个线程执行到了同步代码,获取了同步对象,其他线程就无法进入,直到同步对象被释放
线程池
线程池是一种基于池化思想管理和使用线程的机制,它将多个线程预先存储在一个“池子”内,当有任务出现时,可以避免重新创建和销毁线程所带来的性能开销,只需从“池子”内取出相应的线程执行对应的任务即可。常见的线程池类型主要包括以下几种:
线程池类型
- FixedThreadPool(固定大小的线程池)
特点:线程池中的线程数量固定,即使有空闲线程,也不会回收。当新的任务提交时,如果线程池中的线程都在忙碌,则任务会进入等待队列。
适用场景:适用于负载较重,但任务量相对稳定的场景。 - CachedThreadPool(可缓存的线程池)
特点:线程池中的线程数量几乎可以无限增加(最大值是Integer.MAX_VALUE,基本不会达到),当线程闲置时可以回收。它采用的存储任务的队列是SynchronousQueue,队列容量为0,不实际存储任务,只负责任务的中转和传递。
适用场景:适用于执行大量短期异步任务的场景,可以有效减少线程的创建和销毁开销。 - ScheduledThreadPool(定时任务线程池)
特点:支持定时或周期性执行任务。它允许你调度命令在给定的延迟后运行,或者定期地执行。
适用场景:适用于需要定时或周期性执行任务的场景,如定时清理缓存、定时检查系统状态等。 - SingleThreadExecutor(单线程化线程池)
特点:线程池中只有一个线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
适用场景:适用于需要保证任务顺序执行的场景,如按顺序处理用户请求等。 - SingleThreadScheduledExecutor(单线程定时任务线程池)
特点:与ScheduledThreadPool相似,但内部只有一个线程,专门用于执行定时任务。
适用场景:适用于需要单线程执行定时任务的场景,如定时检查某个资源的状态等。 - WorkStealingPool(抢占式线程池)
特点:这是JDK 1.8中引入的一种线程池,它拥有多个任务队列,线程可以执行自己队列中的任务,也可以窃取其他队列中的任务来执行。
适用场景:适用于任务执行时间相差较大的场景,可以充分利用系统资源,提高任务的执行效率。 - ThreadPoolExecutor(原始线程池)
特点:ThreadPoolExecutor是Java中最核心的线程池类,它提供了最原始的线程池创建方式,允许你设置核心线程数、最大线程数、线程空闲存活时间、任务队列等参数。
适用场景:适用于需要精细控制线程池行为的场景,如根据系统负载动态调整线程池大小等。
使用自定义ThreadPoolExecutor
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class CustomThreadPoolExecutor {
public static void main(String[] args) {
// 定义核心线程数、最大线程数、空闲线程存活时间、时间单位、工作队列、拒绝策略
int corePoolSize = 5;
int maximumPoolSize = 10;
long keepAliveTime = 1L;
TimeUnit unit = TimeUnit.SECONDS;
// 使用ArrayBlockingQueue作为工作队列,容量为10
java.util.concurrent.BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(10);
// 定义拒绝策略,这里使用ThreadPoolExecutor.AbortPolicy(默认),抛出RejectedExecutionException异常
RejectedExecutionHandler handler = new ThreadPoolExecutor.AbortPolicy();
// 创建ThreadPoolExecutor
ThreadPoolExecutor executor = new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
keepAliveTime,
unit,
workQueue,
handler);
// 提交任务到线程池执行
for (int i = 0; i < 20; i++) {
int taskId = i;
executor.submit(() -> {
System.out.println(Thread.currentThread().getName() + " is processing task " + taskId);
try {
// 模拟任务执行时间
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
// 关闭线程池(不再接受新任务,但已提交的任务会继续执行)
// 注意:这不会立即关闭线程池,因为线程池会等待所有已提交的任务完成。
// 如果你想要立即关闭线程池,不考虑正在执行的任务,可以使用shutdownNow()。
executor.shutdown();
// 通常,我们会等待所有任务完成。这里简单演示,实际应用中可能需要更复杂的逻辑来等待。
while (!executor.isTerminated()) {
// 等待线程池中的任务全部执行完毕
}
System.out.println("All tasks completed.");
}
}
参数解释
- corePoolSize(核心线程数):线程池中始终保持存活的线程数,即使它们处于空闲状态。如果设置了 allowCoreThreadTimeOut(true),这些线程在空闲时间超过 keepAliveTime 后也将被终止。
- maximumPoolSize(最大线程数):线程池中允许的最大线程数。当工作队列已满时,如果线程数小于最大线程数,则会创建新的线程来处理任务。
- keepAliveTime(线程存活时间):当线程数大于核心线程数时,这是多余空闲线程在终止前等待新任务的最长时间。
- unit(时间单位):keepAliveTime 参数的时间单位,例如 TimeUnit.SECONDS、TimeUnit.MINUTES 等。
- workQueue(工作队列):用于保存等待执行的任务的阻塞队列。这个队列仅持有 Runnable 任务。ThreadPoolExecutor 提供了几种内置的阻塞队列实现,如 ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue 等,你也可以根据需要实现自己的 BlockingQueue。
- threadFactory(线程工厂)可省略,使用默认值:用于创建新线程的工厂。你可以通过实现 ThreadFactory 接口来自定义线程的创建,例如设置线程的优先级、守护线程状态、线程名等。如果未指定,则使用Executors.defaultThreadFactory() 创建新线程。
- handler(拒绝策略)可省略,使用默认值:当线程池和工作队列都满了,无法再接受新任务时,将使用此拒绝策略处理新任务。ThreadPoolExecutor提供了几种内置的拒绝策略实现,如AbortPolicy(默认,抛出异常)、CallerRunsPolicy(在调用者的线程中执行任务)、DiscardOldestPolicy(丢弃队列中最旧的任务)、DiscardPolicy(静默地丢弃无法处理的任务)。你也可以根据需要实现自己的拒绝策略。
workQueue有哪些
工作队列是用于保存等待执行的任务的BlockingQueue接口的实现。它决定了任务在无法立即执行时的排队策略。ThreadPoolExecutor提供了几种内置的BlockingQueue实现,用于workQueue,具体包括以下几种:
- ArrayBlockingQueue:
- 基于数组的有界阻塞队列,按照先进先出(FIFO)的顺序对元素进行排序。
- 在创建ArrayBlockingQueue时,需要指定队列的容量。
- 当线程池中的线程数量达到corePoolSize后,新任务会被放入这个队列等待处理。如果队列已满,且线程数小于maximumPoolSize,则会创建新线程;若线程数已达到maximumPoolSize,则执行拒绝策略。
- LinkedBlockingQueue:
- 基于链表结构的阻塞队列,可以选择是否指定容量。如果不指定容量,则默认为Integer.MAX_VALUE,即近似无界。
- 它同样按照FIFO的顺序对元素进行排序。
- 使用LinkedBlockingQueue时,由于队列的近似无界性,可能会导致在任务提交非常频繁时,队列迅速膨胀,从而耗尽系统资源。因此,在使用时需要特别注意队列容量的管理。
- SynchronousQueue:
- 一个不存储元素的阻塞队列,即每个插入操作必须等待另一个线程的移除操作,反之亦然。
- 使用SynchronousQueue时,提交的任务不会被真实保存,而是总是尝试直接将其交给线程执行。如果没有空闲线程,则尝试创建新线程;如果线程数已达到maximumPoolSize,则执行拒绝策略。
- 由于SynchronousQueue的特性,它通常与较大的maximumPoolSize一起使用,以避免频繁执行拒绝策略。
- PriorityBlockingQueue:
- 一个支持优先级的无界阻塞队列,元素按照优先级进行排序。
- 队列中的元素必须实现Comparable接口或构造时提供Comparator,以便进行排序。
- PriorityBlockingQueue的排序稳定性依赖于其比较器的实现,且它不会保证具有相同优先级的元素的顺序。
选择哪种类型的workQueue取决于具体的应用场景和需求。例如,如果需要限制队列的大小以避免资源耗尽,则可以选择ArrayBlockingQueue;如果希望任务能够尽快得到执行,且对队列容量没有严格要求,则可以选择SynchronousQueue或LinkedBlockingQueue(但需注意后者可能导致的资源问题)。同时,PriorityBlockingQueue适用于需要按照优先级执行任务的场景。
拒绝策略
当线程池无法处理新任务时(例如,因为线程池已关闭、线程池已满且队列已满),会执行配置的拒绝策略。Java提供了四种预定义的拒绝策略:
ThreadPoolExecutor.AbortPolicy:默认策略,直接抛出RejectedExecutionException异常。
ThreadPoolExecutor.CallerRunsPolicy:在调用者线程中直接运行被拒绝的任务。
ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列中最前面的任务,然后尝试重新提交被拒绝的任务。
ThreadPoolExecutor.DiscardPolicy:不处理被拒绝的任务,直接丢弃。
线程池状态
- RUNNING:线程池正常运行,可以接受新任务并执行队列中的任务。
- SHUTDOWN:线程池不接受新任务,但会执行队列中的任务。
- STOP:线程池不接受新任务,也不会执行队列中的任务,会中断正在执行的任务。
- TIDYING:所有任务都已终止,线程池工作线程数量为0,正在执行终止后的工作。
- TERMINATED:线程池已完全终止。