Java开发笔记(九十七)利用Runnable启动线程
前面介绍了线程的基本用法,按理说足够一般的场合使用了,只是每次开辟新线程,都得单独定义专门的线程类,着实开销不小。注意到新线程内部真正需要开发者重写的仅有run方法,其实就是一段代码块,分线程启动之后也单单执行该代码段而已。因而完全可以把这段代码抽出来,把它定义为类似方法的一串任务代码,这样能够像调用公共方法一样多次调用这段代码,也就无需另外定义新的线程类,只需命令已有的Thread去执行该代码段就好了。
在Java中定义某个代码段,则要借助于接口Runnable,它是个函数式接口,唯一需要实现的只有run方法。之所以定义成函数式接口的形式,是因为要给任务方法套上面向对象的壳,这样才好由外部去调用封装好的任务对象。现在有个阶乘运算的任务,希望开个分线程计算式子“10!”的结果,那便定义一个实现了Runnable接口的任务类FactorialTask,并重写run方法补充求解“10!”的代码逻辑。编写完成的FactorialTask类代码示例如下:
// 定义一个求阶乘的任务 private static class FactorialTask implements Runnable { @Override public void run() { int product = 1; for (int i=1; i<=10; i++) { product *= i; } PrintUtils.print(Thread.currentThread().getName(), "阶乘结果="+product); } }
接着创建FactorialTask类的任务对象,并通过线程类的构造方法传入该任务,这就实现了在分线程中启动阶乘任务的功能。下面是外部给阶乘任务开启新线程的代码例子:
// 通过Runnable创建线程的第一种方式:传入普通实例 FactorialTask task = new FactorialTask(); new Thread(task).start(); // 创建并启动线程
鉴于阶乘任务的实现代码很短,似无必要定义专门的任务类,不妨循着比较器Comparator的旧例,采取匿名内部类的方式书写更为便捷。于是可在线程类Thread的构造方法中直接填入实现后的Runnable任务代码,具体的调用代码如下所示:
// 通过Runnable创建线程的第二种方式:传入匿名内部类的实例 new Thread(new Runnable() { @Override public void run() { int product = 1; for (int i=1; i<=10; i++) { product *= i; } PrintUtils.print(Thread.currentThread().getName(), "阶乘结果="+product); } }).start(); // 创建并启动线程
由于Runnable是函数式接口,因此完全可以使用Lambda表达式加以简化,下面便是利用Lambda表达式取代匿名内部类的任务线程代码:
// 通过Runnable创建线程的第三种方式:使用Lambda表达式 new Thread(() -> { int product = 1; for (int i=1; i<=10; i++) { product *= i; } PrintUtils.print(Thread.currentThread().getName(), "阶乘结果="+product); }).start(); // 创建并启动线程
虽说Runnable接口的花样会比直接从Thread派生的多一些,但Runnable方式依旧要求实现run方法,看起来像是换汤不换药,感觉即使没有Runnable也不影响线程的运用,最多在编码上有点繁琐罢了。可事情没这么简单,要知道引入线程的目的是为了加快处理速度,多个线程同时运行的话,必然涉及到资源共享及其合理分配。比如火车站卖动车票,只有一个售票窗口卖票的话,明显卖得慢,肯定要多开几个售票窗口,一起卖票才卖得快。假设目前还剩一百张动车票,此时开了三个售票窗口,这样等同于启动了三个售票线程,每个线程都在卖剩下的一百张票。倘若不采取Runnable接口,而是直接定义新线程的话,售票线程的定义代码应该类似下面这般:
// 单独定义一个售票线程 private static class TicketThread extends Thread { private int ticketCount = 100; // 可出售的车票数量 public TicketThread(String name) { setName(name); // 设置当前线程的名称 } @Override public void run() { while (ticketCount > 0) { // 还有余票可供出售 ticketCount--; // 余票数量减一 // 以下打印售票日志,包括售票时间、售票线程、当前余票等信息 String left = String.format("当前余票为%d张", ticketCount); PrintUtils.print(Thread.currentThread().getName(), left); } } }
然后分别创建并启动三个售票线程,就像以下代码所示的那样:
//创建多个线程分别启动,三个线程每个各卖100张,总共卖了300张票 new TicketThread("售票线程A").start(); new TicketThread("售票线程B").start(); new TicketThread("售票线程C").start();
猜猜看,上面三个售票线程总共卖了多少张票,实地运行测试代码后发现,这三个线程竟然卖掉了三百张票,而不是期望的一百张余票。究其原因,乃是各线程售卖的车票为专享而非共享,每个线程只认可自己掌握的车票,不认可其它线程的车票,结果导致三个线程各卖各的,加起来一共卖了三百张票。所以单独定义的线程类处理独立的事务倒还凑合,要是处理共享的事务就难办了。
如果采用Runnable接口来定义售票任务,就可以很方便地进行资源共享,只要命令三个线程同时执行售票任务即可。下面是开启三个线程运行售票任务的代码例子:
//只创建一个售票任务,并启动三个线程一起执行售票任务,总共卖了100张票 Runnable seller = new Runnable() { private int ticketCount = 100; // 可出售的车票数量 @Override public void run() { while (ticketCount > 0) { // 还有余票可供出售 ticketCount--; // 余票数量减一 // 以下打印售票日志,包括售票时间、售票线程、当前余票等信息 String left = String.format("当前余票为%d张", ticketCount); PrintUtils.print(Thread.currentThread().getName(), left); } } }; new Thread(seller, "售票线程A").start(); // 启动售票线程A new Thread(seller, "售票线程B").start(); // 启动售票线程B new Thread(seller, "售票线程C").start(); // 启动售票线程C
因为100张余票位于同一个售票任务seller里面,所以这些车票理应为执行任务的线程们所共享。运行上述的任务测试代码,观察到如下的线程工作日志:
16:27:21.077 售票线程C 当前余票为98张 16:27:21.083 售票线程A 当前余票为96张 16:27:21.083 售票线程C 当前余票为95张 16:27:21.077 售票线程B 当前余票为97张 ………………………这里省略中间的日志…………………… 16:27:21.118 售票线程B 当前余票为2张 16:27:21.118 售票线程A 当前余票为1张 16:27:21.118 售票线程C 当前余票为4张 16:27:21.118 售票线程B 当前余票为0张
可见此时三个售票线程一共卖掉了100张车票,才符合多窗口同时售票的预期功能。
更多Java技术文章参见《Java开发笔记(序)章节目录》