多线程有几种实现方式?如果被问到这个问题一定很头疼,因为百度一下随便就能出现各种各样的答案。两种、三种、四种、五种、六种、七种。。。
但本质上来讲,个人认为只有一种方式:实现Runnable接口。
先放个图:
1、实现Runnable接口
1 public class DemoThreadTask implements Runnable{ 2 @Override 3 public void run() { 4 // TODO Auto-generated method stub 5 } 6 7 public static void main(String[] args) { 8 DemoThreadTask task = new DemoThreadTask(); 9 Thread t = new Thread(task); 10 t.start(); 11 ... 12 } 13 }
实现Runnable接口,利用Runnable实例构造Thread,是较常用且最本质实现。
此构造方法相当于对Runnable实例进行一层包装,在线程t
启动时,调用Thread的run方法从而间接调用target.run():
1 public class Thread implements Runnable { 2 /* What will be run. */ 3 private Runnable target; 4 5 public void run() { 6 if (target != null) { 7 target.run(); 8 } 9 } 10 ... 11 }
2、继承Thread类
1 public class DemoThread extends Thread{ 2 @Override 3 //重写run方法 4 public void run() { 5 // TODO Auto-generated method stub 6 } 7 8 public static void main(String[] args) { 9 DemoThread t = new DemoThread(); 10 t.start(); 11 ... 12 } 13 }
这种实现方式是显示的继承了Thread,但从类图中我们可以看到,Thread类本身就继承自Runnable,所以继承Thread的本质依然是实现Runnable接口定义的run方法。
需要注意的是继承Thread方式,target对象为null,重写了run方法,导致方式1中的Thread原生的run方法失效,因此并不会调用到target.run()的逻辑,而是直接调用子类重写的run方法。
因为java是单根继承,此方式一般不常用。
3、实现Callable接口并通过FutureTask包装
1 public class DemoCallable implements Callable<String>{ 2 @Override 3 public String call() throws Exception { 4 // TODO Auto-generated method stub 5 return null; 6 } 7 8 public static void main(String[] args) throws Exception { 9 DemoCallable c = new DemoCallable(); 10 FutureTask<String> future = new FutureTask<>(c); 11 Thread t = new Thread(future); 12 t.start(); 13 ... 14 String result = future.get(); //同步获取返回结果 15 System.out.println(result); 16 } 17 }
实现Callable接口通过FutureTask包装,可以获取到线程的处理结果,future.get()方法获取返回值,如果线程还没执行完,则会阻塞。
这个方法里,明明没有看到run方法,没有看到Runnable,为什么说本质也是实现Runnable接口呢?
回看开篇的类图,FutureTask实现了RunnableFuture,RunnableFuture则实现了Runnable和Future两个接口。
因此构造Thread时,FutureTask还是被转型为Runnable使用。因此其本质还是实现Runnable接口。
至于FutureTask的工作原理,后续篇章继续分析。
4、匿名内部类
匿名内部类也有多种变体,上述三种方式都可以使用匿名内部类来隐式实例化。
1 public class Demo{ 2 3 public static void main(String[] args) throws Exception { 4 //方式一:Thread匿名内部类 5 new Thread(){ 6 @Override 7 public void run() { 8 // TODO Auto-generated method stub 9 } 10 }.start(); 11 12 //方式二:Runnable匿名内部类 13 new Thread(new Runnable() { 14 @Override 15 public void run() { 16 // TODO Auto-generated method stub 17 } 18 }).start(); 19 20 ... 21 } 22 }
匿名内部类的优点在于使用方便,不用额外定义类,缺点就是代码可读性差。
5、Lambda表达式
Lambda表达式是jdk8引入的,已不是什么新东西,现在都jdk10了。demo如下:
1 public class Demo{ 2 public static void main(String[] args) throws Exception { 3 new Thread(() -> System.out.println("running") ).start() ; 4 ... 5 } 6 }
如此简洁的Lambda表达式,有没有吸引到你呢?当然本质不多说,还是基于Runnable接口。
6、线程池
1 public class DemoThreadTask implements Runnable{ 2 @Override 3 public void run() { 4 // TODO Auto-generated method stub 5 System.out.println("running"); 6 } 7 8 public static void main(String[] args) { 9 DemoThreadTask task = new DemoThreadTask(); 10 ExecutorService ex = Executors.newCachedThreadPool(); 11 ex.execute(task); 12 ... 13 } 14 }
线程池与前面所述其他方式的区别在于执行线程的时候由ExecutorService去执行,最终还是利用Thread创建线程。
线程池的优势在于线程的复用,从而提高效率。
7、定时器
1 public class DemoTimmerTask { 2 3 public static void main(String[] args) throws Exception { 4 Timer timer = new Timer(); 5 timer.scheduleAtFixedRate((new TimerTask() { 6 @Override 7 public void run() { 8 System.out.println("定时任务1执行了...."); 9 } 10 }), 2000, 1000); 11 } 12 }
TimerTask的实现了Runnable接口,Timer内部有个TimerThread继承自Thread,因此绕回来还是Thread + Runnable。
总结,多线程的实现方式,在代码中写法千变万化,但其本质万变不离其宗。
本质都是实现 Runnable 。
参考:
https://www.jianshu.com/p/7950ea349dbb