创建线程的三种方式及区别

为了便于后面自己手写线程池,我们必须对如何创建线程非常熟悉,尤其是实现Callable接口的方式

创建线程共有3种方式:

  • 继承Thread类

  • 实现Runnable接口

  • 实现Callable接口

一、继承Thread类创建线程

(1)继承Thread类,并重写run方法,该run方法就代表了线程将要完成的任务,因此把run()方法称为执行体;

(2)new刚刚创建的Thread子类的实例,即创建了线程对象;

(3)调用线程对象的shart()方法来启动线程;

代码示例:

package com.jiguiquan.www.newthread;

public class ExtendsThread {
	public static void main(String[] args) {
		for (int i = 0; i < 10; i++) {
			System.out.println(Thread.currentThread().getName()+"\t "+i);
			if (i == 5) {
				new FirstThreadTest().start();
				new FirstThreadTest().start();
			}
		}
	}
}

class FirstThreadTest extends Thread {
	public void run() {
		for (int i = 0; i < 10; i++) {
			System.out.println(Thread.currentThread().getName()+"\t "+i);
		}
	}
}

二、实现Runnable接口创建线程类

(1)创建一个类,实现Runnable接口,并实现该接口的run方法,该run()方法同样是该线程的线程执行体;

(2)new 刚刚创建的Runnable接口的实现类的实例,并以该实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象(在此环节,可以给此线程对象命名);

(3)调用线程对象的start()方法来启动线程;

代码示例:

package com.jiguiquan.www.newthread;

public class ImplementRunnable {
	public static void main(String[] args) {
		for (int i = 0; i < 10; i++) {
			System.out.println(Thread.currentThread().getName()+"\t "+i);
			if (i == 6) {
				new Thread(new RunnableThreadTest(), "新线程1").start();
				new Thread(new RunnableThreadTest(), "新线程2").start();
			}
		}
	}
}

class RunnableThreadTest implements Runnable {
	@Override
	public void run() {
		for (int i = 0; i < 10; i++) {
			System.out.println(Thread.currentThread().getName()+"\t "+i);
		}
	}
}

执行结果:

main	 0
main	 1
main	 2
main	 3
main	 4
main	 5
main	 6
main	 7
main	 8
新线程1	 0
新线程2	 0
main	 9
新线程2	 1
新线程2	 2
新线程2	 3
新线程2	 4
新线程2	 5
新线程1	 1
新线程1	 2
新线程1	 3
新线程1	 4
新线程1	 5
新线程1	 6
新线程1	 7
新线程1	 8
新线程1	 9
新线程2	 6
新线程2	 7
新线程2	 8
新线程2	 9

三、通过Callable和Future的方式创建线程

首先得到知道,在所有的现有已知的Thread的构造方法中并没有传入Callable实现对象的构造方法

image.png

思考:为什么没有传入Callable的构造方法,是Oracle公司的技术人员的疏忽吗?

肯定不是,而是jdk中早就预留了解决办法,继续往下看:

解决方案的思路是:找一个中间类,即实现了Callable接口,又实现了Runnable接口——适配性——面向接口编程的精髓——FutureTask

image.png

FutureTask类实现了Runnable接口

image.png

FutureTask的构造方法中可以传入一个实现了Callable接口的实现类

所以FutureTask就是我们要找的,可以从中穿针引线的中间类;

第一步,实现线程的调用:

package com.jiguiquan.www.newthread;

import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
/**
 ** 如何使用实现Callable接口的方式来创建线程?
 *	Callable接口有Runnable没有的优势;
 *	解决方案是:找一个中间类,即实现了Callable接口,又实现了Runnable接口
 * @author jiguiquan
 *
 */
public class CallableThreadTest {
	public static void main(String[] args) {
		//使用FutureTask这个中间类,就可以传入实现了Callable接口的CallableThread类
		FutureTask<Integer> futureTask = new FutureTask<>(new CallableThread());
		
		//如何启动?
		Thread t1 = new Thread(futureTask);
		t1.start();
	}
}

class CallableThread implements Callable<Integer>{
	@Override
	public Integer call() throws Exception {
		System.out.println("*******come in Callable"); 
		return 1024;
	}
}

执行结果:

image.png

显然,实现了Callable接口的线程,被顺利调用了;

第二步,如何获取Callable实现类线程的返回值:

核心api——futureTask.get() 获取线程中call()方法的返回值;

public static void main(String[] args) throws Exception {
		//使用FutureTask这个中间类,就可以传入实现了Callable接口的CallableThread类
		FutureTask<Integer> futureTask = new FutureTask<>(new CallableThread());
		
		//如何启动?
		Thread t1 = new Thread(futureTask);
		t1.start();
		
		System.out.println("返回的结果是:"+futureTask.get());
	}

执行结果:

image.png

是不是很简单!


四、使用Callable接口实现多个线程作业的异步计算方案

经典案例:ABCD四个人一起作业,A只需要1s完成任务,B和D也都需要1s中完成任务,但是C需要10秒钟才可以完成任务;

分析:我们可以让ABC三个人按原计划在main线程中作业,但是让C单独去旁边,重新开辟一个新线程进行计算作业,完成作业后,把计算结果交出来,然后和ABD三人的结果相加,即可等到了最终结果;

原单线程计算结果需要13秒;但是使用Callable的办法,只需要10秒就可以完成全部作业;

实现代码:

package com.jiguiquan.www.newthread;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;
/**
 ** 如何使用实现Callable接口的方式来创建线程?
 *	Callable接口有Runnable没有的优势;
 *	解决方案是:找一个中间类,即实现了Callable接口,又实现了Runnable接口
 * @author jiguiquan
 *
 */
public class CallableThreadTest {
	public static void main(String[] args) throws Exception {
		Long start = System.currentTimeMillis();
		//使用FutureTask这个中间类,就可以传入实现了Callable接口的CallableThread类
		FutureTask<Integer> futureTask = new FutureTask<>(new CallableThread());
		
		//如何启动?
		Thread t1 = new Thread(futureTask,"AA");
		Thread t2 = new Thread(futureTask,"BB");
		t1.start();
		t2.start();  //即使为futureTask创建两个线程,同时启动,futureTask也只会执行一次
		
		System.out.println("********多人计算********");
		TimeUnit.SECONDS.sleep(3);   //主线程,代表ABD三人计算,耗费3秒
		int result1 = 100;
		
		while (!futureTask.isDone()) { 
			//如果futureTask作业结果没有完成,那么线程在这里“自旋”等待,直到完成后,跳出循环
		}
		int result2 = futureTask.get();
		
		Long end = System.currentTimeMillis();
		System.out.println("计算的最终结果是:"+(result1+result2));
		System.out.println("总耗时为:"+(end-start)/1000+" 秒");
	}
}

class CallableThread implements Callable<Integer>{
	@Override
	public Integer call() throws Exception {
		System.out.println("*******come in Callable"); 
		TimeUnit.SECONDS.sleep(10);  //代表C一人计算,耗费的10秒
		return 1024;
	}
}

执行结果:

image.png

结论,遇到这种多人作业,其中一人短板的时候,可以让他单独开辟一个线程去作业,最后将计算结果交回给主线程,统计进去就可以;

为什么不使用其他方式:原因很简单,其他线程的实现方式,没有返回值,即算完后,主线程没有办法直到它的计算结果,肯定不满足要求;

**注意**

1、如果想要获取Callable线程的计算结果,必须要将get方法放到最后:

因为get()方法是一个堵塞操作,因为futureTask的任务还没有做完,所以它获取不到返回值,只有堵塞等待,如果将它放到前面,那么它会堵塞住整个主线程,肯定是不对的,主线程都没办法作业了;

2、即使为futureTask开辟2个线程,start两次,futureTask也只会执行1次,因为任务只有一个;

3、如果想让Callable实现类中的方法执行两次,那么就只能创建两个futureTask1和futureTask2了,分别启动;(间接说明了,futureTask才是线程Thread对象的target)


五、创建线程的3种方式的对比

1、使用Runnable、Callable的方式可实现多接口,Thread的方式只能单继承(虽然简单,但是太弱);

2、Runnable和Callable接口方式的对比:

  • 线程执行体不同——Callable接口实现的方法体是Call(),而Runnable接口实现的方法体是run();

  • 是否有返回值——Callable的任务执行后可以有返回值,而Runnable的任务时不能有返回值的;

  • 是否允许抛出异常——call()方法可以抛出异常,而run()方法不可以;

  • 运行Callable任务可以拿到一个Future对象,返回异步执行的结果。它还提供了检查计算是否完成的方法,以“自旋”的方式等待计算的完成,并get()到计算的结果;通过Future对象可以了解任务执行情况,甚至可以取消任务的执行,还可获取执行结果。(boolean cancel(boolean mayInterruptIfRunning)方法可用来执行取消任务的操作)

jiguiquan@163.com

文章作者信息...

留下你的评论

*评论支持代码高亮<pre class="prettyprint linenums">代码</pre>

相关推荐