目录
示例代码(使用 Executors.newFixedThreadPool)
在 Java 编程中,多线程是一项非常重要的技术,它能够充分利用计算机的多核处理器资源,提高程序的执行效率和响应性。本文将详细介绍 Java 中创建多线程的四种方式,包括继承 Thread 类、实现 Runnable 接口、实现 Callable 接口以及使用线程池,并对每种方式的原理、代码示例和适用场景进行深入剖析。
一、继承 Thread 类创建线程
步骤
- 创建一个类继承自
Thread
类。 - 重写
run
方法,在run
方法中定义线程要执行的任务。 - 创建该类的实例,然后调用
start
方法启动线程。
示例代码
class MyThread extends Thread{
// Ctrl + o
// 展示所有的可以重写的方法
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("数据:"+i);
}
}
}
public class Demo01 {
/**
* 第一种方案,继承Thread类 重写run方法 实现
* @param args
*/
public static void main(String[] args) {
// 在Main方法中,启动了一个子线程,子线程什么时候工作
MyThread thread = new MyThread();
thread.start();// 启动一个线程,调用start方法,不要调用run方法
// 一个线程类,是可以创建多个不同的子线程的
MyThread thread2 = new MyThread();
thread2.start();// 启动一个线程,调用start方法,不要调用run方法
// 主线程,直接运行代码 会出现子线程和主线程抢占资源的情况
for (int i = 10; i < 100; i++) {
System.err.println("Main:"+i);
}
}
}
原理
当调用 start
方法时,会在新的线程中执行 run
方法。需要注意的是,start
方法只是启动线程,不会立即执行 run
方法。线程要等待获取 CPU 资源后才会执行 run
方法,而且在执行过程中可能会被其他线程抢占 CPU 资源。
二、实现 Runnable 接口创建线程
步骤
- 创建一个类实现
Runnable
接口。 - 实现
run
方法,在run
方法中定义线程要执行的任务。 - 创建
Runnable
接口实现类的实例,将其作为参数传递给Thread
类的构造函数,然后调用start
方法启动线程。
示例代码
class A implements Runnable{
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
public class Demo03 {
/**
* 多线程创建的第二种方式,使用 Runnable接口
* 该接口还需要传递给Thread类才能启动,否则自己启动不了
*
* 两种方式:推荐使用第二种
* 1、Thread类是一个线程类,它只需要管理好线程就行了,不需要管业务怎么写
* 2、具体的业务可以交给Runnable接口实现
* 3、java是单继承的,继承了Thread,就无法继承别的类了,但是可以实现多个接口。
*/
public static void main(String[] args) {
A a = new A();
new Thread(a).start();
// Runnable接口本身就是一个函数式接口,就可以使用lambda表达式,代码可以简化为如下:
new Thread( ()-> {
for (int i = 0; i < 1000; i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
}
}).start();
for (int i = 0; i < 1000; i++) {
System.err.println(Thread.currentThread().getName()+":"+i);
}
}
}
原理
Runnable
接口定义了一个无返回值的 run
方法,用于包含线程要执行的代码。Thread
类的构造函数可以接收一个 Runnable
接口的实现对象,当调用 Thread
的 start
方法时,会在新的线程中执行 Runnable
对象的 run
方法。这种方式比继承 Thread
类更灵活,因为 Java 是单继承的,如果一个类已经继承了其他类,就不能再继承 Thread
类了,但是可以实现 Runnable
接口来实现多线程。
三、实现 Callable 接口创建线程
步骤
- 创建一个类实现
Callable
接口,该接口是一个函数式接口,有一个泛型参数,用于指定返回值类型。 - 实现
call
方法,在call
方法中定义线程要执行的任务,并返回一个结果。 - 创建
Callable
接口实现类的实例,将其包装在一个FutureTask
对象中,FutureTask
实现了RunnableFuture
接口,而RunnableFuture
接口继承了Runnable
和Future
接口。 - 将
FutureTask
对象作为参数传递给Thread
类的构造函数,然后调用start
方法启动线程。可以通过FutureTask
的get
方法获取call
方法的返回结果。
示例代码
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;
class MyCall implements Callable<Integer>{
@Override
public Integer call() throws Exception {
return 200;
}
}
class MyRun implements Runnable{
@Override
public void run() {
System.out.println("我是子线程....");
}
}
public class Demo08 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask<Integer> futureTask = new FutureTask<>(new MyCall());
new Thread(futureTask,"计算线程").start();
Integer i = futureTask.get();
System.out.println(i);
// --------------------------------------
new Thread(new MyRun()).start();
// ------------------使用callable 模拟 子线程进行大量计算并返回结果------------------
FutureTask<Integer> f1 = new FutureTask<>(
()->{
System.out.println(Thread.currentThread().getName()+" come in callable");
TimeUnit.SECONDS.sleep(4);
return 1024;
}
);
FutureTask<Integer> f2 = new FutureTask<>(
()->{
System.out.println(Thread.currentThread().getName()+" come in callable");
TimeUnit.SECONDS.sleep(4);
return 2048;
}
);
new Thread(f1,"线程一:").start();
new Thread(f2,"线程二:").start();
while(!f1.isDone()){
System.out.println("f1 wait中.....");
}
while(!f2.isDone()){
System.out.println("f2 wait中.....");
}
// 其实 get 获取不到值会一直阻塞,直到获取到值为止
int a = f1.get();
int b = f2.get();
System.out.println(a+b);
}
}
原理
Callable
接口与 Runnable
接口类似,但是 Callable
接口的 call
方法可以有返回值,并且可以抛出异常。FutureTask
用于包装 Callable
对象,它可以在未来某个时刻获取 call
方法的返回结果。通过这种方式,可以实现有返回值的多线程任务。
与Runnable接口相比的不同之处
(1)是否有返回值
(2)是否抛异常
(3)落地方法不一样,一个是run,一个是call
四、使用线程池创建线程
步骤
- 通过
Executors
工具类的静态方法(如newFixedThreadPool
、newCachedThreadPool
、newSingleThreadExecutor
)创建一个线程池对象,或者直接使用ThreadPoolExecutor
类来创建自定义的线程池。 - 创建
Runnable
或Callable
接口实现类的实例,作为任务提交给线程池。对于Runnable
任务,可以使用execute
方法提交;对于Callable
任务,可以使用submit
方法提交。
示例代码(使用 Executors.newFixedThreadPool
)
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class MyThreadPoolDemo {
public static void main(String[] args) {
// ExecutorService threadPool = Executors.newFixedThreadPool(5); //一个银行网点,5个受理业务的窗口
// ExecutorService threadPool = Executors.newSingleThreadExecutor(); //一个银行网点,1个受理业务的窗口
ExecutorService threadPool = Executors.newCachedThreadPool(); //一个银行网点,可扩展受理业务的窗口
//10个顾客请求
try {
for (int i = 1; i <=10; i++) {
threadPool.execute(()->{
System.out.println(Thread.currentThread().getName()+"\t 办理业务");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool.shutdown();
}
}
}
原理
线程池用于管理和复用线程。当提交一个任务到线程池时,线程池会根据自身的状态和配置来决定如何处理任务。如果线程池中有空闲线程,就会将任务分配给空闲线程执行;如果没有空闲线程且线程数量未达到最大限制,就会创建新的线程来执行任务;如果线程数量达到最大限制且任务队列已满,会根据线程池的拒绝策略来处理任务。这样可以有效地控制线程的数量,提高系统的性能和资源利用率,减少线程创建和销毁的开销。
线程池的优势
线程池做的工作只要是控制运行的线程数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量,超出数量的线程排队等候,等其他线程执行完毕,再从队列中取出任务来执行。
它的主要特点为:线程复用;控制最大并发数;管理线程。
第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的销耗。
第二:提高响应速度。当任务到达时,任务可以不需要等待线程创建就能立即执行。
第三:提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会销耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
Java中的线程池是通过Executor框架实现的,该框架中用到了Executor,Executors,ExecutorService,ThreadPoolExecutor这几个类。
经常使用的线程池做法
1、Executors.newFixedThreadPool(int)
执行长期任务性能好,创建一个线程池,一池有N个固定的线程,有固定线程数的线程
newFixedThreadPool创建的线程池corePoolSize和maximumPoolSize值是相等的,它使用的是LinkedBlockingQueue
2、Executors.newSingleThreadExecutor()
一个任务一个任务的执行,一池一线程。
newSingleThreadExecutor 创建的线程池corePoolSize和maximumPoolSize值都是1,它使用的是LinkedBlockingQueue
3、Executors.newCachedThreadPool()
执行很多短期异步任务,线程池根据需要创建新线程,但在先前构建的线程可用时将重用它们。可扩容,遇强则强。
newCachedThreadPool创建的线程池将corePoolSize设置为0,将maximumPoolSize设置为Integer.MAX_VALUE,它使用的是SynchronousQueue,也就是说来了任务就创建线程运行,当线程空闲超过60秒,就销毁线程。
自定义线程池
虽然根据API 我们能很轻松的使用到线程池,但是在实际开发中我们经常自定义线程池,怎么做呢?
参数说明
1、corePoolSize:线程池中的常驻核心线程数
2、maximumPoolSize:线程池中能够容纳同时执行的最大线程数,此值必须大于等于1
3、keepAliveTime:多余的空闲线程的存活时间
当前池中线程数量超过corePoolSize时,当空闲时间达到keepAliveTime时, 多余线程会被销毁直到只剩下corePoolSize个线程为止。
4、unit:keepAliveTime的单位
5、workQueue:任务队列,被提交但尚未被执行的任务 就是我们之前讲的阻塞队列
6、threadFactory:表示生成线程池中工作线程的线程工厂,用于创建线程,一般默认的即可
7、handler:拒绝策略,表示当队列满了,并且工作线程大于等于线程池的最大线程数(maximumPoolSize)时如何来拒绝
请求执行的runnable的策略
线程池的拒绝策略
AbortPolicy(默认):直接抛出RejectedExecutionException异常阻止系统正常运行。
CallerRunsPolicy:“调用者运行”一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,俗称从哪儿来到哪儿去。
DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加人队列中尝试再次提交当前任务。
DiscardPolicy:该策略默默地丢弃无法处理的任务,不予任何处理也不抛出异常。如果允许任务丢失,这是最好的一种策略。以上内置拒绝策略均实现了RejectedExecutionHandle接口
1、在创建了线程池后,线程池中的线程数为零。
2、当调用execute()方法添加一个请求任务时,线程池会做出如下判断:
2.1如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务;
2.2如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列;
2.3如果这个时候队列满了且正在运行的线程数量还小于maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
2.4如果队列满了且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会启动饱和拒绝策略来执行。
3、当一个线程完成任务时,它会从队列中取下一个任务来执行。
4、当一个线程无事可做超过一定的时间(keepAliveTime)时,线程会判断:
如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉。
所以线程池的所有任务完成后,它最终会收缩到corePoolSize的大小。
示例代码
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.*;
/**
* 线程池
* Arrays
* Collections
* Executors
*/
public class MyThreadPoolDemo {
public static void main(String[] args) {
ExecutorService threadPool = new ThreadPoolExecutor(
2,
5,
2L,
TimeUnit.SECONDS,
new ArrayBlockingQueue<Runnable>(3),
Executors.defaultThreadFactory(),
//new ThreadPoolExecutor.AbortPolicy()
//new ThreadPoolExecutor.CallerRunsPolicy()
//new ThreadPoolExecutor.DiscardOldestPolicy()
new ThreadPoolExecutor.DiscardOldestPolicy()
);
//10个顾客请求
try {
for (int i = 1; i <= 10; i++) {
threadPool.execute(() -> {
System.out.println(Thread.currentThread().getName() + "\t 办理业务");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool.shutdown();
}
}
}
五、总结
Java 提供了多种创建多线程的方式,每种方式都有其特点和适用场景。继承 Thread
类简单直接,适用于简单的线程任务;实现 Runnable
接口更加灵活,适合在已有类层次结构中使用多线程;实现 Callable
接口可用于需要获取线程执行结果的场景;使用线程池则可以高效地管理和复用线程,适用于需要大量线程处理任务的情况,并且可以通过合理配置线程池参数来优化系统性能。在实际开发中,需要根据具体的需求和场景选择合适的多线程创建方式,以充分发挥多线程编程的优势,提高程序的质量和效率。