Java开发笔记(九十六)线程的基本用法
每启动一个程序,操作系统的内存中通常会驻留该程序的一个进程,进程包含了程序的完整代码逻辑。一旦程序退出,进程也就随之结束;反之,一旦强行结束进程,程序也会跟着退出。普通的程序代码是从上往下执行的,遇到分支语句则进入满足条件的分支,遇到循环语句总有跳出循环的时候,遇到方法调用则调用完毕仍然返回原处,之后继续执行控制语句或者方法调用下面的代码。总之一件事情接着一件事情处理,前一件事情处理完了才能处理后一件事情,这种运行方式被称作“串行处理”。串行处理的代码结构清晰,但同一时刻只能执行某段代码,也就是说,只要一个CPU就足够应付了。但现在不管电脑还是手机,中央处理器都是多核CPU,一个设备上集成了四个或更多的CPU,而串行处理的程序自始至终都只用一个CPU,显然无法发挥多核CPU的性能优势。既然串行存在效率问题,就需要另一种允许同时执行多项任务的处理方式,该方式被称作“并行处理”。所谓并行处理,指的是程序在同一时刻进行不止一个事务的处理,比如看网络视频时一边下载一边播放,这样就能提高程序的运行效率。
并行处理的思想体现到程序调度上面,又有多进程与多线程两种方式,多进程仿佛孙悟空拔毫毛变出许多小孙悟空,每只小孙悟空都四肢齐全、有鼻子有眼睛,完全是孙悟空的克隆版本,而且可以单独上阵打斗。至于线程则为进程中的一条控制流,它是操作系统能够调度的最小执行单元,线程犹如人的手,吃饭穿衣都靠它。多线程仿佛哪吒变出三头六臂,每只手臂都拿着一把兵器,战斗力顿时倍增。不过变出来的手臂依附于哪吒本人,要是哪吒挂了,再多的手臂也只能拜拜,当然只要进程还在运行,多些线程绝对有助于加快程序的办事速度。况且一个线程占用的系统资源远小于一个进程,想想看,三个孙悟空有六只手臂同时占据了三个人的空间,而三头六臂的哪吒也有六只手臂但只占据一个人的空间,很明显多线程的性价比要优于多进程。
一个进程默认自带一个线程,这个默认线程被称作主线程,要想在主线程之外另外开辟新线程,就用到了Java的Thread线程类。Thread类封装了线程的生命周期及其调度操作,程序员只需由Thread类派生出新的线程类,并重写run方法添加具体的业务逻辑即可。下面便是一个计数器线程的代码例子,功能很简单,仅仅循环打印0-999的计数日志:
// 定义一个计数器线程
private static class CountThread extends Thread {
@Override
public void run() {
for (int i=0; i<1000; i++) { // 一千次计数,并打印每次计数的日志
// getName方法获取当前线程的名称,getId方法获取当前线程的编号
PrintUtils.print(getName(), "当前计数值为"+i);
}
}
}
上面代码在打印日志时调用了自己写的print方法,该方法主要打印当前时间、当前线程名称、具体事件描述等信息,为节约代码篇幅,往后的线程内部日志都通过print方法来打印,以下是该方法的实现代码:
//定义了线程专用的日志打印工具
public class PrintUtils {
// 打印线程的运行日志,包括当前时间、当前线程名称、具体事件描述等信息
public static void print(String threadName, String event) {
SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss.SSS");
String dateTime = sdf.format(new Date());
String desc = String.format("%s %s %s", dateTime, threadName, event);
System.out.println(desc);
}
}
定义好了计数器线程,轮到外部启动它倒也容易,先创建一个计数器线程的对象,再调用该对象的start方法,接着计数器线程便会自动执行run方法的内部代码。外部启动计数器线程的调用代码示例如下:
CountThread thread = new CountThread(); // 创建一个计数器线程
thread.start(); // 开始线程运行
运行上述的调用代码,观察到如下的线程运行日志,可见一个名叫Thread-0的分线程正常跑起来了:
17:36:01.049 Thread-0 当前计数值为0
17:36:01.051 Thread-0 当前计数值为1
17:36:01.051 Thread-0 当前计数值为2
17:36:01.051 Thread-0 当前计数值为3
………………………这里省略余下的日志……………………
除了start方法,Thread类还提供了其它一些有用的方法。倘若程序先后启动两个线程,那么通常来说,先启动的线程比后启动的线程要跑得快些。可是有时候业务上又需要后启动的线程跑得更快,此时可调用指定线程的join方法,该方法字面上的意思是“加入”,实际作用却是“插队”。凡是调用了join方法的线程,它们的内部代码相较其它线程会优先处理,由于不同线程之间是并行展开着的,因此优先的意思并非一定会插到前面,而是尽量安排先执行,最终的执行顺序还得由操作系统来决定。下面是演示线程插队功能的代码例子:
// 测试线程的插队操作
private static void testJoin() {
// 创建第一个计数器线程
CountThread thread1 = new CountThread();
thread1.start(); // 第一个线程开始运行
// 创建第二个计数器线程
CountThread thread2 = new CountThread();
thread2.start(); // 第二个线程开始运行
try {
thread2.join(); // 第二个线程说:“我很着急,我要插队”
} catch (InterruptedException e1) { // 插队行为可能会被中断,需要捕获中断异常
e1.printStackTrace();
}
}
只有两个分线程的话,尚能通过join方法区分插队的线程与普通线程;要是分线程多于两个,好几个线程都调用join方法,都提出本线程想插队,操作系统又该如何伺候这些猴急的线程们?就算是插队,也得有个插队顺序吧,不是谁嗓门大谁就能排到前面的,所以还需定义一个规矩来区分插队动作的轻重缓急。于是Thread类又提供了优先级设置方法setPriority,调用该方法即可指定每个线程的优先级大小,数值越大的表示它拥有越高的优先级,就越应该安排到前面执行。如此一来,通过优先级数值的大小,能够有效辨别各个线程的排队顺序,再也不必烦恼要到哪里插队了。给多个线程分别设置优先级的代码示例如下:
// 测试线程的优先级顺序
private static void testPriority() {
// 创建第一个计数器线程
CountThread thread1 = new CountThread();
thread1.setPriority(1); // 第一个线程的优先级为1
thread1.start(); // 第一个线程开始运行
// 创建第二个计数器线程
CountThread thread2 = new CountThread();
thread2.setPriority(9); // 第二个线程的优先级为9,值越大优先级越高
thread2.start(); // 第二个线程开始运行
}
正常情况下,分线程的内部代码执行完毕后,该线程会自动退出运行。但有时需要提前结束线程,或者先暂停线程,等到时机成熟再恢复线程,Thread类也确实提供了相关的处理方法,例如stop方法用于停止线程运行,suspend方法用于暂停线程运行,resume方法用于恢复线程运行。然而Java同时注明了这三个方法都已经过时,为啥?缘由在于它们仨是不安全的,当一个线程正在欢快运行的时候,突然外部咔嚓一下,不由分说把它干翻,这本身就是很危险的举动,因为谁也无法预料此时线程在做什么、线程意外终止会产生什么后果等等。比如某个线程正在写文件,现在不管三七二十一干掉该线程,结果很可能造成文件损坏。故而由外部强行干预线程的运行实在不是一个好点子,理想的做法是:外部给分线程发个纸条,表示你被炒鱿鱼了,咱通情达理也没立刻赶你走,你收拾收拾差不多了再走也不迟。
这样的话,很自然想到在线程内部增加一个标志位,分线程每隔一阵子便检查该标志,一旦发现标志位发生改变,就自动择机退出运行。据此可以重新编写包含标志位的计数器线程,并在run方法中不时地检查该标志,新线程的定义代码示例如下:
// 定义一个主动检查运行标志的线程
private static class ActiveCheckThread extends Thread {
private boolean canRun = true; // 能否运行的标志
// 设置当前线程能否继续运行的标志
public void setCanRun(boolean canRun) {
this.canRun = canRun;
} @Override
public void run() {
for (int i=0; i<1000; i++) {
PrintUtils.print(getName(), "当前计数值为"+i);
if (!canRun) { // 如果不允许运行,就打印停止运行的日志,并跳出线程的循环处理
PrintUtils.print(getName(), "主动停止运行");
break;
}
}
}
}
上述的线程代码提供了setCanRun方法给外部调用,通过该方法即可设置当前线程能否继续运行的标志。外部在启动ActiveCheckThread线程之后,再调用setCanRun方法,就实现了给分线程递纸条的功能。下面是ActiveCheckThread线程的调用代码例子:
// 线程自己主动检查是否要停止运行
private static void testActiveCheck() {
// 创建一个会自行检查运行标志的线程
ActiveCheckThread thread = new ActiveCheckThread();
thread.start(); // 开始线程运行
try {
Thread.sleep(50); // 睡眠50毫秒
} catch (InterruptedException e) { // 睡眠可能会被打断,需要捕获中断异常
e.printStackTrace();
}
thread.setCanRun(false); // 告知该线程不要再跑了,请择机退出
}
运行上面的测试代码,观察到以下的线程日志,可见分线程按照标志位提前停止运行了。
………………………这里省略前面的日志……………………
16:38:18.457 Thread-0 当前计数值为14
16:38:18.457 Thread-0 当前计数值为15
16:38:18.457 Thread-0 当前计数值为16
16:38:18.458 Thread-0 主动停止运行
设置标志位的办法固然可行,但不是很好用,原因有二:其一,分线程要很积极主动的去检查标志位,可是人算不如天算,标志位的检查代码毕竟不能塞得到处都是,那么在遗忘的角落就没法响应外部的信号了;其二,设置标志位是个新增的方法,那么每个线程类的标志设置方法都不尽相同,外部又怎知甲乙丙丁各自提供了哪些设置方法呢?好在Thread类另外提供了线程中断机制,分线程倒也不必新增能否运行的标志,原来的代码结构可以保持不变。在中断机制里,凡是属于正常的业务逻辑,外部概不横加干涉,只有在耗时较久的场合,例如睡眠、等待之类的情况,才可能会收到中断信号,也就是中断异常InterruptedException。于是分线程只管捕捉中断异常,若无异常则照常运行;若有异常则进入中断分支,对相关事宜妥善处理一下,即可退出线程运行。
据此改造先前的计数器线程,在每次计数之后增加调用sleep方法,且睡眠期间允许接收中断信号,另外补充异常处理的try/catch语句,并在异常分支进行善后工作。改造后的计数器线程PassiveInterruptThread代码示例如下:
// 定义一个被动接受中断信号的线程
private static class PassiveInterruptThread extends Thread {
@Override
public void run() {
try {
for (int i=0; i<1000; i++) {
PrintUtils.print(getName(), "当前计数值为"+i);
Thread.sleep(10); // 睡眠10毫秒,睡眠期间允许接收中断信号
}
} catch (InterruptedException e) { // 收到了异常中断的信号,打印中断日志并退出线程运行
PrintUtils.print(getName(), "被中断运行了");
}
}
}
接下来外部启动计数线程之后,调用interrupt方法往分线程发送中断信号,注意这个interrupt方法为Thread类的自有方法,每个线程都适用。下面是PassiveInterruptThread线程的调用代码例子:
// 线程被动接收外部的中断信号
private static void testPassiveInterrupt() {
// 创建一个会接收外部中断信号的线程
PassiveInterruptThread thread = new PassiveInterruptThread();
thread.start(); // 开始线程运行
try {
Thread.sleep(50); // 睡眠50毫秒
} catch (InterruptedException e) {
e.printStackTrace();
}
thread.interrupt(); // 不管你正在干什么,先停下来再说
}
运行上面的线程调用代码,观察到如下的线程日志,可见分线程的确收到了外部的中断信号:
………………………这里省略前面的日志……………………
17:04:33.284 Thread-0 当前计数值为3
17:04:33.294 Thread-0 当前计数值为4
17:04:33.304 Thread-0 当前计数值为5
17:04:33.305 Thread-0 被中断运行了
更多Java技术文章参见《Java开发笔记(序)章节目录》
Java开发笔记(九十六)线程的基本用法的更多相关文章
- Java开发笔记(六十)匿名内部类的优势
前面依次介绍了简单接口和扩展接口,给出的范例都是自定义的接口代码,其实Java系统本身就自带了若干行为接口,为了更好地理解系统接口的详细用法,接下来还是从一个基础的例子出发,抽丝剥茧地逐步说明接口的几 ...
- Java开发笔记(六十一)Lambda表达式
前面介绍了匿名内部类的简单用法,通过在sort方法中运用匿名内部类,不但能够简化代码数量,还能保持业务代码的连续性.只是匿名内部类的结构仍显啰嗦,虽然它省去了内部类的名称,但是花括号里面的方法定义代码 ...
- Java开发笔记(六十二)如何定义函数式接口
前面介绍了Lambda表达式的用法,从实践中发现它确实极大地方便了开发者,然而不管是匿名内部类还是Lambda表达式,所举的例子都离不开各类数组的排序方法,倘使Lambda表达式仅能用于sort方法, ...
- Java开发笔记(六十三)双冒号标记的方法引用
前面介绍了如何自己定义函数式接口,本文接续函数式接口的实现原理,阐述它在数组处理中的实际应用.数组工具Arrays提供了sort方法用于数组元素排序,可是并未提供更丰富的数组加工操作,比如从某个字符串 ...
- Java开发笔记(六十四)静态方法引用和实例方法引用
前面介绍了方法引用的概念及其业务场景,虽然在所列举的案例之中方法引用确实好用,但是显而易见这些案例的适用场合非常狭窄,因为被引用的方法必须属于外层匿名方法(即Lambda表达式)的数据类型,像isEm ...
- Java开发笔记(六十五)集合:HashSet和TreeSet
对于相同类型的一组数据,虽然Java已经提供了数组加以表达,但是数组的结构实在太简单了,第一它无法直接添加新元素,第二它只能按照线性排列,故而数组用于基本的操作倒还凑合,若要用于复杂的处理就无法胜任了 ...
- Java开发笔记(六十六)映射:HashMap和TreeMap
前面介绍了两种集合的用法,它们的共性为每个元素都是唯一的,区别在于一个无序一个有序.虽说往集合里面保存数据还算容易,但要从集合中取出数据就没那么方便了,因为集合居然不提供get方法,没有get方法怎么 ...
- Java开发笔记(六十七)清单:ArrayList和LinkedList
前面介绍了集合与映射两类容器,它们的共同特点是每个元素都是唯一的,并且采用二叉树方式的类型还自带有序性.然而这两个特点也存在弊端:其一,为啥内部元素必须是唯一的呢?像手机店卖出了两部Mate20,虽然 ...
- Java开发笔记(六十八)从泛型方法探究泛型的起源
前面介绍各种容器之时,通过在容器名称后面添加包裹数据类型的一对尖括号,表示该容器存放的是哪种类型的元素.这样一来总算把Java当中的各类括号都凑齐了,例如包裹一段代码的花括号.指定数组元素下标的方括号 ...
- Java开发笔记(六十九)泛型类的定义及其运用
前面从泛型方法的用法介绍到了泛型的起源,既然单个方法允许拥有泛化的参数类型,那么一个类也应当支持类级别的泛化类型,例如各种容器类型ArrayList.HashMap等等.一旦某个类的定义代码在类名称后 ...
随机推荐
- 003-更改pip的源让下载安装更加快捷
1 找到pip目录 C:\Python36\Lib\site-packages\pip\models 2 修改下面的index.py文件 将url设定为 https://pypi.douban.com ...
- mpvue微信小程序分包
## 微信小程序分包(mpvue) 使用mpvue分包示例:1.下载vue脚手架(先有node环境,v8.12.0) npm install -g vue-cli 2.先用vue初始化一个mpvue小 ...
- JQ对象和原生DOM对象
相同点:两者本质上都是DOM元素. 不同点:JQ对象是在原生DOM对象上进行了一次封装,使开发人员使用起来更简洁.高效. 两者之间用法也完全不同,很说初学者经常混淆. 其实区分两者并不难, 1.语法不 ...
- JAVA 内部类 (二)
一.为什么要使用内部类 为什么要使用内部类?在<Think in java>中有这样一句话:使用内部类最吸引人的原因是:每个内部类都能独立地继承一个(接口的)实现,所以无论外围类是否已经继 ...
- RTC驱动程序分析
drivers\rtc\rtc-s3c.c s3c_rtc_init platform_driver_register s3c_rtc_probe ...
- PLSQL ORA-12154 TNS无法解析指定的连接标识符
若你的机子上Windows 64位操作系统, 将PL Sql 的默认安装目录 Program Files (x86) 文件夹改为Program Files 或者别的便可以了
- CF-796B
B. Find The Bone time limit per test 2 seconds memory limit per test 256 megabytes input standard in ...
- war,jar包是啥
http://www.blogjava.net/athrunwang/archive/2011/11/18/364191.html 经常听开发说war,jar,car,这些是个什么东东呢? .jar ...
- C++多态性:虚函数的调用原理
多态性给我们带来了好处:多态使得我们可以通过基类的引用或指针来指明一个对象(包含其派生类的对象),当调用函数时可以自动判断调用的是哪个对象的函数. 一个函数说明为虚函数,表明在继承的类中重载这个函数时 ...
- RXJS组件间超越父子关系的相互通信
RXJS组件间超越父子关系的相互通信 用到这个的需求是这样的: 组件A有数据变化,将变化的数据流通知组件B接收这个数据流并做相应的变化 实例化RXJS的subject对象 import { Injec ...