在Java中,创建线程有多种方式,每种方式都有其适用场景和优缺点。以下是四种常见的创建线程的方式:
一、方式一:继承Thread类
这是最直观的方式,通过创建一个新的类继承Thread
类,并重写其run
方法。
class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("数据:" + i);
}
}
}
public class Demo01 {
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start();
MyThread thread2 = new MyThread();
thread2.start();
for (int i = 10; i < 100; i++) {
System.err.println("Main:" + i);
}
}
}
优点:简单直观。
缺点:Java不支持多重继承,因此如果一个类已经继承了另一个类,那么它就不能再继承
Thread
类。
二、方式二:实现Runnable接口创建线程目标类
这种方式更加灵活,因为它允许类继承其他类的同时实现Runnable
接口。
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 {
public static void main(String[] args) {
A a = new A();
new Thread(a).start();
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);
}
}
}
优点:避免了Java单继承的限制,可以更容易地扩展功能。
缺点:需要手动管理线程的生命周期。
三、使用Callable和FutureTask创建线程
Callable
接口与Runnable
类似,但它可以返回值并抛出异常。
class MyCall implements Callable<Integer> {
@Override
public Integer call() throws Exception {
return 200;
}
}
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);
}
}
优点:可以返回值和抛出异常。
缺点:使用起来比
Runnable
复杂,需要配合FutureTask
使用。
通过FutureTask类和Callable接口的联合使用可以创建能够获取异步执行结果的线程,具体步骤如下:
(1) 创建一个Callable接口的实现类,并实现其call()方法,编写好异步执行的具体逻辑,可以有返回值。
(2) 使用Callable实现类的实例构造一个FutureTask实例。
(3) 使用FutureTask实例作为Thread构造器的target入参,构造新的Thread线程实例。
(4) 调用Thread实例的start()方法启动新线程,启动新线程的run()方法并发执行。其内部的执行过程为:启动Thread实例的run()方法并发执行后,会执行FutureTask实例的run()方法,最终会并发执行Callable实现类的call()方法。
(5) 调用FutureTask对象的get()方法阻塞性地获得并发线程的执行结果。
按照以上步骤,通过Callable接口和Future接口相结合创建多线程,实例如下:
创建一个Callable接口的实现类:
public class CallableTaskDemo implements Callable {
// 编写好异步执行的具体逻辑,可以有返回值。
// (Runnable接口中的run()方法是没有返回值得,Callable接口的call()方法有返回值)
@Override
public Long call() throws Exception {
Long startTime = System.currentTimeMillis();
System.out.println(Thread.currentThread().getName()+" 线程开始运行");
Thread.sleep(1000);
for(int i=0;i<100000000;i++){
int j = i*10000;
}
Long endTime = System.currentTimeMillis();
Long used = endTime-startTime;
System.out.println(Thread.currentThread().getName()+" 线程结束运行");
return used;
}
}
在这个例子中有两个线程:一个是执行main()方法的主线程,叫作main;另一个是main线程通过thread.start()方法启动的业务线程,叫作callableTaskThread。该线程是一个包含FutureTask任务作为target的Thread线程。
public class CreateDemo {
public static void main(String[] args) throws InterruptedException {
CallableTaskDemo callableTaskDemo = new CallableTaskDemo();
FutureTask<Long> futureTask = new FutureTask<Long>(callableTaskDemo);
Thread thread = new Thread(futureTask,"callableTaskThread");
thread.start();
Thread.sleep(500);
System.out.println("main线程执行一会");
for(int i=0;i<100000000/2;i++){
int j = i*10000;
}
// 获取并发任务的执行结果
try {
System.out.println(thread.getName()+" 线程占用时间:"+futureTask.get());
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
main线程通过thread.start()启动callableTaskThread线程之后,会继续自己的事情,callableTaskThread线程开始并发执行。
callableTaskThread线程首先执行的是thread.run()方法,然后在其中会执行到其target(futureTask任务)的run()方法;接着在这个futureTask.run()方法中会执行futureTask的callable成员的call()方法,这里的callable成员(ReturnableTask实例)是通过FutureTask构造器在初始化时传递进来的、自定义的Callable实现类的实例。
说明:
Callable 接口:Callable 是一个泛型接口,call() 方法中定义了线程的任务,并返回一个结果。
Future 接口:Future 表示一个异步计算的结果,使用 get() 方法可以获取线程执行的结果。
ExecutorService:通常与 ExecutorService 结合使用,可以更好地管理线程池和任务。
优点:
返回结果:Callable 可以返回执行结果,而 Runnable 不能。
异常处理:Callable 可以抛出受检查的异常,便于处理复杂任务中的错误。
线程池支持:与 ExecutorService 结合,可以实现灵活的线程管理。
缺点:
相对复杂:相比 Runnable 和 Thread,Callable 和 Future 的使用稍微复杂一些,特别是涉及到线程池的管理。
四、使用线程池
线程池是一种更加高效的方式来管理线程,它可以复用线程,减少创建和销毁线程的开销。
第一种:使用自带的API实现
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秒,就销毁线程。
代码演示:
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();
}
}
}
第二种:使用自定义线程池(面试的重点,不要轻视)
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();
}
}
}
总结:
每种创建线程的方式都有其适用场景。在实际开发中,我们通常会根据需求选择最合适的方式。例如,对于简单的后台任务,可以使用
Runnable
接口;对于需要返回结果的任务,可以使用Callable
接口;而对于需要频繁创建和销毁线程的场景,使用线程池是更好的选择。
平台声明:以上文章转载于《CSDN》,文章全部或者部分内容、文字的真实性、完整性、及时性本站不作任何保证或承诺,仅作参考。
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/m0_56341622/article/details/143865368