内容简介:在并发编程中,最基本的就是创建线程了,那么一般的创建姿势是怎样的,又都有些什么区别
线程创建的几种方式
在并发编程中,最基本的就是创建线程了,那么一般的创建姿势是怎样的,又都有些什么区别
一般来讲线程创建有四种方式:
- 继承Thread
- 实现Runnable接口
- 实现Callable接口,结合 FutureTask使用
- 利用该线程池ExecutorService、Callable、Future来实现
所以本篇博文从布局来讲,分为两部分
- 实例演示四种使用方式
- 对比分析四种使用方式的异同,以及适合的应用场景
I. 实例演示
目标: 创建两个线程并发实现从1-1000的累加
1. 继承Thread实现线程创建
实现逻辑如下
public class AddThread extends Thread {
private int start, end;
private int sum = 0;
public AddThread(String name, int start, int end) {
super(name);
this.start = start;
this.end = end;
}
public void run() {
System.out.println("Thread-" + getName() + " 开始执行!");
for (int i = start; i <= end; i ++) {
sum += i;
}
System.out.println("Thread-" + getName() + " 执行完毕! sum=" + sum);
}
public static void main(String[] args) throws InterruptedException {
int start = 0, mid = 500, end = 1000;
AddThread thread1 = new AddThread("线程1", start, mid);
AddThread thread2 = new AddThread("线程2", mid + 1, end);
thread1.start();
thread2.start();
// 确保两个线程执行完毕
thread1.join();
thread2.join();
int sum = thread1.sum + thread2.sum;
System.out.println("ans: " + sum);
}
}
输出结果
Thread-线程1 开始执行!
Thread-线程2 开始执行!
Thread-线程1 执行完毕! sum=125250
Thread-线程2 执行完毕! sum=375250
ans: 500500
一般实现步骤:
- 继承
Thread
类 - 覆盖
run()
方法 - 直接调用
Thread#start()
执行
逻辑比较清晰,只需要注意覆盖的是run方法,而不是start方法
2. 实现Runnable接口方式创建线程
public class AddRun implements Runnable {
private int start, end;
private int sum = 0;
public AddRun(int start, int end) {
this.start = start;
this.end = end;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 开始执行!");
for(int i = start; i <= end; i++) {
sum += i;
}
System.out.println(Thread.currentThread().getName() + " 执行完毕! sum=" + sum);
}
public static void main(String[] args) throws InterruptedException {
int start = 0, mid = 500, end = 1000;
AddRun run1 = new AddRun(start, mid);
AddRun run2 = new AddRun(mid + 1, end);
Thread thread1 = new Thread(run1, "线程1");
Thread thread2 = new Thread(run2, "线程2");
thread1.start();
thread2.start();
thread1.join();
thread2.join();
int sum = run1.sum + run2.sum;
System.out.println("ans: " + sum);
}
}
输出结果
线程2 开始执行!
线程1 开始执行!
线程2 执行完毕! sum=375250
线程1 执行完毕! sum=125250
ans: 500500
一般实现步骤:
- 实现
Runnable
接口 - 获取实现Runnable接口的实例,作为参数,创建Thread实例
- 执行
Thread#start()
启动线程
说明
相比于继承Thread,这里是实现一个接口,最终依然是借助 Thread#start()
来启动线程
然后就有个疑问:
两者是否有本质上的区别,在实际项目中如何抉择?
3. 实现Callable接口,结合FutureTask创建线程
Callable接口相比于Runnable接口而言,会有个返回值,那么如何利用这个返回值呢?
demo如下
public class AddCall implements Callable<Integer> {
private int start, end;
public AddCall(int start, int end) {
this.start = start;
this.end = end;
}
@Override
public Integer call() throws Exception {
int sum = 0;
System.out.println(Thread.currentThread().getName() + " 开始执行!");
for (int i = start; i <= end; i++) {
sum += i;
}
System.out.println(Thread.currentThread().getName() + " 执行完毕! sum=" + sum);
return sum;
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
int start = 0, mid = 500, end = 1000;
FutureTask<Integer> future1 = new FutureTask<>(new AddCall(start, mid));
FutureTask<Integer> future2 = new FutureTask<>(new AddCall(mid + 1, end));
Thread thread1 = new Thread(future1, "线程1");
Thread thread2 = new Thread(future2, "线程2");
thread1.start();
thread2.start();
int sum1 = future1.get();
int sum2 = future2.get();
System.out.println("ans: " + (sum1 + sum2));
}
}
输出结果
线程2 开始执行!
线程1 开始执行!
线程2 执行完毕! sum=375250
线程1 执行完毕! sum=125250
ans: 500500
一般实现步骤:
- 实现
Callable
接口 - 以
Callable
的实现类为参数,创建FutureTask
实例 - 将
FutureTask
作为Thread的参数,创建Thread实例 - 通过
Thread#start
启动线程 - 通过
FutreTask#get()
阻塞获取线程的返回值
说明
Callable接口相比Runnable而言,会有结果返回,因此会由FutrueTask进行封装,以期待获取线程执行后的结果;
最终线程的启动都是依赖Thread#start
4. 线程池方式创建
demo如下,创建固定大小的线程池,提交Callable任务,利用Future获取返回的值
public class AddPool implements Callable<Integer> {
private int start, end;
public AddPool(int start, int end) {
this.start = start;
this.end = end;
}
@Override
public Integer call() throws Exception {
int sum = 0;
System.out.println(Thread.currentThread().getName() + " 开始执行!");
for (int i = start; i <= end; i++) {
sum += i;
}
System.out.println(Thread.currentThread().getName() + " 执行完毕! sum=" + sum);
return sum;
}
public static void main(String[] arg) throws ExecutionException, InterruptedException {
int start=0, mid=500, end=1000;
ExecutorService executorService = Executors.newFixedThreadPool(2);
Future<Integer> future1 = executorService.submit(new AddPool(start, mid));
Future<Integer> future2 = executorService.submit(new AddPool(mid+1, end));
int sum = future1.get() + future2.get();
System.out.println("sum: " + sum);
}
}
输出
pool-1-thread-1 开始执行!
pool-1-thread-2 开始执行!
pool-1-thread-1 执行完毕! sum=125250
pool-1-thread-2 执行完毕! sum=375250
sum: 500500
一般实现逻辑:
- 创建线程池(可以利用JDK的Executors,也可自己实现)
- 创建Callable 或 Runnable任务,提交到线程池
- 通过返回的
Future#get
获取返回的结果
II. 对比分析
1. 分类
上面虽然说是有四种方式,但实际而言,主要划分为两类
- 继承Thread类,覆盖run方法填写业务逻辑
- 实现Callable或Runnable接口,然后通过Thread或线程池来启动线程
此外,还有一种利用Fork/Join框架来实现并发的方式,后续专门说明,此处先略过
2. 区分说明
继承和实现接口的区别
先把线程池的方式拎出来单独说,这里主要对比Thread, Callable, Runnable三中方式的区别
个人理解,线程的这两种方式的区别也就只有继承和实现接口的本质区别:
一个是继承Thread类,可以直接调用实例的 start()
方法来启动线程;另一个是实现接口,需要借助 Thread#start()
来启动线程
继承因为 java 语言的限制,当你的任务需要继承一个自定义的类时,会有缺陷;而实现接口却没有这个限制
至于网上很多地方说的实现Runnable接口更利于资源共享什么的,比如下面这种作为对比的
public class ShareTest {
private static class MyRun implements Runnable {
private volatile AtomicInteger ato = new AtomicInteger(5);
@Override
public void run() {
while (true) {
int tmp = ato.decrementAndGet();
System.out.println(Thread.currentThread() + " : " + tmp);
if (tmp <= 0) {
break;
}
}
}
}
public static void main(String[] args) throws InterruptedException {
MyRun run = new MyRun();
Thread thread1 = new Thread(run, "线程1");
Thread thread2 = new Thread(run, "线程2");
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("over");
}
}
输出:
Thread[线程1,5,main] : 4
Thread[线程2,5,main] : 3
Thread[线程1,5,main] : 2
Thread[线程2,5,main] : 1
Thread[线程1,5,main] : 0
Thread[线程2,5,main] : -1
over
MyRun
实现Runnable
接口,然后创建一个实例,将这个实例作为多个Thread的参数构造Thread类,然后启动线程,发现这几个线程共享了 MyRun#ato
变量
然而上面这个实现接口改成继承Thread,其他都不变,也没啥两样
public class ShareTest {
private static class MyRun extends Thread {
private volatile AtomicInteger ato = new AtomicInteger(5);
@Override
public void run() {
while (true) {
int tmp = ato.decrementAndGet();
System.out.println(Thread.currentThread() + " : " + tmp);
if (tmp <= 0) {
break;
}
}
}
}
public static void main(String[] args) throws InterruptedException {
MyRun run = new MyRun();
Thread thread1 = new Thread(run, "线程1");
Thread thread2 = new Thread(run, "线程2");
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("over");
}
}
输出如下
Thread[线程1,5,main] : 4
Thread[线程2,5,main] : 3
Thread[线程1,5,main] : 2
Thread[线程1,5,main] : 0
Thread[线程2,5,main] : 1
Thread[线程2,5,main] : -1
over
上面除了说明使用Runnable更利于资源共享神马的,其实并没有之外,还有一个比较有意思的,为什么会输出-1?
如果我这个任务是售票的话,妥妥的就超卖了,这个问题留待后续详解
Runnable, Callable两种区别
这两个就比较明显了,最根本的就是
- Runnable 无返回结果
- Callable 有返回结果
从根源出发,就直接导致使用姿势上的区别
举个形象的例子说明两种方式的区别:
小明家今儿没米了,小明要吃饭怎么办?
小明他妈对小明说,去你大爷家吃饭吧,至于小明到底吃没吃着,小妈他妈就不管了,这就是Runnable方式;
小明他妈一想,这一家子都要吃饭,我先炒个菜,让小明去大爷家借点米来,所以就等着小明拿米回来开锅,这就是Callable方式
1.Runnable
Runnable不关心返回,所以任务自己默默的执行就可以了,也不用告诉我完成没有,我不care,您自己随便玩,所以一般使用就是
new Thread(new Runnable() { public void run() {...} }).start()
换成JDK8的 lambda表达式就更简单了 new Thread(() -> {}).start();
2.Callable
相比而言,callbale就悲催一点,没法这么随意了,因为要等待返回的结果,但是这个线程的状态我又控制不了,怎么办?借助FutrueTask
来玩,所以一般可以看到使用方式如下:
FutureTask<Object> future = new FutureTask<>(() -> null);
new Thread(future).start();
Object obj = future.get(); // 这里会阻塞,直到线程返回值
Thread启动和线程池启动方式
这个就高端了,线程池一听就感觉厉害了,前面的四中方式有三种都是Thread#start()
来启动线程,这也是我们最常用的方式,这里单独说一下线程池的使用姿势
- 首先是创建一个线程池
- 利用
ExecutorService#submit()
提交线程 Future<Object>
接收返回
ExecutorService executorService = Executors.newFixedThreadPool(2);
Future<Integer> future1 = executorService.submit(()-> 10);
int ans = future1.get();
说明,这里提交线程之后,并不表示线程立马就要执行,也不表示一定可以执行(这个留待后续线程池的学习中探讨)
III. 小结
四种创建方式
-
继承Thread类,覆盖run方法,调用
Thread#start
启动 -
实现Runnable接口,创建实例,作为
Thread
构造参数传入,调用Thread#start
启动new Thread(() -> {}).start()
-
实现Callable接口,创建实例,作为
FutureTask<>
构造参数创建FutureTask
对象,将FutureTask对象作为Thread
构造参数传入,调用Thread#start
启动FutureTask<Object> future = new FutureTask<>(() -> null); new Thread(future).start(); Object obj = future.get(); // 这里会阻塞,直到线程返回值
-
创建一个线程池,利用
ExecutorService#submit()
提交线程,Future<Object>
接收返回ExecutorService executorService = Executors.newFixedThreadPool(2); Future<Integer> future1 = executorService.submit(()-> 10); int ans = future1.get();
区别与应用场景
- 继承和实现接口的方式唯一区别就是继承和实现的区别,不存在共享变量的问题
- 需要获取返回结果时,结合 FutureTask和Callable来实现
- Thread和Runnable的两种方式,原则上想怎么用都可以,个人也没啥好推荐的,随意使用
- 线程池需要注意线程复用时,对ThreadLocal中变量的串用问题(本篇没有涉及,等待后续补上)
注意
- 利用线程池创建线程,实际上依然是借助的Runnable或者Callable,能否算一种新的方式纯看个人理解
- 采用Timer方式实现定时任务的方式,也是一种新的创建线程的方式,这里也没有多说,后续将有一篇专门说明定时任务的博文介绍其用法
IV. 其他
声明
尽信书则不如,已上内容,纯属一家之言,因本人能力一般,见识有限,如有问题,请不吝指正,感激
扫描关注,java分享
以上所述就是小编给大家介绍的《Java并发学习之四种线程创建方式的实现与对比》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Algorithms Sequential & Parallel
Russ Miller、Laurence Boxer / Charles River Media / 2005-08-03 / USD 59.95
With multi-core processors replacing traditional processors and the movement to multiprocessor workstations and servers, parallel computing has moved from a specialty area to the core of computer scie......一起来看看 《Algorithms Sequential & Parallel》 这本书的介绍吧!