JAVA 多线程实现、线程池创建使用、多线程的异步操作

Java
343
0
0
2023-05-26
标签   Java多线程

目录

二、 多线程 的实现方式(四种)

三、 线程池 的创建使用(五种)

1、newFixedThreadPool定长线程池

2、Cached thread Pool可缓存线程池

3、newSingleThreadExecutor单线程化线程池

4、newScheduledThreadPool周期性线程池

无返回值的周期性线程池

有返回值的周期性线程池

5、 ThreadPoolExecutor (手动创建线程池)

线程池的submit和execute方法区别

一、初认多线程

1、什么是 线程

进程是:一个应用程序(1个进程是一个软件)。

线程是:一个进程中的执行场景/执行单元。

注意: 一个进程可以启动多个线程。

java 主线程: 每个java程序都含有一个线程,那就是主线程(main线程)。Java应用程序都是从主类main方法执行的,当 jvm 加载代码,发现卖弄方法之后,就会启动一个线程,这个线程就是主线程,负责执行main方法。如果在主线程里面创建其他线程,就会在主线程和其他线程来回切换,直到其他所有线程结束才会结束主线程。

所谓多线程,就是说一个应用程序有多条执行路径,每当我们打开一个应用程序的时候,就相打开了一个进程,而进程中执行的操作(这就是一条线程对应用程序进行访问),就是线程。以 迅雷 为例,打开迅雷就相当于打开一个进程,下载文件的操作就是线程,多线程就是同时下载多个文件。

以接口为例,当许多人同时调用一个接口,我们就可以把每一个人看做一条线程去调用我们的接口。

二、多线程的实现方式(四种)

1、继承 Thread 类

通过继承 Thread 类似实现多线程的步骤如下:

  1. 创建 MyThread 类,让其继承 Thread 类并重写 run() 方法。
  2. 创建 MyThread 类的实例对象,即创建一个新线程。
  3. 调用 start() 方法,启动线程。
public class MyThread extends Thread {
@Override
public void run() {
System.out.println("我是通过继承 Thread 类创建的多线程,我叫" + Thread.currentThread().getName());
}
}
class TestMyThread {
public static void main(String[] args) {
MyThread myThread1 = new MyThread();
myThread1.setName("Thread-1");
MyThread myThread2 = new MyThread();
myThread2.setName("Thread-2");
MyThread myThread3 = new MyThread();
myThread3.setName("Thread-3");
myThread1.start();
myThread2.start();
myThread3.start();
}
}

为了演示线程执行顺序的随机性,我特意创建了三个线程,并为每一个线程命名,下面是我运行五次程序的执行结果:

// 第一次
我是通过继承 Thread 类创建的多线程,我叫Thread-2
我是通过继承 Thread 类创建的多线程,我叫Thread-1
我是通过继承 Thread 类创建的多线程,我叫Thread-3
// 第二次
我是通过继承 Thread 类创建的多线程,我叫Thread-1
我是通过继承 Thread 类创建的多线程,我叫Thread-3
我是通过继承 Thread 类创建的多线程,我叫Thread-2
// 第三次
我是通过继承 Thread 类创建的多线程,我叫Thread-1
我是通过继承 Thread 类创建的多线程,我叫Thread-3
我是通过继承 Thread 类创建的多线程,我叫Thread-2

从上面的执行结果我们可以看到线程的执行顺序和代码中编写的顺序没有关系,线程的执行顺序是具有随机性的。

2、实现 Runnable 接口

通过实现 Runnable 接口实现多线程的步骤如下:

1.创建 MyRunnable 类实现 Runnable 接口。

2.创建 MyRunnable 类的实例对象 myRunnable 。

3.把实例对象 myRunnable 作为参数来创建 Thread 类的实例对象 thread,实例对象 thread 就是一个新线程。

4.调用 start() 方法,启动线程。、

public class RunnableTest implements Runnable{
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("我是通过实现 Runnable 接口创建的多线程,我叫" + Thread.currentThread().getName());
}
}
}
class Test {
public static void main(String[] args) {
Thread myThread1 = new Thread(new RunnableTest());
Thread myThread2 = new Thread(new RunnableTest());
myThread1.start();
myThread2.start();
}
}

执行结果如下:

我是通过实现 Runnable 接口创建的多线程,我叫Thread-1
我是通过实现 Runnable 接口创建的多线程,我叫Thread-0
我是通过实现 Runnable 接口创建的多线程,我叫Thread-1
我是通过实现 Runnable 接口创建的多线程,我叫Thread-0
我是通过实现 Runnable 接口创建的多线程,我叫Thread-1
我是通过实现 Runnable 接口创建的多线程,我叫Thread-0
我是通过实现 Runnable 接口创建的多线程,我叫Thread-1
我是通过实现 Runnable 接口创建的多线程,我叫Thread-0
我是通过实现 Runnable 接口创建的多线程,我叫Thread-1
我是通过实现 Runnable 接口创建的多线程,我叫Thread-0

注:

相比于继承 Thread 类的方法来说,实现 Runnable 接口是一个更好地选择,因为 Java 不支持多继承,但是可以实现多个接口。

有一点值得注意的是 Thread 类也实现了 Runnable 接口,这意味着构造函数 Thread(Runnable target) 不仅可以传入 Runnable 接口的对象,而且可以传入一个 Thread 类的对象,这样就可以将一个 Thread 对象中的 run() 方法交由其他线程进行调用。

3、实现 Callable 接口

Callable 接口只有一个 call() 方法,源码如下:

public interface Callable<V> {
V call() throws Exception ;
}

从源码我们可以看到 Callable 接口和 Runnable 接口类似,它们之间的区别在于 run() 方法没有返回值,而 call() 方法是有返回值的。

通过实现 Callable 接口实现多线程的步骤如下:

1.创建 MyCallable 类实现 Callable 接口。

2.创建 MyCallable 类的实例对象 myCallable。

3.把实例对象 myCallable 作为参数来创建 FutureTask 类的实例对象 futureTask。

4.把实例对象 futureTask 作为参数来创建 Thread 类的实例对象 thread,实例对象 thread 就是一个新线程。

5.调用 start() 方法,启动线程。

public class CallbaleTest implements Callable {
@Override
public Integer call() throws Exception {
int a = 6;
int b = 9;
System.out.println("我是通过实现 Callable 接口创建的多线程,我叫" + Thread.currentThread().getName());
return a + b;
}
}
class TestMyCallable {
public static void main(String[] args) throws ExecutionException, InterruptedException {
CallbaleTest myCallable = new CallbaleTest();
FutureTask< integer > futureTask = new FutureTask(myCallable);
Thread thread = new Thread(futureTask);
Thread thread1 = new Thread(futureTask);
thread.start();
thread1.start();
Integer integer = futureTask.get();
System.out.println("返回值为:" + integer);
}
}

执行后的结果如下:

我是通过实现 Callable 接口创建的多线程,我叫Thread-0
返回值为:15

注:FutureTask 类提供了一个 get() 方法用来获取 call() 方法的返回值,但需要注意的是调用这个方法会导致程序阻塞,必须要等到线程结束后才会得到返回值。

4、线程池(下面讲)

三、线程池的创建使用(五种)

上面讲的是通过new Thread等方式创建线程,这种方式的弊端是:

a. 每次new Thread新建对象性能差。

b. 线程缺乏统一管理,可能无限制新建线程,相互之间竞争,及可能占用过多系统资源导致死机或oom。

c. 缺乏更多功能,如定时执行、定期执行、线程中断。

下面将要介绍的是 Jdk 提供的四种线程池的好处在于:

a. 重用存在的线程,减少对象创建、消亡的开销,性能佳。

b. 可有效控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。

c. 提供定时执行、定期执行、单线程、并发数控制等功能。

1、newFixedThreadPool定长线程池

Executors.newFixedThreadPool:创建一个固定大小的线程池,可控制并发的线程数,超出的线程会在队列中等待。

import java.util.concurrent.*;
public class Test {
public static void main(String[] args) {
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(2);
for (int j = 0; j < 3; j++) {
fixedThreadPool.execute(() -> {
for (int i = 0; i < 2; i++) {
System.out.println("线程名:" + Thread.currentThread().getName() + " i是:" + i);
}
});
}
fixedThreadPool.shutdown();//关闭线程池
// shutdown Now();//停止接收新任务,原来的任务停止执行,但是它并不对正在执行的任务做任何保证,有可能它们都会停止,也有可能执行完成。
}
}

输出:

线程名:pool-1-thread-1 i是:0
线程名:pool-1-thread-1 i是:1
线程名:pool-1-thread-2 i是:0
线程名:pool-1-thread-1 i是:0
线程名:pool-1-thread-1 i是:1
线程名:pool-1-thread-2 i是:1

2、CachedThreadPool可缓存线程池

Executors.newCachedThreadPool:创建一个可缓存的线程池,若线程数超过处理所需,缓存一段时间后会回收,若线程数不够,则新建线程。

可缓存线程池为无限大,当执行第二个任务时第一个任务已经完成,会回收复用第一个任务的线程,而不用每次新建线程,可灵活回收空闲线程,若无可回收,则新建线程。

public class Test {
public static void main(String[] args) {
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
for (int j = 0; j < 3; j++) {
cachedThreadPool.execute(() -> {
for (int i = 0; i < 2; i++) {
System.out.println("线程名:" + Thread.currentThread().getName() + " i是:" + i);
}
});
}
cachedThreadPool.shutdown();//关闭线程池
}
}

输出:

线程名:pool-1-thread-1 i是:0
线程名:pool-1-thread-1 i是:1
线程名:pool-1-thread-3 i是:0
线程名:pool-1-thread-2 i是:0
线程名:pool-1-thread-2 i是:1
线程名:pool-1-thread-3 i是:1

使用场景:

CachedThreadPool 是根据短时间的任务量来决定创建的线程数量的,所以它适合短时间内有突发大量任务的处理场景。

3、newSingleThreadExecutor单线程化线程池

newSingleThreadExecutor线程池你可以理解为特殊的newFixedThreadPool线程池,它只会创建一个线程,并且所有任务按照指定顺序。如果你创建了多个任务,因为只会有一个线程,多余的任务会被阻塞到队列里依次执行。

下面的示例循环3次,每次都是用的一个线程,这个线程会先执行第一个循环的任务,在执行第二个循环的任务,再执行第三个循环的任务,所以输出的 i 是有序的。

public class newSingleThreadExecutor {
public static void main(String[] args) {
ExecutorService singleThreadPool = Executors.newSingleThreadExecutor();
for (int j = 0; j < 3; j++) {
singleThreadPool.execute(() -> {
for (int i = 0; i < 3; i++) {
System.out.println("线程名:" + Thread.currentThread().getName() + " i是:" + i);
}
});
}
System.out.println("准备关闭线程池");
singleThreadPool.shutdown();//关闭线程池
}
}

输出:

准备关闭线程池
线程名:pool-1-thread-1 i是:0
线程名:pool-1-thread-1 i是:1
线程名:pool-1-thread-1 i是:2
线程名:pool-1-thread-1 i是:0
线程名:pool-1-thread-1 i是:1
线程名:pool-1-thread-1 i是:2
线程名:pool-1-thread-1 i是:0
线程名:pool-1-thread-1 i是:1
线程名:pool-1-thread-1 i是:2

单个线程的线程池有什么意义?

单个线程的线程池相比于线程来说,它的优点有以下 2 个:

可以复用线程:即使是单个线程池,也可以复用线程。

提供了任务管理功能:单个线程池也拥有任务队列,在任务队列可以存储多个任务,这是线程无法实现的,并且当任务队列满了之后,可以执行拒绝策略,这些都是线程不具备的。

4、newScheduledThreadPool周期性线程池

周期性线程池用来处理延时任务或定时任务。

无返回值的周期性线程池

public class newScheduledThreadPool {
public static void main(String[] args) {
ScheduledExecutorService scheduleThreadPool = Executors.newScheduledThreadPool(3);
System.out.println("测试1");
for (int i = 0; i < 5; i++) {
scheduleThreadPool.schedule(() -> {
System.out.println("线程名:" + Thread.currentThread().getName() + "已经过了3秒");
}, 3, TimeUnit.SECONDS);
}
System.out.println("测试2");
scheduleThreadPool.shutdown();//关闭线程池
}
}

说明:

我们声明了3个线程,创建的时候用循环创建了5个,多出来的2个会阻塞直到前3个线程有执行完的再复用他们的线程;因为采用了延时3秒输出,所以会先输出测试1、测试2,然后等待3秒后再执行输出线程的内容。

输出:

测试1
测试2
线程名:pool-1-thread-2已经过了3
线程名:pool-1-thread-3已经过了3
线程名:pool-1-thread-1已经过了3
线程名:pool-1-thread-3已经过了3
线程名:pool-1-thread-2已经过了3

有返回值的周期性线程池

public static void main(String[] args) {
ScheduledExecutorService scheduleThreadPool = Executors.newScheduledThreadPool(3);
System.out.println("测试1");
ScheduledFuture<String> scheduledFuture = scheduleThreadPool.schedule(() -> {
return "线程名:" + Thread.currentThread().getName() + "已经过了3秒";
}, 3, TimeUnit.SECONDS);
System.out.println("测试2");
try {
//获取线程返回的值并输出
System.out.println(scheduledFuture.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
scheduleThreadPool.shutdown();//关闭线程池
}
}

输出:

测试1
测试2
线程名:pool-1-thread-1已经过了3

定时线程执行

定时执行可以用scheduleAtFixedRate方法进行操作,里面的参数4表示代码或启动运行后第4秒开始执行,3表示每3秒执行一次。因为我们设置了3个线程,所以运行后线程会在第4秒开始用3个线程每3秒执行一次。

public static void main(String[] args) {
ScheduledExecutorService scheduleThreadPool = Executors.newScheduledThreadPool(3);
System.out.println("测试1");
scheduleThreadPool.scheduleAtFixedRate(() -> {
System.out.println("线程名:" + Thread.currentThread().getName() + "已经过了3秒");
}, 4, 3, TimeUnit.SECONDS);
System.out.println("测试2");
}

输出:

测试1
测试2
线程名:pool-1-thread-1已经过了3
线程名:pool-1-thread-1已经过了3
线程名:pool-1-thread-2已经过了3
线程名:pool-1-thread-3已经过了3
......

5、ThreadPoolExecutor(手动创建线程池)

上面我们介绍了四种JDK自带的线程池,但是平常不推荐使用。

ThreadPoolExecutor 相比于其他创建线程池的优势在于,它可以通过参数来控制最大任务数和拒绝策略,让线程池的执行更加透明和可控,所以在阿里巴巴《Java开发手册》是这样规定的:

【强制要求】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

这一方面是由于jdk中自带的线程池,都有其局限性,不够灵活;另外使用ThreadPoolExecutor有助于大家明确线程池的运行规则,创建符合自己的业务场景需要的线程池,避免资源耗尽的风险。

需要进行线程池的初始化,所以引入以下依赖:

<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>29.0-jre</version>
</dependency>

在开始前需要注意线程池的几个参数:

(在下面代码的ThreadPoolExecutor里你会看到这些参数):

corePoolSize=> 线程池里的核心线程数量
maximumPoolSize=> 线程池里允许有的最大线程数量
keepAliveTime=> 空闲线程存活时间
unit=> keepAliveTime的时间单位,比如分钟,小时等
workQueue=> 缓冲队列
threadFactory=> 线程工厂用来创建新的线程放入线程池
handler=> 线程池拒绝任务的处理策略,比如抛出异常等策略
线程池按以下行为执行任务
1. 当线程数小于核心线程数时,创建线程。
2. 当线程数大于等于核心线程数,且任务队列未满时,将任务放入任务队列。
3. 当线程数大于等于核心线程数,且任务队列已满
-1 若线程数小于最大线程数,创建线程
-2 若线程数等于最大线程数,抛出异常,拒绝任务

无返回值的线程创建

代码初始化了线程池并用 executorService.execute 分别创建了两个线程,一个用来输出本线程的名字,另一个用来异步调用 printA() 方法。

public static void main(String[] args) {
System.out.println("开始");
//线程池的初始化
ThreadFactory namedThreadFactory = new ThreadFactoryBuilder().setNameFormat("demo-pool-%d").build();
ExecutorService executorService = new ThreadPoolExecutor(
60,
100,
0L,
TimeUnit.MILLISECONDS,
new LinkedBlockingDeque<>(1024),
namedThreadFactory,
new ThreadPoolExecutor.AbortPolicy());
//开启一个新线程用来输出线程的名字
executorService.execute(() -> System.out.println("第1个线程名字" + Thread.currentThread().getName()));
//再开启一个新线执行printA()
executorService.execute(() -> {
System.out.println("第2个线程名字" + Thread.currentThread().getName());
printA();
});
System.out.println("完成");
executorService.shutdown();
}
public static void printA() {
for (int i = 0; i < 3; i++) {
System.out.println("打印:aaaaaaaaaaaaa");
}
}

输出:

开始
完成
第1个线程名字demo-pool-0
第2个线程名字demo-pool-1
打印:aaaaaaaaaaaaa
打印:aaaaaaaaaaaaa
打印:aaaaaaaaaaaaa

有返回值的多线程调用

使用submit

public static void main(String[] args) {
System.out.println("开始");
//线程池的初始化
ThreadFactory namedThreadFactory = new ThreadFactoryBuilder().setNameFormat("demo-pool-%d").build();
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 10,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingDeque<>(1024), namedThreadFactory,
new ThreadPoolExecutor.AbortPolicy());
//异步调用对象integerCallableTask中的call()计算1-100的和
Future<Integer> future = threadPoolExecutor.submit(() -> {
int nummber = 100;
int sum = 0;
for (int i = 0; i <= nummber; i++) {
sum += i;
}
return sum;
});
try {
//获取计算的结果
Integer result = future.get();
System.out.println("和是:" + result);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
System.out.println("完成");
//shutdown():停止接收新任务,原来的任务继续执行
//shutdownNow():停止接收新任务,原来的任务停止执行
threadPoolExecutor.shutdown();
}

输出:

开始
和是:5050
完成

线程池的submit和execute方法区别

1、接收的参数不一样

execute接收的参数是new Runnable(),重写run()方法,是没有返回值的:

源码:

public interface Executor {
/**
* Executes the given command at some time in the future. The command
* may execute in a new thread, in a pooled thread, or in the calling
* thread, at the discretion of the {@code Executor} implementation.
*
* @param command the runnable task
* @throws RejectedExecutionException if this task cannot be
* accepted for execution
* @throws NullPointerException if command is null
*/ void execute(Runnable command);
}

submit接收的参数是Callable<Object>,重写call()方法,是有返回值的:

源码:

/**
* @throws RejectedExecutionException {@inheritDoc}
* @throws NullPointerException {@inheritDoc}
*/ public <T> Future<T> submit(Callable<T> task) {
if (task == null) throw new NullPointerException();
RunnableFuture<T> ftask = newTaskFor(task);
execute(ftask);
return ftask;
}

2、submit有返回值用于返回多线程计算后的值,而execute没有返回值