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

Java开发笔记(九十七)利用Runnable启动线程的更多相关文章

  1. Java开发笔记(一百零三)线程间的通信方式

    前面介绍了多线程并发之时的资源抢占情况,以及利用同步.加锁.信号量等机制解决资源冲突问题,不过这些机制只适合同一资源的共享分配,并未涉及到某件事由的前因后果.日常生活中,经常存在两个前后关联的事务,像 ...

  2. Java开发笔记(十七)各得其所的多路分支

    前面提到条件语句的标准格式为“if (条件) { /* 条件成立时的操作代码 */ } else { /* 条件不成立时的操作代码 */ }”,乍看之下仿佛只有两个分支,一个是条件成立时的分支,另一个 ...

  3. Java开发笔记(序)章节目录

    现将本博客的Java学习文章整理成以下笔记目录,方便查阅. 第一章 初识JavaJava开发笔记(一)第一个Java程序Java开发笔记(二)Java工程的帝国区划Java开发笔记(三)Java帝国的 ...

  4. Java开发笔记(九十八)利用Callable启动线程

    前面介绍了如何利用Runnable接口构建线程任务,该方式确实方便了线程代码的复用与共享,然而Runnable不像公共方法那样有返回值,也就无法将线程代码的处理结果传给外部,造成外部既不知晓该线程是否 ...

  5. Java开发笔记(三十七)利用正则串分割字符串

    前面介绍了处理字符串的常用方法,还有一种分割字符串的场景也很常见,也就是按照某个规则将字符串切割为若干子串.分割规则通常是指定某个分隔符,根据字符串内部的分隔符将字符串进行分割,例如逗号.空格等等都可 ...

  6. Java开发笔记(七十九)利用反射技术操作私有属性

    早在介绍多态的时候,曾经提到公鸡实例的性别属性可能被篡改为雌性,不过面向对象的三大特性包含了封装.继承和多态,只要把性别属性设置为private私有级别,也不提供setSex这样的性别修改方法,那么性 ...

  7. Java开发笔记(一百一十四)利用Socket传输文本消息

    前面介绍了HTTP协议的网络通信,包括接口调用.文件下载和文件上传,这些功能固然已经覆盖了常见的联网操作,可是HTTP协议拥有专门的通信规则,这些规则一方面有利于维持正常的数据交互,另一方面不可避免地 ...

  8. Java开发笔记(九十九)定时器与定时任务

    前面介绍了线程的几种运行方式,不管哪种方式,一旦调用了线程实例的start方法,都会立即启动线程的事务处理.然而某些业务场景在事务执行时间方面有特殊需求,例如期望延迟若干时间之后才开始事务运行,又如期 ...

  9. Java开发笔记(一百一十七)AWT窗口

    前面介绍的所有Java代码,都只能通过日志观察运行情况,就算编译成class文件,也必须在命令行下面运行,这样的程序无疑只能给开发者做调试用,不能拿给一般人使用.因为普通用户早已习惯在窗口界面上操作, ...

随机推荐

  1. Codeforces 762C Two strings 字符串

    Cpdeforces 762C 题目大意: 给定两个字符串a,b\((len \leq 10^5)\),让你去b中的一个连续的字段,使剩余的b串中的拼接起来的两个串是a穿的子序列.最大化这个字串的长度 ...

  2. ELK安装配置简单使用

    ELK是三款软件的总称,包括了elasticsearch.logstash.kibana,其实在生产使用中,我们还需要使用到其他的更多辅助软件来更好更合理的收集展示数据. Elasticsearch: ...

  3. docker学习 (二)基本概念

    基本概念: Docker包括三个基本概念: 镜像(Image): 特殊的文件系统,提供容器运行时所需的程序.库.资源.配置文件.镜像不包含动态数据,内容在构建后不会被改变. 容器(Container) ...

  4. <正则吃饺子> :关于Collections中 比较器的简单使用

    在线文档地址: http://tool.oschina.net/apidocs/apidoc?api=jdk-zh sort public static <T extends Comparabl ...

  5. 基于keepalived的nginx高可用

    #nginx,keepalived安装略过 MASTER 节点配置文件(192.168.1.11) vi /etc/keepalived/keepalived.conf global_defs { # ...

  6. Robot FrameWork基础学习(二)

    在Robot Framework中,测试套件(Test Suite)主要是存放测试案例,而资源文件(Resource)就是用来存放用户关键字. 内部资源:Resource 外部资源: External ...

  7. why std::stack has separate top() and pop()

    SGI explanation: http://www.sgi.com/tech/stl/stack.html One might wonder why pop() returns void, ins ...

  8. 基于Laravel框架的一个简单易学的微信商城(新手必学)

    俗话说,麻雀虽小可五脏俱全呀! 今天分享的这个基于Laravel的小项目大概功能有这些: 1.实现会员登录.注册功能.数据双向验证功能.2.实现手机短信验证.邮件激活账号.邮件通知.3.ajax提交数 ...

  9. Django 中ORM 的使用

    一:Django 中 orm 的使用 1:手动新建一个数据库 2 :告诉Django连接哪个数据库 settings.py里配置数据库连接信息: #数据库相关的配置项 DATABASES ={ 'de ...

  10. HDU-6395 多校7 Sequence(除法分块+矩阵快速幂)

    Sequence Time Limit: 4000/2000 MS (Java/Others)    Memory Limit: 262144/262144 K (Java/Others)Total ...