1. Java创建线程有哪几种方式?
一个线程在Java中使用一个Thread实例来描述。Thread类是Java语言的一个重要的基础类,位于java.lang包中。Thread类有不少非常重要的属性和方法,用于存储和操作线程的描述信息。
Thread类的构造方法:
1.1 线程创建方法一:继承Thread类创建线程类
(1) 继承Thread类,创建一个新的线程类。
(2) 同时重写run()方法,将需要并发执行的业务代码编写在run()方法中。
// 继承Thread类,创建一个新的线程类
public class ThreadDemo extends Thread {
// 重写了Thread类的run()方法,将需要并发执行的用户业务代码编写在继承的run()方法中
@Override
public void run() {
for(int i=0;i<2;i++){
System.out.println(getName()+" 轮次:"+i);
}
System.out.println(getName()+" 运行结束");
}
}
public class CreateDemo {
public static void main(String[] args) {
ThreadDemo threadDemo = new ThreadDemo();
// 启动线程
threadDemo.start();
System.out.println(Thread.currentThread().getName()+" 运行结束");
}
}
执行结果:
main 运行结束
Thread-0 轮次:0
Thread-0 轮次:1
Thread-0 运行结束
1.2 线程创建方法二:实现Runnable接口创建线程目标类
通过继承Thread类并重写它的run()方法只是创建Java线程的一种方式。是否可以不继承Thread类实现线程的新建呢?
1、Thread类中的 run() 方法
public class Thread implements Runnable {
// 执行目标
private Runnable target;
// 调用执行目标的run()方法
@Override
public void run() {
if (target != null) {
target.run();
}
}
// 包含执行目标的构造器
public Thread(Runnable target, String name) {
init(null, target, name, 0);
}
}
在Thread类的run()方法中,如果target(执行目标)不为空,就执行target属性的run()方法。而target属性是Thread类的一个实例属性,并且target属性的类型为Runnable。
Thread类有一系列的构造器,其中有多个构造器可以为target属性赋值,这些构造器包括如下两个:
-
public Thread(Runnable target)
-
public Thread(Runnable target,String name)
使用这两个构造器传入target执行目标实例(Runnable实例),就可以直接通过Thread类的run()方法以默认方式实现,达到线程并发执行的目的。在这种场景下,可以不通过继承Thread类实现线程类的创建。在为Thread的构造器传入target实例前,先来看看Runnable接口是何方神圣。
2、Runnable接口
Runnable是一个极为简单的接口,位于java.lang包中。接口中只有一个方法run() :
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
Runnable有且仅有一个抽象方法——void run(),代表被执行的用户业务逻辑的抽象,在使用的时候,将用户业务逻辑编写在Runnable实现类的run()方法中。当Runnable实例传入Thread实例的target属性后,Runnable接口的run()的方法将被异步调用。
3、通过实现Runnable接口创建线程类
创建线程的第二种方法就是实现Runnable接口,将需要异步执行的业务逻辑代码放在Runnable实现类的run()方法中,将Runnable实例作为target执行目标传入Thread实例。该方法的具体步骤如下:
(1) 定义一个新类实现Runnable接口。
(2) 实现Runnable接口中的run()抽象方法,将线程代码逻辑存放在该run()方法中。
(3) 通过Thread类创建线程对象,将Runnable实例作为实际参数传递给Thread类的构造器,由Thread构造器将该Runnable实例赋值给自己的target执行目标属性。
(4) 调用Thread实例的start()方法启动线程。
(5) 线程启动之后,线程的run()方法将被JVM执行,该run()方法将调用target属性的run()方法,从而完成Runnable实现类中业务代码逻辑的并发执行。
// 实现Runnable接口,需要异步并发执行的代码逻辑被编写在它的run()方法中
public class RunnableDemo implements Runnable {
@Override
public void run() {
for(int i=0;i<3;i++){
System.out.println(Thread.currentThread().getName()+" 轮次:"+i);
}
System.out.println(Thread.currentThread().getName()+" 运行结束");
}
}
public class CreateDemo2 {
public static void main(String[] args) {
Runnable target = new RunnableDemo();
// 通过Thread类创建线程对象,并将Runnable实例作为实际参数传入
Thread thread = new Thread(target,"RunnableThread");
// 线程对象创建完成后,调用Thread线程实例的start()方法启动新线程的并发执行。
// 这时,Runnable实例的run()方法会在新线程Thread的实例方法run()中被调用。
thread.start();
System.out.println(Thread.currentThread().getName()+" 运行结束");
}
}
执行结果:
Thread-0 轮次:0
Thread-0 轮次:1
Thread-0 轮次:2
Thread-0 运行结束
通过实现Runnable接口的方式创建的执行目标类,如果需要访问线程的任何属性和方法,必须通过Thread.currentThread()
获取当前的线程对象,通过当前线程对象间接访问,通过继承Thread类的方式创建的线程类,可以在子类中直接调用Thread父类的方法访问当前线程的名称、状态等信息。这也是使用Runnable实现异步执行与继承Thread方法实现异步执行不同的地方。
4、优雅创建Runnable线程目标类的两种方式
使用Runnable创建线程目标类除了直接实现Runnable接口之外,还有两种比较优雅的代码组织方式:
(1) 通过匿名类优雅地创建Runnable线程目标类。
(2) 使用Lambda表达式优雅地创建Runnable线程目标类。
1. 通过匿名类优雅地创建Runnable线程目标类
在实现Runnable编写target执行目标类时,如果target实现类是一次性类,可以使用匿名实例的形式。
public class CreateDemo3 {
public static void main(String[] args) {
// 使用匿名内部类创建和启动线程
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
for(int i=0;i<3;i++){
System.out.println(Thread.currentThread().getName()+" 轮次:"+i);
}
System.out.println(Thread.currentThread().getName()+" 运行结束");
}
});
thread.start();
}
}
2. 使用Lambda表达式优雅地创建Runnable线程目标类
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
源代码中的小玄机为:在Runnable接口上声明了一个@FunctionalInterface注解。该注解的作用是:标记Runnable接口是一个“函数式接口”。在Java中,“函数式接口”是有且仅有一个抽象方法的接口。反过来说,如果一个接口中包含两个或两个以上的抽象方法,就不能使用@FunctionalInterface注解,否则编译会报错。
Runnable接口是一个函数式接口,在接口实现时可以使用Lambda表达式提供匿名实现,编写出比较优雅的代码。 如果一个接口中有多个抽象方法,那样没有办法使用Lambda表达式简化。
public class CreateDemo4 {
public static void main(String[] args) {
// 使用Lambda表达式创建和启动线程
Thread thread = new Thread(()->{
for(int i=0;i<3;i++){
System.out.println(Thread.currentThread().getName()+" 轮次:"+i);
}
System.out.println(Thread.currentThread().getName()+" 运行结束");
});
thread.start();
}
}
通过Lambda表达式直接编写Runnable接口的run()方法的实现代码,接口的名称(Runnable)、方法的名称run()统统都被省略,仅剩下了run()方法的形参列表和方法体。总体而言,经过对比可以发现:使用Lambda表达式创建target执行目标实例,代码已经做到了极致的简化。
5、通过实现Runnable接口的方式创建线程目标类的优缺点
通过实现Runnable接口的方式创建线程目标类有以下缺点:
-
所创建的类并不是线程类,而是线程的target执行目标类,需要将其实例作为参数传入线程类的构造器,才能创建真正的线程。
-
如果访问当前线程的属性,不能直接访问Thread的实例方法,必须通过Thread.currentThread()获取当前线程实例,才能访问和控制当前线程。
通过实现Runnable接口的方式创建线程目标类有以下优点:
-
可以避免由于Java单继承带来的局限性。如果异步逻辑所在类已经继承了一个基类,就没有办法再继承Thread类。比如,当一个Dog类继承了Pet类,再要继承Thread类就不行了。所以在已经存在继承关系的情况下,只能使用实现Runnable接口的方式。
-
逻辑和数据更好分离。通过实现Runnable接口的方法创建多线程更加适合同一个资源被多段业务逻辑并行处理的场景。在同一个资源被多个线程逻辑异步、并行处理的场景中,通过实现Runnable接口的方式设计多个target执行目标类可以更加方便、清晰地将执行逻辑和数据存储分离,更好地体现了面向对象的设计思想。
通过实现Runnable接口的方式创建线程目标类更加适合多个线程的代码逻辑去共享计算和处理同一个资源的场景。这个优点不是太好理解,接下来通过具体例子说明一下:
1. 继承Thread类的方式创建2个线程,同时操作共享资源goodAmout
public class StoreGoods extends Thread {
private int goodAmount = 3;
// 通过基类的构造方法创建线程名称
public StoreGoods(String name){
super(name);
}
@Override
public void run() {
for(int i=0;i<=3;i++){
if(this.goodAmount>0){
System.out.println(Thread.currentThread().getName()+" 卖出一件,还剩:"+ --goodAmount);
// 让线程睡眠1秒
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
System.out.println(Thread.currentThread().getName()+" 运行结束");
}
}
public class SaleDemo {
public static void main(String[] args) throws InterruptedException {
System.out.println("商店版本的销售");
// 创建两个线程,同时操作共享资源goodAmount
for(int i=1;i<=2;i++){
Thread thread = new StoreGoods("店员-"+i);
thread.start();
}
System.out.println(Thread.currentThread().getName()+" 运行结束");
}
}
上面的代码新建了2个线程,相当于2个不同的商店店员,每个商店店员负责一个数量goodAmount,并且负责将自己的数量卖完。每个商店店员(线程)各卖各的,其剩余数量都是从2卖到0,没有关联
商店版本的销售
店员-1 卖出一件,还剩:2
店员-2 卖出一件,还剩:2
店员-2 卖出一件,还剩:1
店员-1 卖出一件,还剩:1
main 运行结束
店员-2 卖出一件,还剩:0
店员-1 卖出一件,还剩:0
店员-2 运行结束
店员-1 运行结束
2. 实现Runnable接口的方式创建2个线程,同时操作共享资源goodAmout
public class MallGoods implements Runnable {
// 多人销售可能导致数据出错,使用原子数据类型保证数据安全
private AtomicInteger goodAmount = new AtomicInteger(3);
@Override
public void run() {
for(int i=0;i<=3;i++){
// goodsAmount.get()和goodAmount.decrementAndGet()之间没有加锁,可能存在线程安全问题
if(this.goodAmount.get()>0){
System.out.println(Thread.currentThread().getName()+" 卖出一件,还剩:"+ goodAmount.decrementAndGet());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
System.out.println(Thread.currentThread().getName()+" 运行结束");
}
}
public class SaleDemo {
public static void main(String[] args) throws InterruptedException {
System.out.println("商场版本的销售");
MallGoods mallGoods = new MallGoods();
for(int i=1;i<=2;i++){
Thread thread = new Thread(mallGoods,"商场销售员-"+i);
thread.start();
}
System.out.println(Thread.currentThread().getName()+" 运行结束");
}
}
上面代码创建了2个线程,相当于商场招聘了2个不同的商场销售员。2个线程共享了一个Runnable类型的target执行目标实例——mallGoods实例。这里的关键点是:2个商场销售员线程通过线程的target.run()方法共同访问mallGoods实例的同一个商品数量goodsAmount,剩余数量从2卖到0,大家一起售卖,卖一个少一个,卖完为止。
商场版本的销售
main 运行结束
商场销售员-1 卖出一件,还剩:2
商场销售员-2 卖出一件,还剩:1
商场销售员-1 卖出一件,还剩:0
商场销售员-2 运行结束
商场销售员-1 运行结束
通过对比可以看出:
(1) 通过继承Thread类实现多线程能更好地做到多个线程并发地完成各自的任务,访问各自的数据资源。
(2) 通过实现Runnable接口实现多线程能更好地做到多个线程并发地完成同一个任务,访问同一份数据资源。多个线程的代码逻辑可以方便地访问和处理同一个共享数据资源(如例子中的MallGoods.goodsAmount),这样可以将线程逻辑和业务数据进行有效的分离,更好地体现了面向对象的设计思想。
(3) 通过实现Runnable接口实现多线程时,如果数据资源存在多线程共享的情况,那么数据共享资源需要使用原子类型(而不是普通数据类型),或者需要进行线程的同步控制,以保证对共享数据操作时不会出现线程安全问题。
在大多数情况下,偏向于通过实现Runnable接口来实现线程执行目标类,更容易和线程池配合使用,异步执行任务在大多数情况下是通过线程池去提交的,而很少通过创建一个新的线程去提交,所以更多的做法是,通过实现Runnable接口创建异步执行任务,而不是继承Thread去创建异步执行任务。
1.5 线程创建方法三:使用Callable和FutureTask创建线程
前面已经介绍了继承Thread类或者实现Runnable接口这两种方式来创建线程类,但是这两种方式有一个共同的缺陷:不能获取异步执行的结果。这是一个比较大的问题,很多场景都需要获取异步执行的结果,通过Runnable无法实现,是因为它的run()方法不支持返回值。
为了解决异步执行的结果问题,Java语言在1.5版本之后提供了一种新的多线程创建方法:通过Callable接口和FutureTask类相结合创建线程。
1、Callable接口
Callable接口位于java.util.concurrent包中,查看它的Java源代码,如下:
@FunctionalInterface
public interface Callable<V> {
V call() throws Exception;
}
Callable接口是一个泛型接口,也是一个“函数式接口”。其唯一的抽象方法call()有返回值,返回值的类型为Callable接口的泛型形参类型。call()抽象方法还有一个Exception的异常声明,容许方法的实现版本的内部异常直接抛出,并且可以不予捕获。
面试题:说一下 Runnable 和 Callable 有什么区别?
Callable接口类似于Runnable。不同的是,Runnable的唯一抽象方法run()没有返回值,也没有受检异常的异常声明。比较而言,Callable接口的call()有返回值,并且声明了受检异常,其功能更强大一些。
问题:Callable实例能否和Runnable实例一样,作为Thread线程实例的target来使用呢?答案是不行。Thread的target属性的类型为Runnable,而Callable接口与Runnable接口之间没有任何继承关系,并且二者唯一的方法在名字上也不同。显而易见,Callable接口实例没有办法作为Thread线程实例的target来使用。既然如此,那么该如何使用Callable接口创建线程呢?一个在Callable接口与Thread线程之间起到搭桥作用的重要接口马上就要登场了。
2、RunnableFuture接口
这个重要的中间搭桥接口就是RunnableFuture接口,该接口与Runnable接口、Thread类紧密相关。与Callable接口一样,RunnableFuture接口也位于java.util.concurrent包中,使用的时候需要用import导入。
RunnableFuture是如何在Callable与Thread之间实现搭桥功能的呢?RunnableFuture接口实现了两个目标:一是可以作为Thread线程实例的target实例,二是可以获取异步执行的结果。它是如何做到一箭双雕的呢?请看RunnableFuture接口的代码:
public interface RunnableFuture<V> extends Runnable, Future<V> {
void run();
}
RunnableFuture继承了Runnable接口,从而保证了其实例可以作为Thread线程实例的target目标;同时,RunnableFuture通过继承Future接口,保证了可以获取未来的异步执行结果。
3、Future接口
Future接口至少提供了三大功能:
- 能够取消异步执行中的任务;
- 判断异步任务是否执行完成;
- 获取异步任务完成后的执行结果;
Future接口的源代码如下:
public interface Future<V> {
// 取消异步执行中的任务
boolean cancel(boolean mayInterruptIfRunning);
boolean isCancelled();
// 判断异步任务是否执行成功
boolean isDone();
// 获取异步任务完成后的结果
V get() throws InterruptedException, ExecutionException;
V get(long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException;
}
-
get():获取异步任务执行的结果。注意,这个方法的调用是阻塞性的。如果异步任务没有执行完成,异步结果获取线程(调用线程)会一直被阻塞,一直阻塞到异步任务执行完成,其异步结果返回给调用线程。
-
get(Long timeout,TimeUnit unit):设置时限,(调用线程)阻塞性地获取异步任务执行的结果。该方法的调用也是阻塞性的,但是结果获取线程(调用线程)会有一个阻塞时长限制,不会无限制地阻塞和等待,如果其阻塞时间超过设定的timeout时间,该方法将抛出异常,调用线程可捕获此异常。
-
boolean isDone():获取异步任务的执行状态。如果任务执行结束,就返回true。
-
boolean isCancelled():获取异步任务的取消状态。如果任务完成前被取消,就返回true。
-
boolean cancel(boolean mayInterruptRunning):取消异步任务的执行。
总体来说,Future是一个对异步任务进行交互、操作的接口。但是Future仅仅是一个接口,通过它没有办法直接完成对异步任务的操作,JDK提供了一个默认的实现类——FutureTask。
4、FutureTask类
FutureTask类是Future接口的实现类,提供了对异步任务的操作的具体实现。但是,FutureTask类不仅实现了Future接口,还实现了Runnable接口,或者更加准确地说,FutureTask类实现了RunnableFuture接口。
public class FutureTask<V> implements RunnableFuture<V> {
}
前面讲到RunnableFuture接口很关键,既可以作为Thread线程实例的target目标,又可以获取并发任务执行的结果,是Thread与Callable之间一个非常重要的搭桥角色。但是,RunnableFuture只是一个接口,无法直接创建对象,如果需要创建对象,就需用到它的实现类——FutureTask。所以说,FutureTask类才是真正的在Thread与Callable之间搭桥的类。
FutureTask类的UML关系图大致如图:
从FutureTask类的UML关系图可以看到:FutureTask实现了RunnableFuture接口,而RunnableFuture接口继承了Runnable接口和Future接口,所以FutureTask既能作为一个Runnable类型的target执行目标直接被Thread执行,又能作为Future异步任务来获取Callable的计算结果。
FutureTask如何完成多线程的并发执行、任务结果的异步获取呢?FutureTask内部有一个Callable类型的成员——callable实例属性,具体如下:
private Callable<V> callable;
callable实例属性用来保存并发执行的Callable类型的任务,并且callable实例属性需要在FutureTask实例构造时进行初始化。FutureTask类实现了Runnable接口,在其run()方法的实现版本中会执行callable成员的call()方法。
此外,FutureTask内部还有另一个非常重要的Object类型的成员——outcome实例属性:
private Object outcome;
FutureTask的outcome实例属性用于保存callable成员call()方法的异步执行结果。在FutureTask类的run()方法完成callable成员的call()方法的执行之后,其结果将被保存在outcome实例属性中,供FutureTask类的get()方法获取。
5、使用Callable和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实现类的实例。
main线程和callableTaskThread线程的执行流程大致如图:
执行时长返回之后,将被作为结果保存在FutureTask内部的outcome实例属性中。至此,异步的returnableThread线程执行完毕。在main线程处理完自己的事情后(以上实例中是一个消磨时间的循环),通过futureTask的get实例方法获取异步执行的结果。这里有两种情况:
- futureTask的结果outcome不为空,callable.call()执行完成。在这种情况下,futureTast.get会直接取回outcome结果,返回给main线程(结果获取线程)。
- futureTask的结果outcome为空,callable.call()还没有执行完。在这种情况下,main线程作为结果获取线程会被阻塞住,一直阻塞到callable.call()执行完成。当执行完后,最终结果会保存到outcome中,futureTask会唤醒main线程,去提取callable.call()执行结果。
1.6 线程创建方法四:通过线程池创建线程
前面的示例中,所创建的Thread实例在执行完成之后都销毁了,这些线程实例都是不可复用的。实际上创建一个线程实例在时间成本、资源耗费上都很高,在高并发的场景中,断然不能频繁进行线程实例的创建与销毁,而是需要对已经创建好的线程实例进行复用,这就涉及线程池的技术。Java中提供了一个静态工厂来创建不同的线程池,该静态工厂为Executors工厂类
1、线程池的创建与执行目标提交
通过Executors工厂类创建一个线程池,一个简单的示例如下:
public class CreateDemo {
// 创建一个包含3个线程的线程池
private static ExecutorService threadpool = Executors.newFixedThreadPool(3);
}
以上示例通过工厂类Executors的newFixedThreadPool(int threads)方法创建了一个线程池,所创建的线程池的类型为ExecutorService。工厂类的newFixedThreadPool(int threads)方法用于创建包含固定数目的线程池,示例中的线程数量为3。
ExecutorService是Java提供的一个线程池接口,每次我们在异步执行target目标任务的时候,可以通过ExecutorService线程池实例去提交或者执行。ExecutorService实例负责对池中的线程进行管理和调度,并且可以有效控制最大并发线程数,提高系统资源的使用率,同时提供定时执行、定频执行、单线程、并发数控制等功能。
向ExecutorService线程池提交异步执行target目标任务的常用方法有:
// 执行一个Runnbale类型的target执行目标实例,无返回值
void execute(Runnable command);
// 执行一个Callable类型的target执行目标实例,返回一个Future异步任务实例
<T> Future<T> submit(Callable<T> task);
// 执行一个Runnbale类型的target执行目标实例,返回一个Future异步任务实例
Future<?> submit(Runnable task);
2、线程池的使用实战
Runnbale类型的target执行目标实例:
public class RunnableTaskDemo implements Runnable {
@Override
public void run() {
for(int i=0;i<3;i++){
System.out.println(Thread.currentThread().getName()+" 轮次:"+i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
Callable类型的target执行目标实例:
public class CallableTaskDemo implements Callable {
@Override
public Long call() throws Exception {
Long startTime = System.currentTimeMillis();
System.out.println(Thread.currentThread().getName()+" 线程开始运行");
for(int i=0;i<3;i++){
System.out.println(Thread.currentThread().getName()+" 轮次:"+i);
Thread.sleep(1000);
}
Long used = System.currentTimeMillis()-startTime;
System.out.println(Thread.currentThread().getName()+" 线程结束运行");
return used;
}
}
创建3个线程,执行3个目标任务:
public class CreateDemo {
private static ExecutorService threadpool = Executors.newFixedThreadPool(3);
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 1、执行Runnbale类型的target目标实例,无返回
threadpool.execute(new RunnableTaskDemo());
//2、 执行Runnbale类型的target目标实例,无返回,内部类形式
threadpool.execute(new Runnable() {
@Override
public void run() {
for(int i=0;i<3;i++){
System.out.println(Thread.currentThread().getName()+" 轮次:"+i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
// 3、提交Callable执行目标实例,有返回
Future future = threadpool.submit(new CallableTaskDemo());
System.out.println("异步执行的结果为:" + future.get());
}
}
执行结果为:
pool-1-thread-1 轮次:0
pool-1-thread-2 轮次:0
pool-1-thread-3 线程开始运行
pool-1-thread-3 轮次:0
pool-1-thread-2 轮次:1
pool-1-thread-1 轮次:1
pool-1-thread-3 轮次:1
pool-1-thread-2 轮次:2
pool-1-thread-1 轮次:2
pool-1-thread-3 轮次:2
pool-1-thread-3 线程结束运行
异步执行的结果为:3002
本小节的案例仅供学习使用,实际生产环境禁止使用Executors创建线程池 。
ExecutorService线程池的execute(…)与submit(…)方法的区别如下:
(1) 接收的参数不一样:
submit()可以接收两种入参:无返回值的Runnable类型的target执行目标实例和有返回值的Callable类型的target执行目标实例。而execute()仅仅接收无返回值的target执行目标实例,或者无返回值的Thread实例。
(2) submit()有返回值,而execute()没有:文章来源:https://www.toymoban.com/news/detail-401393.html
submit()方法在提交异步target执行目标之后会返回Future异步任务实例,以便对target的异步执行过程进行控制,比如取消执行、获取结果等。execute()没有任何返回,target执行目标实例在执行之后没有办法对其异步执行过程进行控制,只能任其执行,直到其执行结束。文章来源地址https://www.toymoban.com/news/detail-401393.html
到了这里,关于Java多线程 - Java创建线程的4种方式的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!