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文件,也必须在命令行下面运行,这样的程序无疑只能给开发者做调试用,不能拿给一般人使用.因为普通用户早已习惯在窗口界面上操作, ...
随机推荐
- 洛谷P2983 [USACO10FEB]购买巧克力Chocolate Buying
题目描述 Bessie and the herd love chocolate so Farmer John is buying them some. The Bovine Chocolate Sto ...
- finetune
微调的具体方法和技巧有很多种,这里总结了在不同场景下的微调技巧: 1)新数据集比较小且和原数据集相似.因为新数据集比较小(比如<5000),如果fine-tune可能会过拟合:又因为新旧数据集类 ...
- Data Guard 异构平台支持手册
背景 最简单的DG 环境是同数据库版本,同操作系统平台. 但有时需要在异构平台上来部署DG 环境,比如异构下使用DG 来进行数据迁移,从而降低停机时间和风险等等. 1. 查看主备库的Platform ...
- 转:Serializable---序列化
Serializable 今天在看代码的时候,看到[Serializable],不明白是什么意思.查阅了网上的一些资料,才明白这是指给类添加序列化的特性,即添加后它就可以进行序列化,那什 ...
- python UnicodeEncodeError: 'ascii' codec can't encode characters 解决办法
程序直接运行没有问题,一旦用nohup python test.py 执行遇中文就报python UnicodeEncodeError错误: 最终测试出的解决方法为,在开头添加红色的三句: #!/us ...
- Spring入门第十课
Spring表达式语言:SpEL Spring表达式语言(简称SpEL)是一个支持运行时查询和操作对象图的强大的表达式语言. 语法类似于EL:SpEL使用#{...}作为定界符,所有在大括号中的字符都 ...
- SQL中的drop,truncate和delete的区别
(1) DELETE语句执行删除的过程是每次从表中删除一行,并且同时将该行的删除操作作为事务记录在日志中保存以便进行进行回滚操作.TRUNCATE TABLE 则一次性地从表中删除所有的数据并不把 ...
- 牛客多校2 D-money(dp记录/贪心)
D-money 链接:https://www.nowcoder.com/acm/contest/140/D来源:牛客网 时间限制:C/C++ 1秒,其他语言2秒 空间限制:C/C++ 131072K, ...
- python学习笔记10 ----网络编程
网络编程 网络编程需要知道的概念 网络体系结构就是使用这些用不同媒介连接起来的不同设备和网络系统在不同的应用环境下实现互操作性,并满足各种业务需求的一种粘合剂.网络体系结构解决互质性问题彩是分层方法. ...
- Docker环境下的前后端分离项目部署与运维(七)Redis高速缓存
Redis高速缓存 利用内存保存数据,读写速度远超硬盘:可以减少I/O操作,降低I/O压力. 发红包.抢红包的数据可以存在高速缓存中,加快处理速度,不需要经过数据库 淘宝首页一些优惠活动商品等热数据可 ...