Java多线程学习之任务的创建以及在线程中执行任务
传统的创建任务、驱动任务的方式
1.继承Thread类
通过继承Thead类,并重写run方法,在run方法里面编码具体的任务,调用对象的start方法驱动任务。
public class TestThread extends Thread{ private int count = 5; //创建介绍String形参的构造器,一般参数作为任务的名称,以区分不同的线程 public TestThread(String name) { super(name); } //run方法体,就是要执行的任务 public void run() { while (true) { System.out.println(this); if(--count == 0) break; } } //重写toString方法实现可读性更好的线程打印 public String toString() { return getName()+" count="+count; } public static void main(String[] args) {
//创建5个任务对象,并且调用start方法让线程执行任务; for(int i=0;i<5;i++) { new TestThread("#Thread "+i).start(); }
//这里往下都是当前线程(即是执行main函数这个线程),而自定义的TestThread任务都是在main线程中开启新的线程来驱动 } }
输出:
#Thread 0 count=5
#Thread 3 count=5
#Thread 1 count=5
#Thread 2 count=5
#Thread 1 count=4
#Thread 3 count=4
#Thread 3 count=3
#Thread 0 count=4
#Thread 3 count=2
#Thread 1 count=3
...
#Thread 0 count=1
#Thread 4 count=3
#Thread 4 count=2
#Thread 2 count=2
#Thread 4 count=1
#Thread 2 count=1
从log可以看出,这些任务被调动的机会很随机,这取决于线程调度器,而调度器的调度机制又取决于运行的操作系统,即是在同一个平台上,运行多次每次都会出现不一致的结果。
2.实现Runnable接口
这种方式与继承Thread类的方式类似,任务类实现Runnable接口的run方法,但是这个任务只是被定义了,并没有产生线程的行为,所以还必须把任务类作为创建Thread对象的构造器形参,并通过调用Thread实例的start方法,使任务附着到线程中。
public class LiftOff implements Runnable{ private int count=4; private static int taskId = 0; private int id = taskId++; private String getStatus() { return "task"+id+"("+count+")"; } //定义任务 public void run() { while (count-->0) { System.out.println(getStatus()); } } public static void main(String[] args) { for(int i=0;i<3;i++) {
//将任务对象实例通过构造器传递给Thread实例,使任务附着到线程上得到执行 new Thread(new LiftOff()).start(); } } }
输出:
task1(3)
task2(3)
task0(3)
task2(2)
task1(2)
task2(1)
task0(2)
task2(0)
task1(1)
task0(1)
task1(0)
task0(0)
同继承Thread类执行线程一样,任务都是被“随机”执行。不过,这种方式下,任务与线程明确分开在不同的对象,架构比前一种要清晰。
使用Executor
从JavaSE5起,提供了执行器(Executor)来简化并发编程。使用Executor,我们不需要收到创建Thread实例,将任务交给Excutor,至于把任务附着到线程上执行,又或者管理线程的生命周期,都不需要我们处理。
public class CacheThreadPoolTest { public static void main(String[] args) {
//通过静态方法创建newCacheThreadPool的Exector,可以创建FixedThreadPool、SingleThreadExecutor等类型的Executor ExecutorService exec = Executors.newCachedThreadPool(); for(int i=0;i<5;i++) {
//将任务加到Executor上 exec.execute(new LiftOff()); }
//开始顺序关闭Executor开启的线程,之后不能再添加任务到Executor,任务执行完后相应的线程将被关闭 exec.shutdown(); } }
Executor很好地分离的任务及驱动任务的线程角色,客户端只需要专注于任务定义,而线程创建管理则交给Executor来负责。下面看看几种不同的Executor的特点:
CachedThreadPool:Executor首选的线程池,通常会创建与所需数量相同的线程,在回收旧线程时停止创建新线程;
FixedThreadPool:一次性预先创建固定数量的线程,可以节省后期线程创建的开销,并且防止滥用线程;
SingleThreadExecutor:类似线程数量为1的FixedThreadPool,适用于所以任务都要在同一个线程中执行的情景(执行次序为任务被提交的先后顺序),还可以用于执行短任务;
public class LiftOff implements Runnable{ private int count=4; private static int taskId = 0; private int id = taskId++; private String getStatus() { return "task"+id+"("+count+")"; } public void run() { try { while (count-->0) { System.out.println(getStatus()); Thread.sleep(2000); } }catch (Exception e) { e.printStackTrace(System.out); } } }
public class SingleThreadExecutorTest { public static void main(String[] args) { ExecutorService executor = Executors.newSingleThreadExecutor(); for(int i=0;i<3;i++) { executor.execute(new LiftOff()); }
//这里一定要关闭线程,否则主线程将一直阻塞
executor.shutdown(); } }
输出:
task0(3)
task0(2)
task0(1)
task0(0)
task1(3)
task1(2)
task1(1)
task1(0)
task2(3)
task2(2)
task2(1)
task2(0)
从任务中产生返回值
无论是实现Runnable接口还是继承Thread类,都只是单独执行任务,没能返回值。要想从任务中产生返回值,需要使用Callable接口,其中重写call方法并得到参数化的Future类型的返回值。实现Callable接口的任务需要Executor使用submit来提交。
//定义任务,实现Callable,返回值类型为String
public class TaskWithResult implements Callable<String>{ private static int taskId = 0; private int id = taskId++; public String toString() { return "#task"+id; } public String call() throws Exception { return this.toString(); } }
public class CallableTest { public static void main(String[] args) throws Exception{ ExecutorService executor = Executors.newSingleThreadExecutor(); List<Future<String>> futureList = new ArrayList<Future<String>>(); for(int i=0;i<3;i++) {
//通过executor的submit来提交,submit返回参数化的Future类型 futureList.add(executor.submit(new TaskWithResult())); } executor.shutdown(); for(Future<String> future: futureList) {
//Future的get可以得到任务的真正返回值 System.out.println(future.get()); } } }
输出:
#task0
#task1
#task2
需要注意的是,从Future中得到返回值时使用了get,但是get会引起阻塞直到任务完成并返回,为了避免阻塞,可以用isDone方法来查询Future是否完成。