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开发笔记(序)章节目录》
Java开发笔记(九十七)利用Runnable启动线程的更多相关文章
- Java开发笔记(一百零三)线程间的通信方式
前面介绍了多线程并发之时的资源抢占情况,以及利用同步.加锁.信号量等机制解决资源冲突问题,不过这些机制只适合同一资源的共享分配,并未涉及到某件事由的前因后果.日常生活中,经常存在两个前后关联的事务,像 ...
- Java开发笔记(十七)各得其所的多路分支
前面提到条件语句的标准格式为“if (条件) { /* 条件成立时的操作代码 */ } else { /* 条件不成立时的操作代码 */ }”,乍看之下仿佛只有两个分支,一个是条件成立时的分支,另一个 ...
- Java开发笔记(序)章节目录
现将本博客的Java学习文章整理成以下笔记目录,方便查阅. 第一章 初识JavaJava开发笔记(一)第一个Java程序Java开发笔记(二)Java工程的帝国区划Java开发笔记(三)Java帝国的 ...
- Java开发笔记(九十八)利用Callable启动线程
前面介绍了如何利用Runnable接口构建线程任务,该方式确实方便了线程代码的复用与共享,然而Runnable不像公共方法那样有返回值,也就无法将线程代码的处理结果传给外部,造成外部既不知晓该线程是否 ...
- Java开发笔记(三十七)利用正则串分割字符串
前面介绍了处理字符串的常用方法,还有一种分割字符串的场景也很常见,也就是按照某个规则将字符串切割为若干子串.分割规则通常是指定某个分隔符,根据字符串内部的分隔符将字符串进行分割,例如逗号.空格等等都可 ...
- Java开发笔记(七十九)利用反射技术操作私有属性
早在介绍多态的时候,曾经提到公鸡实例的性别属性可能被篡改为雌性,不过面向对象的三大特性包含了封装.继承和多态,只要把性别属性设置为private私有级别,也不提供setSex这样的性别修改方法,那么性 ...
- Java开发笔记(一百一十四)利用Socket传输文本消息
前面介绍了HTTP协议的网络通信,包括接口调用.文件下载和文件上传,这些功能固然已经覆盖了常见的联网操作,可是HTTP协议拥有专门的通信规则,这些规则一方面有利于维持正常的数据交互,另一方面不可避免地 ...
- Java开发笔记(九十九)定时器与定时任务
前面介绍了线程的几种运行方式,不管哪种方式,一旦调用了线程实例的start方法,都会立即启动线程的事务处理.然而某些业务场景在事务执行时间方面有特殊需求,例如期望延迟若干时间之后才开始事务运行,又如期 ...
- Java开发笔记(一百一十七)AWT窗口
前面介绍的所有Java代码,都只能通过日志观察运行情况,就算编译成class文件,也必须在命令行下面运行,这样的程序无疑只能给开发者做调试用,不能拿给一般人使用.因为普通用户早已习惯在窗口界面上操作, ...
随机推荐
- hdu3518 Boring Counting[后缀排序]
裸的统计不同的重复出现子串(不重叠)种数的题.多次使用后缀排序要注意小细节.y数组在重复使用时一定要清空,看那个line25 +k就明白了 ,cnt也要清空,为什么就不说了 #include<b ...
- 洛谷 P2285 [HNOI2004]打鼹鼠
题目描述 鼹鼠是一种很喜欢挖洞的动物,但每过一定的时间,它还是喜欢把头探出到地面上来透透气的.根据这个特点阿牛编写了一个打鼹鼠的游戏:在一个n*n的网格中,在某些时刻鼹鼠会在某一个网格探出头来透透气. ...
- BZOJ3772:精神污染
浅谈主席树:https://www.cnblogs.com/AKMer/p/9956734.html 题目传送门:https://www.lydsy.com/JudgeOnline/problem.p ...
- 一般项目转为Maven项目所遇到的问题
最近搞CI,准备使用Maven,但以前的项目不是Maven项目,需要把项目转换为Maven项目.这遇到几个小问题,一是jar包的依赖,二是从本地仓库取出依赖jar包. 由于没有本地仓库,要手动添加ja ...
- 如何让Surface RT支持网站的flash
Go to the desktop version of IE10, hit the click ALT button on your keyboard, click on and then Comp ...
- μC/OS-II与RT-Thread对比—任务调度
在任务调度器的实现上,μC/OS-II和RT-Thread都采用了位图调度(bitmap scheduling),任务优先级的值越小则代表具有越高的优先级,主要区别在于实现形式,是采用多级队列的形式, ...
- POJ1330(LCA入门题)
Nearest Common Ancestors Time Limit: 1000MS Memory Limit: 10000K Total Submissions: 23388 Accept ...
- 常见的post提交数据类型
原文地址:https://www.cnblogs.com/yoyoketang/p/6771696.html 常见的post提交数据类型有四种: 1.第一种:application/json:这是最常 ...
- appium+python 快速给真机安装app
#coding=utf-8from appium import webdriverfrom time import sleepimport os,time,unittest '''给手机快速装app的 ...
- mongodb主从复制配置
dbpath=/home/mongodb/data logpath=/home/mongodb/log/mongodb.log logappend=true port= fork=true noaut ...