最近与出版社合作,准备出一本Java多线程相关的书籍。但目前还在整理中,我先把一些我认为可以的章节先整合一部分出来。
本文中的主要内容有:(1)线程的概念。(2)线程的6种状态以及线程的生命周期。 (3)上下文context一词的理解。 (4)多线程的上下文切换。
首先介绍线程的概念。我们可以把线程看作是有活力、有生命力的,它是让看似安静的一段段代码活动起来的一种形式。在学习本章前读者如果已经进行过Java简易入门基础的学习,那么理解好线程会是一件非常容易的事情。当理解好线程的概念后,会由单线程向多线程进军。
1.1.1 线程是什么?
如今,智能手机与我们的生活密不可分。智能手机之所以这样吸引我们,与其有丰富多彩的应用程序有密切的关系,在使用这些应用程序,如查阅资讯、点击图标、拉取列表、播放视频和音乐等内容时,给与视觉和听觉上的享受。而且它们能及时地对我们的操作进行反馈,非常友好。这里的每一次反馈,都可能是有一个线程在专心致志地为我们服务。所以看似陌生的线程实际上已经默默服务多时。
每一个刚接触程序设计的初级人员,在学习了某种编程语言后,都会开始学着编写一些基本的短小的代码段。在Java中,这些短小的代码段一般我们会放入一个class,然后保存到一个扩展名是.java的文件。然后通过命令行或者集成开发环境工具编译,生成.class文件并让这个.class文件运行起来,得到我们想要的结果。
例如,有一个简单的模仿游戏打开宝箱得到礼品的程序代码,参考如下:
public class OpenBox {
public static void main(String\[\] args) {
// 设置宝箱中可能包含的水果
List<String> fruits = new ArrayList<String>();
fruits.add("green apple");
fruits.add("red apple");
fruits.add("banana");
fruits.add("cherry");
fruits.add("watermelon");
// 获取随机的下标,用于生成随机的水果,范围在:0 ~ 最大水果链表的下标
Random randomUtil = new Random();
int randomInt = randomUtil.nextInt(fruits.size());
System.out.println("打开宝箱,得到了" \+ fruits.get(randomInt) + "!");
}
}
我们将其以文件形式,保存到系统中,如图1.1所示。
图1.1 已经保存到系统的java的类文件
这样,该文件中就包含了我们想要运行的一小段程序,当我们使用Java的命令或者单击集成开发环境的“run”按钮启动时,程序就会运行起来,并且会按我们的编写好的逻辑,反馈相关信息给到我们。参考结果如图1.2所示。
图1.2 OpenBox的运行结果
以上这些看似简单的操作过程,至少能让我们更好地理解以下的几个概念:程序、进程、线程。
程序可以理解为个人的思维整合所设计和编写的一种有特殊意义的文本作品,其包含了一些有特殊含义的词汇、符号、数据以及短语缩写,我们经常俗称这些为代码。程序本身是一种静态的文本作品,但通过特殊的环境,能让其产生动态的逻辑和具备运算能力
上文中的OpenBox.java文件中的文本内容,就是程序。
进程则是在对某程序的运行过程,一般的,一份程序的一次运行能产生一个进程,进程是一个动态的概念。进程的运行是需要用到程序的内容的,更确切地讲,进程的运行是离不开程序,离不开程序中有特殊含义的文本。实际上进程运行中有专门存放这些文本的一个专用区域,这个区域就叫代码文本区域。程序与进程是一对多的关系,即可以对同一份程序,同时运行1个或多个进行。我们使用集成开发环境,点击“run”按钮的那一刻,OpenBox.java对应的一个进程就立刻产生了。
理解好程序和进程的关系,我们就可以对线程加以描述和解释。线程是比进程更细小的一级划分,线程可以利用进程所拥有的资源,并且能独立完成一项任务,如计算、打印显示信息等。在引入线程的操作系统中,通常都是把进程作为分配资源的基本单位,而把线程作为独立运行和独立调度的基本单位。进程与线程也是一对多的关系,即一个进程中至少有1个线程与之对应,如果一个进程有多个线程同时存在,那么就是多线程的进程了。上面的OpenBox.java程序运行的时候,产生了一个进程的同时,也产生了一个单线程与之对应,也就是说,当我们运行OpenBox.java程序的时候,它是一个单线程的进程。
代码文件、进程、线程的简单关系,可以参照图1.3。
图1.3 代码与进程、线程的关系示意图
【补充】近年来,随着大数据的兴起,对于大数据的处理要求比传统的普通数据处理要求有了更高的标准,Java应对大数据的处理也在不断地进步,特别是在开源社区中许多开发贡献者提供了许多大数据处理相关的组件和中间件。其中一个称为quasar的组件,实现了Java的纤程。纤程是比线程更小的一级划分,它所占用的系统资源更少,可以理解为更轻量级的一种特殊线程。一般的,在占用系统资源的大小而言,我们可以这样的排序:进程 > 线程>纤程。这里我们暂时先不展开,有兴趣的朋友可以通过quasar的开源地址:https://github.com/puniverse/quasar进行相关了解。
1.1.2 单线程与多线程
在前一小节的讲解中,我们可以知道一个进程至少包含一个线程。如果该进程只有一个线程,我们可以称呼它为单线程的进程。简单的应用程序,的确只要单线程就可以了,但一些复杂的应用程序,与人的交互和反馈比较多的时候,就需要多线程来完成这些任务。像一些大型的软件和游戏,这类的应用程序,一般都包含了多线程来丰富整个系统的功能,以做到强大的处理,或者多状态的即时反馈来增加趣味性。
例如图1.5的二三十年前的经典任天堂FC游戏,虽然是简单的游戏,但给许多青少年带来了无比的快乐。
图1.5 经典任天堂FC游戏集合
这些游戏中都加入了多线程,玩家可以获得到更多的快乐体验,给玩家更多的反馈。多线程能让一个游戏充满活力,让游戏中的物体活动起来。下面我们来看看这款游戏在该画面中的一些多线程,如图1.6所示。
图1.6 经典超级马里奥游戏中的线程示意
图中,玩家手柄控制的主角马里奥是一个独立的线程,能吃的会走动的蘑菇是一个独立的线程,而行走中的敌人香菇君也是一个独立的线程,另外背景音乐也是一个线程。当然,这幅游戏画面中,还有其他许多的线程在工作,这些众多的线程共同组成了这一生动活泼的经典游戏,读者可以自己思考这款游戏中还有哪些线程在默默地为游戏玩家提供服务。
这样的一个游戏实例,实际上也是一个进程。我们可以看到,一个进程可以包含一个或多个线程。单线程的进程虽然也存在,但实际上我们更多的会使用多线程的进程。
2.1 线程的状态
线程的状态是线程的基本属性。在Java中,线程一共分为6种状态,它们分别是:新建(NEW)、可运行(RUNNABLE)、阻塞(BLOCKED)、等待(WAITING)、调校时间的等待(TIMED_WAITING)、终止(TERMINATED)。本节内容将会简单介绍这6种状态,以及在程序中如何获取一个线程的当前状态等。
2.1.1 线程的6种状态
我们可以查阅Thread的源代码来了解,Java中线程的一些状态:
public class Thread implements Runnable {
/\* Make sure registerNatives is the first thing <clinit> does. */
private static native void registerNatives();
static {
registerNatives();
}
private volatile char name\[\];
private int priority;
private Thread threadQ;
private long eetop;
……//省略部分代码
/**
\* A thread state. A thread can be in one of the following states:
\* <ul>
\* <li>{[@link](https://my.oschina.net/u/393) #NEW}<br>
\* A thread that has not yet started is in this state.
\* </li>
\* <li>{[@link](https://my.oschina.net/u/393) #RUNNABLE}<br>
\* A thread executing in the Java virtual machine is in this state.
\* </li>
\* <li>{[@link](https://my.oschina.net/u/393) #BLOCKED}<br>
\* A thread that is blocked waiting for a monitor lock
\* is in this state.
\* </li>
\* <li>{[@link](https://my.oschina.net/u/393) #WAITING}<br>
\* A thread that is waiting indefinitely for another thread to
\* perform a particular action is in this state.
\* </li>
\* <li>{[@link](https://my.oschina.net/u/393) #TIMED_WAITING}<br>
\* A thread that is waiting for another thread to perform an action
\* for up to a specified waiting time is in this state.
\* </li>
\* <li>{@link #TERMINATED}<br>
\* A thread that has exited is in this state.
\* </li>
\* </ul>
*
\* <p>
\* A thread can be in only one state at a given point in time.
\* These states are virtual machine states which do not reflect
\* any operating system thread states.
*
\* @since 1.5
\* @see #getState
*/
public enum State {
/**
\* Thread state for a thread which has not yet started.
*/
NEW,
/**
\* Thread state for a runnable thread. A thread in the runnable
\* state is executing in the Java virtual machine but it may
\* be waiting for other resources from the operating system
\* such as processor.
*/
RUNNABLE,
/**
\* Thread state for a thread blocked waiting for a monitor lock.
\* A thread in the blocked state is waiting for a monitor lock
\* to enter a synchronized block/method or
\* reenter a synchronized block/method after calling
\* {@link Object#wait() Object.wait}.
*/
BLOCKED,
/**
\* Thread state for a waiting thread.
\* A thread is in the waiting state due to calling one of the
\* following methods:
\* <ul>
\* <li>{@link Object#wait() Object.wait} with no timeout</li>
\* <li>{@link #join() Thread.join} with no timeout</li>
\* <li>{@link LockSupport#park() LockSupport.park}</li>
\* </ul>
*
\* <p>A thread in the waiting state is waiting for another thread to
\* perform a particular action.
*
\* For example, a thread that has called <tt>Object.wait()</tt>
\* on an object is waiting for another thread to call
\* <tt>Object.notify()</tt> or <tt>Object.notifyAll()</tt> on
\* that object. A thread that has called <tt>Thread.join()</tt>
\* is waiting for a specified thread to terminate.
*/
WAITING,
/**
\* Thread state for a waiting thread with a specified waiting time.
\* A thread is in the timed waiting state due to calling one of
\* the following methods with a specified positive waiting time:
\* <ul>
\* <li>{@link #sleep Thread.sleep}</li>
\* <li>{@link Object#wait(long) Object.wait} with timeout</li>
\* <li>{@link #join(long) Thread.join} with timeout</li>
\* <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li>
\* <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li>
\* </ul>
*/
TIMED_WAITING,
/**
\* Thread state for a terminated thread.
\* The thread has completed execution.
*/
TERMINATED;
}
/**
\* Returns the state of this thread.
\* This method is designed for use in monitoring of the system state,
\* not for synchronization control.
*
\* @return this thread's state.
\* @since 1.5
*/
public State getState() {
// get current thread state
return sun.misc.VM.toThreadState(threadStatus);
}
……//省略部分代码
/\* Some private helper methods */
private native void setPriority0(int newPriority);
private native void stop0(Object o);
private native void suspend0();
private native void resume0();
private native void interrupt0();
private native void setNativeName(String name);
}
上面的一段Thread源代码,介绍了Thread的一些状态定义,其使用了enum(枚举)列举出了所有的状态,参考代码如下:
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}
可以看出在Java中,一共定义了6种线程的基本状态:
(1)新建(NEW):线程创建成果,但还没有调用start( )方法启动
(2)可运行(RUNNABLE):线程处于可执行的状态,表明该线程已经在Java虚拟机中执行,但可能它还在等待操作系统例如CPU等其他资源。
(3)阻塞(BLOCKED):线程处于阻塞,其实是指该线程正在等待一个监控锁,特别是在多线程下的场景等待另一个线程同步块的释放,如出现线程synchronized多线程同步关键字修饰的同步块或者方法中,或者之前调用了wait( )方法,后来被notify( )通知唤醒时重新进入synchronized多线程同步关键字修饰的同步块当中。
(4)等待(WAITING):线程出于等待状态,指的是该线程正在等待另一个线程执行某些特定的操作。譬如A线程调用了wait( )方法,进入等待状态,实际上该线程可能是等待B线程调用notify( )方法或notifyAll( )方法。又或者是A线程使用了C线程的join( )方法,所以按顺序在等待C线程的结束等。
(5)调校时间的等待(TIMED_WAITING):线程的这个状态与等待状态不同,这种是与时间相关的等待,一般是调用了某些设定等待时长参数的方法,导致该线程按该调校的时间进行等待。譬如sleep(500)或wait(1000)等。
(6)终止(TERMINATED):线程执行完毕的状态。
这里已经简单介绍完Java线程的6种状态,读者可能会一时间未能完全明白,但可以先大致记忆这6种状态,在后面的章节中,还会通过其他内容对这6种状态从侧面对加以分析。
2.1.2 线程状态的获取方法
可以在程序中使用某个线程的getState( )方法得到该线程目前这一刻的状态。实际上在多线程的环境下,一个线程的状态可能会在极短的时间内变化多次。先以一个简单的例子来看看一个线程的状态的变化,改写之前的ProgressBar01这个线程类的总的执行入口main( )方法。
参考代码如下:
public class ProgressBar01 extends Thread {
private int progressValue = 0; //进度条的目前进度值
private int accValue = 0; //累加辅助器,初始值为0;
@Override
public void run(){
for (int i = 0; i <= 300; i++){
System.out.println("我已经数到了第" + i + "个数字了哟, 目前进度:"
\+ progressValue + "%");
accValue++;
if (accValue ==3){
progressValue++;
accValue = 0;
}
}
}
public static void main(String\[\] args) {
ProgressBar01 countProgressBar = new ProgressBar01();
System.out.println("数数线程刚创建,还没有调用start()方法,它这时的状态是:"
\+ countProgressBar.getState());
countProgressBar.start();
System.out.println("数数线程调用了start()方法,它这时的状态是:"
\+ countProgressBar.getState());
try {
Thread.sleep(2);
System.out.println("主线程等待2毫秒,数数线程应在执行任务,它这时的状态是:"
\+ countProgressBar.getState());
Thread.sleep(2000);
System.out.println("主线程等待2秒,数数线程应该已完成任务,它这时的状态是:"
\+ countProgressBar.getState());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
运行后的参考结果如下:
数数线程刚创建,还没有调用start()方法,它这时的状态是:NEW
数数线程调用了start()方法,它这时的状态是:RUNNABLE
我已经数到了第0个数字了哟, 目前进度:0%
我已经数到了第1个数字了哟, 目前进度:0%
我已经数到了第2个数字了哟, 目前进度:0%
我已经数到了第3个数字了哟, 目前进度:1%
我已经数到了第4个数字了哟, 目前进度:1%
我已经数到了第5个数字了哟, 目前进度:1%
我已经数到了第6个数字了哟, 目前进度:2%
我已经数到了第7个数字了哟, 目前进度:2%
我已经数到了第8个数字了哟, 目前进度:2%
我已经数到了第9个数字了哟, 目前进度:3%
我已经数到了第10个数字了哟, 目前进度:3%
我已经数到了第11个数字了哟, 目前进度:3%
我已经数到了第12个数字了哟, 目前进度:4%
我已经数到了第13个数字了哟, 目前进度:4%
我已经数到了第14个数字了哟, 目前进度:4%
我已经数到了第15个数字了哟, 目前进度:5%
我已经数到了第16个数字了哟, 目前进度:5%
我已经数到了第17个数字了哟, 目前进度:5%
我已经数到了第18个数字了哟, 目前进度:6%
我已经数到了第19个数字了哟, 目前进度:6%
我已经数到了第20个数字了哟, 目前进度:6%
主线程等待2毫秒,数数线程应在执行任务,它这时的状态是:RUNNABLE
我已经数到了第21个数字了哟, 目前进度:7%
我已经数到了第22个数字了哟, 目前进度:7%
我已经数到了第23个数字了哟, 目前进度:7%
……
我已经数到了第296个数字了哟, 目前进度:98%
我已经数到了第297个数字了哟, 目前进度:99%
我已经数到了第298个数字了哟, 目前进度:99%
我已经数到了第299个数字了哟, 目前进度:99%
我已经数到了第300个数字了哟, 目前进度:100%
主线程等待2秒,数数线程应该已完成任务,它这时的状态是:TERMINATED
通过这个简单的例子,我们可以看到,数数线程在非常短的时间内,就发送了多次的状态变化。例如在刚创建好线程还没有启动时,数数线程的状态是NEW;当main线程立刻调用数数线程的start( )方法时,数数线程的状态变成了RUNNABLE;当数数线程刚开始工作不久,数到某个数字时,也就是数数线程正在运行中时,其状态也是RUNNABLE。当数数线程的工作完毕后,数数线程的状态变为了TERMINATED。
这个需要强调一下,Java的线程不能单独存在RUNNING这一个状态值,实际上运行中Java线程,其状态就是与使用了start( )方法之后的RUNNABLE状态相同,即RUNNABLE状态。
2.1.3 线程的活动情况获取方法
对于Java的线程运行情况的描述,除了获取状态的方法外,还有一个isAlive( )方法可以获得线程的活动情况,可以用于判断一个线程是否还活动(存活)。该方法返回的值为true或false,并不返回线程当前状态,但可以作为一种线程的描述方法。适当地修改上面的示例,加入isAlive( )方法。
参考代码如下:
public class ProgressBar01 extends Thread {
private int progressValue = 0; //进度条的目前进度值
private int accValue = 0; //累加辅助器,初始值为0;
@Override
public void run(){
for (int i = 0; i <= 300; i++){
System.out.println("我已经数到了第" + i + "个数字了哟, 目前进度:"
\+ progressValue + "%");
accValue++;
if (accValue ==3){
progressValue++;
accValue = 0;
}
}
}
public static void main(String\[\] args){
ProgressBar01 countProgressBar = new ProgressBar01();
System.out.println("数数线程刚创建,还没有调用start()方法,它这时的状态是:"
\+ countProgressBar.getState());
System.out.println("数数线程的活动情况:" + countProgressBar.isAlive());
countProgressBar.start();
System.out.println("数数线程调用了start()方法,它这时的状态是:"
\+ countProgressBar.getState());
System.out.println("数数线程的活动情况:" + countProgressBar.isAlive());
try {
Thread.sleep(2);
System.out.println("主线程等待2毫秒,数数线程应在执行任务,它这时的状态是:"
\+ countProgressBar.getState());
System.out.println("数数线程的活动情况:" + countProgressBar.isAlive());
Thread.sleep(2000);
System.out.println("主线程等待2秒,数数线程应该已完成任务,它这时的状态是:"
\+ countProgressBar.getState());
System.out.println("数数线程的活动情况:" + countProgressBar.isAlive());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
运行后的参考结果如下:
数数线程刚创建,还没有调用start()方法,它这时的状态是:NEW
数数线程的活动情况:false
数数线程调用了start()方法,它这时的状态是:RUNNABLE
数数线程的活动情况:true
我已经数到了第0个数字了哟, 目前进度:0%
我已经数到了第1个数字了哟, 目前进度:0%
我已经数到了第2个数字了哟, 目前进度:0%
我已经数到了第3个数字了哟, 目前进度:1%
主线程等待2毫秒,数数线程应在执行任务,它这时的状态是:RUNNABLE
数数线程的活动情况:true
我已经数到了第4个数字了哟, 目前进度:1%
我已经数到了第5个数字了哟, 目前进度:1%
我已经数到了第6个数字了哟, 目前进度:2%
我已经数到了第7个数字了哟, 目前进度:2%
……
我已经数到了第292个数字了哟, 目前进度:97%
我已经数到了第293个数字了哟, 目前进度:97%
我已经数到了第294个数字了哟, 目前进度:98%
我已经数到了第295个数字了哟, 目前进度:98%
我已经数到了第296个数字了哟, 目前进度:98%
我已经数到了第297个数字了哟, 目前进度:99%
我已经数到了第298个数字了哟, 目前进度:99%
我已经数到了第299个数字了哟, 目前进度:99%
我已经数到了第300个数字了哟, 目前进度:100%
主线程等待2秒,数数线程应该已完成任务,它这时的状态是:TERMINATED
数数线程的活动情况:false
可以看出,线程的活动情况,与线程的RUNNABLE状态基本一致,也是在线程start( )方法启动后为true,当线程结束时,线程的活动情况变为false。
2.2 线程生命周期概念
Java线程的生命周期包含Java的6种基本状态,它们按一定的顺序和条件进行转换。Java线程的生命周期如同人的生命周期相似,有一定的趋势,也有一些不可逆的过程。本节将介绍Java线程的生命周期。
2.2.1 线程的生命周期图谱
通过2.1的学习,我们可以知道Java的线程有6种状态,这些状态之间按一定的规则联系,线程的状态会转变,这些状态间的转变有一定的规律和顺序。线程的活动就像人经历成长一样,有始有终,有一个生命周期,所以可以将一个线程的活动总结成线程的生命周期图谱,如图2.1所示。
图2.1 线程的生命周期图谱
图2.1中,圆角矩形一共有6个,每一个圆角矩形对应Java线程的一个状态。可以看出,线程的生命周期,由NEW状态开始,由TERMINATED状态结束。其中RUNNABLE状态中包含了READY(就绪)、RUNNING(运行中)子状态,也就是说,READY、RUNNING都属于RUNNABLE状态。
当一个线程一开始调用start( )方法时,线程就会处于RUNNABLE状态,但可能一开始未得到CPU资源,所以会处于READY子状态,但一旦得到相关的CPU资源,则会进入RUNNING子状态。
注意,READY和RUNNING子状态并非是Java线程所定义的6大状态之一,也无法通过相关方法得到READY、RUNNING的值,这里只是对Java的RUNNABLE状态加以细分说明。线程处于RUNNABLE状态时,有可能会与WAITING、TIMED_WAITING、BLOCKED状态相互转换。当线程完成所设定的任务后,最终会到达TERMINATED状态,完成自己的使命,线程的生命周期结束。
2.2.2 多线程的上下文环境切换
阅读相关的技术文献时经常会出现“上下文”一词,上下文是计算机的专业术语,是对国外计算机文献常用词汇context的中文翻译。该词汇由字面上来看,非常难于理解,上下文,到底何为上,何为下,而文又是什么?对于初学者来说,“上下文”一词非常抽象,甚至查阅了许多的资料和文献,也未能给出一个非常易于理解的概念。
由于对context的中文替代词--上下文进行下定义和解释都非常的抽象,但为了让读者多一个参考,笔者也适当地给出一个解释,说明context的中文翻译“上下文 ”一词的含义。
上下文是一种特定的计算机语境词,是特定环境的抽象化,其常用于命名程序代码中的某个全局变量,或某作用域下的一种变量,该类变量能对某环境下的数据和配置进行管理,或者作为中间的重要环节,起到上层和下层内容的连接或交互,其可以贯穿某事物生命周期的上上下下、方方面面,所以称之为上下文。
如果觉得“上下文”一词太过难意会,不妨将其扩展为另外几个词汇来帮助理解,如上下文环境、上下文容器、上下文信息(其中,“上下文环境”一词,几乎可以在多种场景下表达出context的含义,所以本书后面对context的中文翻译,将直接使用上下文环境替代上下文一词)。例如一些企业级开发中经常用到的spring框架的ApplicationContext,就是一个上下文环境对象,它包含了整个spring项目的许多系统信息、对象信息和配置信息等,可以贯穿整个spring项目,让用户可以方便得到。
同样,每一个线程都有其自己的context,我们称之为线程的上下文环境。当在多线程的情况下,线程之间就可能会发生上下文环境切换。特别的,结合之前学习的线程的生命周期来看,当多线程的情况下,当其中一个线程由RUNNABLE状态转为BLOCKED、WAITING、TIMED_WAITING状态时,就会发生线程间的上下文环境切换。
我们可以给出这样的定义:线程的上下文环境切换是指在多线程的情况下,如果其中的一个线程进入了BLOCKED(阻塞)、WAITING(等待)以及TIMED_WAITING(调校时间的等待)的状态下,这时由另外的一个线程切换介入的过程就可以称之为线程的上下文切换。
来源:oschina
链接:https://my.oschina.net/u/2600078/blog/3164024