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开发笔记(序)章节目录

posted @ 2019-05-13 21:36  pinlantu  阅读(1275)  评论(0编辑  收藏  举报