Java开发笔记(九十七)利用Runnable启动线程

本文介绍了如何使用Runnable接口启动线程,避免每次创建新线程都需要定义新的线程类。通过举例说明了直接继承Thread类与实现Runnable接口的区别,特别是对于资源共享的处理。使用Runnable可以确保线程间正确共享资源,如售票任务中的车票共享,避免了资源重复计数的问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前面介绍了线程的基本用法,按理说足够一般的场合使用了,只是每次开辟新线程,都得单独定义专门的线程类,着实开销不小。注意到新线程内部真正需要开发者重写的仅有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开发笔记(序)章节目录

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值