文章转自http://286.iteye.com/blog/2292038
谢谢博主的总结!
进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。
线程(Thread)有时被称为轻量级进程(Lightweight Process,LWP),是程序执行流的最小单元。一个标准的线程由线程ID,当前指令指针(PC),寄存器集合和堆栈组成。另外,线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只占用运行所需最基本的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。同一进程中的多个线程之间可以并发执行。
每个正在系统中运行的程序实例都是一个进程,进程也可能是整个程序或者是部分程序的动态执行,每个进程包含一至多个线程。而线程是一组指令的集合,或者是程序的特殊段,它可以在程序里独立执行。
这里就已经明确了进程与线程的关系,一个进程至少包含一个线程,而进程是应用程序的基本运行实例。在windows操作系统中执行ctrl+alt+del 键并启动任务管理器就可以查看到当前用户下进程的运行状态,如下图所示:
而在Linux 操作系统下执行ps -ef 或相关命令就也可以查看当前用户下相关的进程信息:
所以无论是Java虚拟机还是其他应用程序,一旦运行必然会产生至少一个进程,如上图所示,我一共执行了3次java HelloWorld 命令,这样就产生了3个JVM 进程,他们之间有着不同的进程号与相关存储空间,所以他们之间是无法进行资源共享的。所以进程间一般是相互隔离的,进程间的相互通信与调用需要特殊手段来实现。
一个进程包含多个线程,在单个进程中同时运行多个线程完成不同的工作就称为多线程。
Java中多线程的实现方法
在Java中有两种基本的线程实现方式,一种是继承Thread 类;另一种是实现Runnable 接口:
1)继承Thread 类并重写Thread 类的run方法
- public class ThreadA extends Thread {
- @Override
- public void run() {
- // 这里是要运行的方法
- }
- }
2)实现Runnable 并实现Runnable 接口的run方法
- public class ThreadB implements Runnable {
- public void run() {
- // 这里是要运行的方法
- }
- }
两种方式都是在run() 方法体中编写我们要并发执行的代码,推荐使用第二种方式,第二种方式更加灵活,线程实现类还可以继承其他有用的父类。
在单CPU或不支持多线程并发技术的操作系统中,多个线程的运行其实不是并发的,这是因为最开始,线程只是用于分配单个处理器的处理时间的一种工具。但随着硬件科技的发展操作系统本身已经支持多个处理器,如果每个线程就都可以分配到一个单独的处理器工作,那么就真正进入了“并发运算”状态,但是线程间能否进行“并行”或“串行”运行需要操作系统说的算。
所以事实上线程间是以争夺CPU资源来实现线程运行的,在同一时间同一CPU中只会有一个线程正在运行,其他线程都会处于等待状态。
线程的状态
当然线程不会只有运行和等待两种状态,线程一般具有六种基本状态,分别为:
NEW:至今尚未启动的线程处于这种状态。
RUNNABLE:可运行或正在运行线程的线程状态。
BLOCKED:受阻塞并等待某个监视器锁的线程状态。
WAITING:无限期地等待另一个线程来执行唤醒操作的状态。
TIMED_WAITING:指定时间后会被自动唤醒的WAITING状态。
TERMINATED:已退出的线程处于这种状态。
1)当我们新建一个线程后该线程的状态则为 NEW,此时该线程尚未被执行,所以 NEW状态表明该线程是新建还未执行过的线程。
2)因为新创建的线程不会自动运行,我们需要调用线程的 start() 方法来运行该线程。此后我们就正式启动了该线程,当 start()方法执行完毕后本线程就进入了 RUNNABLE状态,说明该线程已经就绪随时可以被执行。处于可运行状态的线程已经在 Java虚拟机中运行了,但该线程的 run() 方法并不一定会立即执行,线程可能还需要等待操作系统中的其他资源,比如与其他 RUNNABLE状态线程竞争,与 WAITING状态线程竞。但一旦该线程获取到了CPU资源则会立即执行 run() 方法执行其中的代码。
其实RUNNABLE 可以理解为两种状态的统称:RUNNABLE与 RUNNING(注意 RUNNING 并不是标准状态)。因为在单 CPU的计算机系统中,无法同时运行多个线程,因此可能有多个线程已经处于可运行(RUNNABLE )状态,但同一个时刻却只能有一个线程处于运行(RUNNING)状态。对于多个处于可运行状态的线程是由 CPU时间片轮转等不同算法来对线程进行调度管理的。
正是因为多线程间需要竞争 CPU资源,所以处于 RUNNING 状态的线程未必会把该线程下 run() 方法的所有代码都执行完,可能只执行到了一半,CPU资源就被其他处于 RUNNABLE 状态的线程抢走了,就这样循环往复的竞争下去,直到各自都完成了 run() 方法的全部代码。
3)当一个线程的 run() 方法完整的执行完毕后或抛出异常、错误,该线程就会进入 TERMINATED 状态,证明该线程已经退出不需要再继续执行。
4)很多情况下线程并不会非常顺利的按照之前三个步骤顺序执行,当运行状态中的线程调用Object.wait(), Thread.join(),LockSupport.park()等方法后线程就会进入 WAITING状态。此时运行时线程(本线程)将放弃已经获取的 CPU等相关资源进入等待区(稍后会讲)。处于等待状态的线程不会主动去竞争 CPU资源,直到收到明确的唤醒命令:notify 或notifyAll。
5)与 WAITING状态不同,TIMED_WAITING状态是由于Thread.sleep(时间)、Object.wait(时间) 、Thread.join(时间)、LockSupport.parkNanos、LockSupport.parkUntil等操作而产生的,这些操作明确的指定了等待的时间,当等待时间一过该线程就会自动被唤醒。
6)BLOCKED是受阻塞并且正在等待监视器锁的某一线程的线程状态,也就是说 BLOCKED状态出现在同步的方法与对象中调用了 wait()方法的情况,如果几个线程都在某一同步资源上竞争,同一时间只会有一个线程获取到该资源的监视器锁,其他线程此时都在等待获取监视器锁,所以这些等待获取锁的线程状态就为阻塞状态(BLOCKED)。
线程监视器
Java 中的监视器支持两种线程:互斥和协作。Java 虚拟机通过对象锁来实现互斥,允许多个线程在同一共享数据上独立而互不干扰地工作。协作则是通过 Object 类的wait 方法和notify 方法来实现,允许多个线程为了同一目标而共同工作。
前面提到的第一种同步——互斥,可以在多线程环境中互斥地执行一段被称作监视区域的代码。在任何时候,特定监视器上只会有一个线程执行监视区域。通常,互斥只在多个现场需要共享数据或其他资源时显得重要,如果两个线程并没有任何公有数据或资源,它们通常会互不干扰,也就不需要互斥执行。可是如果Java虚拟机实现不是基于时间片的,即使没有线程共享数据,一个不被阻塞的高优先级的线程也将妨碍其他低优先级的线程。高优先级的线程会独占CPU,从而让低优先级的线程永远得不到CPU时间和执行的机会。
另一种提到的被监视器所支持的同步是协作。互斥帮助线程在访问共享数据时不被其他线程干扰,而协作则是帮助多线程间共同工作。
我们可以将监视器比作一桩建筑,在这座建筑中有三间房间,其中一个特殊房间存放着数据与资源,但是这个房间只允许一个线程进入,并且一旦线程进入该房间,那么该线程就全权拥有了此房间,到它离开之前,该线程可以独占地访问房间中的全部数据资源。如果将房间概念换成监视器,则进入建筑就会被称为——进入监视器;进入特殊房间称为——获得监视器;占据特殊房间称为——持有监视器;离开特殊房间称为——释放监视器;离开建筑称为——退出监视器。
对于一个监视器来说,监视区域是最小的、不可分割的代码块。也就是说在同一个监视器中,监视区域只会同时被一个线程执行,即使同时有多个并发的线程,监视器会保证在监视区域上同一时间只会执行一个线程。一个线程想要进入监视器唯一的途径就是到达该监视器监视区域的开始处,也就是监视器的入口。而线程想要继续执行线程的 run 方法唯一的途径就是获得监视器,也就是获得特殊房间的所有权。
下图就是Java中所使用的监视器模型:
这种监视器被称为“等待并唤醒”监视器(或称为”发信号并继续“监视器)。监视器分为三个区域:入口区,监视器持有者和等待区,其中入口区和等待区可以同时存在多个线程,而持有者只能有一个。
1)当一个新建的线程调用 start()方法后,该线程就会进入监视器的入口区。
2)此时假设监视器中只进入了刚才那一个线程,那么这个线程就会顺利的获得监视器持有权,进入那个神秘的房间,进行线程中 run()方法的执行,直至 run()方法执行完毕或中途抛出异常中断,则此线程会从监视器的出口退出监视器,该线程的流程就为1-->2-->5。
3)另一种情况就是当线程进入入口区后,发现监视器的所有区域都有线程在等待或执行。只有监视器所有者线程退出监视器或主动的交出监视器持有权后,这些等待的线程才能开始竞争,争夺监视器的所有权。
4)只要监视器持有者不再存在,入口区内所有的线程就都会参与竞争。如果监视器持有者正常退出,则参与竞争的线程只有入口区内的所有线程。如果监视器持有者主动转让持有权,并通过 notify()方法唤醒等待区内的一个线程加入竞争,则竞争者为入口区所有线程与等待区某一线程。如果监视器持有者通过 notifyAll()方法唤醒全部等待区线程加入竞争,则竞争者为入口区所有线程与等待区所有线程。
5)如果是监视器持有者执行了等待命令,那么它就会释放监视器持有权,并进入等待区进行等待,直至后续的监视器持有者唤醒。
6)无论是入口区所有线程竞争,还是入口区与等待区线程竞争,最终结果都是只有一个线程胜出获得监视器的持有权,从而开始执行线程代码。
等待并唤醒监视器之所以还被称为发信号并继续监视器,原因是在一个线程做了唤醒操作(发信号)后,它还会继续持有监视器并继续执行,经过一段时间后,唤醒线程释放监视器,等待线程才会苏醒,所以唤醒与苏醒不会是立刻执行完成的。就犹如去叫醒一个睡梦中的人,虽然你已经试着去叫醒他,但是他却未必能立刻醒来。
等待线程将自身挂起是因为监视器保护数据并不处于它想要继续执行的正确状态。同样,唤醒线程执行唤醒操作后还将继续执行,所以在继续执行的过程中它有可能又修改了数据,这就有可能导致等待线程无法继续工作。还有一种情况就是,第三个线程有可能在唤醒线程释放了监视器后,抢先一步获得了监视器,而且这个线程有可能会修改监视器保护数据的状态。所以一次唤醒操作往往被看成一次“提醒”,告诉等待线程“数据已经是你想要的状态了”。每次等待线程苏醒的时候,它都要再次检查状态,以确保可以继续完成工作。如果它不是所需状态,那么这个线程可能会再次执行等待命令,甚至放弃等待退出监视器。
以上可能不是很好理解,不过没关系,结合下面的文章实例再反复思考就明白了。
对象锁
通过之前文章的学习,我们知道堆内存和方法区是被所有线程共享的,所以Java程序需要为两种多线程访问的数据进行协调:堆中的实例变量和方法区中的类变量。因为Java栈中的数据是属于线程私有的,所以程序不需要协调这部分局部变量。
在Java虚拟机中,每个对象和类在逻辑上都是和一个监视器关联的,对于对象来说,相关联的监视器保护对象的实例变量。对于类来说,监视器保护类的类变量。如果一个对象没有实例变量或一个类没有类变量,相关联的监视器就什么都不监视。
为了实现监视器的排他监视能力,Java虚拟机为每一个对象和类都关联一个锁(也可称为互斥体(mutex))。一个锁就像一种任何时候只允许一个线程“拥有”的特权。线程访问实例变量或者类变量不需要获取锁,但如果线程获取了锁,那么在它释放锁之前,就没有其他线程可以获取同样数据的锁了。
”锁住一个对象“就是获取对象相关联的监视器,这样估计更好理解一些。
类锁实际上用对象锁实现,当Java虚拟机装载一个class文件时,它会创建一个java.lang.Class实例,当锁住一个类的时候,实际上锁住的是那个类的 Class对象。
一个线程可以多次对同一个对象上锁。对于每一个对象来说,Java虚拟机维护一个计数器,记录着对象加了多少次锁。没有被锁的对象的计数器值为0,但一个线程第一次获得锁的时候,计数器变为1,每加一次锁计数器就加1。但是只有已经拥有了这个对象的锁的线程才能对该对象再次加锁。在它释放锁之前,其他的线程无法对这个对象加锁。每当线程释放一次锁,计数器就会减1。当计数器为0时说明这个对象的锁已经释放,已经不存在锁,其他线程才可以使用它。
Java虚拟机中的一个线程在它到达监视区域开始处时请求一个锁。在Java中,有两种监视区域:同步语句和同步方法。Java程序中每一个监视区域都和一个对象引用相关联这,当一个线程到达监视区域的第一条指令的时候,线程必须对该引用对象加锁,否则线程不允许执行其中的代码。一旦获得了锁,线程就进入了被保护的代码,当线程离开保护代码的时候,不管它是如何离开的,它都会释放相关对象上的锁。
Java变成人员不需要自己动手加锁,对象锁是在Java虚拟机内部使用的。在Java程序中,只需要编写同步语句或同步方法就可以标志一个监视区域。当Java虚拟机运行你的程序时,每一次进入一个监视区域的时候,它每次都会自动上锁对象或类。
同步的支持
Java语言提供了两种内置方式来标志监视区域:同步语句和同步方法。这两个机制实现了同步的互斥。
1)同步语句
要建立一个同步语句,在一个计算对象引用的表达式中加上 synchronized关键字就可以了。
- synchronized (this) {
- //...
- }
如果没有获得当前对象(this)的锁,在同步语句块内的语句是不会被执行的。如果使用的不是 this引用,而是用一个表达式获得对另一个对象的引用,在线程执行语句体之前,需要获得那个对象的锁。如果用表达式获得对Class类实例的引用,就需要锁住那个类。
JVM中,方法内的同步语句块使用 monitorenter和 monitorexit两个操作码。
monitorenter:弹出objectref(对象引用),获得和objectref相关联的锁。
monitorexit:弹出objectref(对象引用),释放和objectref相关联的锁。
当Java虚拟机遇到 monitorenter的时候,它获得栈中 objectref锁引用的那个锁。如果线程已经拥有了那个对象的锁,锁的计数器将加1.线程中每条 monitorexit指令都会引起锁计数器减1.当计数器值变成0的时候,监视器就被释放了。
2)同步方法
要同步整个方法,只需要在方法修饰符中加上 synchronized关键字。
- public synchronized void eatApple(String name) {
- System.out.println(name + " eat the apple.");
- }
Java虚拟机调用同步方法或者从同步方法中返回没有使用任何特别的操作码。当虚拟机解析对方法的符号引用时,它判断这个方法是否是同步的。如果是同步的,虚拟机就在调用方法之前获取一个锁。对于实例方法来说,虚拟机在方法将要被掉用的时候获取对象相关联的锁。对于类方法来说,它获取方法所属的类的锁(其实就是Class对象上锁)。当同步方法执行完毕的时候,不管是正常结束还是抛出异常,虚拟机都会释放这个锁。
线程状态转换
对象锁与监视器等概念基本已经掌握,接下来就可以通过一些实例来看一看线程的各种状态间是如何转换的,我们又可以如何控制它们。
正常情况下线程的状态是应该随着时间轴从左至右发展,但是很多特殊的场景下我们不得不人为的去干预线程的执行,将线程的状态人为的进行转换,最终能够达到预期的结果。
1)NEW --> RUNNABLE:新创建未执行的线程状态为 NEW,调用 start方法后该线程状态会变为RUNNABLE.
- Thread t = new Thread();
- System.out.println(t.getState());
- t.start();
- System.out.println(t.getState());
- //打印结果:
- NEW
- RUNNABLE
2)RUNNABLE -->RUNNING:一旦可运行状态线程获取了 CPU资源,那么它就会持有监视器并开始执行 run()方法中的代码,虽然在标准的线程状态中没有 RUNNING这个状态,统一用 RUNNABLE来表示,但我们心中可以这样理解。
- public class ThreadB extends Thread {
- public void run() {
- System.out.println("id:" + this.getId());
- System.out.println("state:" + this.getState());
- }
- public static void main(String[] args) {
- Thread t = new ThreadB();
- System.out.println("id:" + t.getId());
- System.out.println("state:" + t.getState());
- t.start();
- }
- }
- //打印结果:
- id:9
- state:NEW
- id:9
- state:RUNNABLE
3)RUNNING --> TERMINATED:线程完成 run()方法的执行或抛出异常中断的情况下会从 RUNNABLE状态变成 TERMINATED终止状态。
- Thread t = new Thread();
- t.start();
- System.out.println("state:" + t.getState());
- //人为终止线程
- t.interrupt();
- System.out.println("state:" + t.getState());
- //打印结果:
- state:RUNNABLE
- state:TERMINATED
4)RUNNING --> WAITING(对应上图①流程):持有监视器正在运行的线程调用 Object.wait(),Thread.join(),LockSupport.park()方法将会处于等待状态。
- public class ThreadA implements Runnable {
- public void run() {
- try {
- wait();
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- public static void main(String[] args) {
- Thread t = new Thread(new ThreadA());
- t.start();
- System.out.println("state:" + t.getState());
- }
- }
- //打印结果:
- Exception in thread "Thread-0" java.lang.IllegalMonitorStateException
- at java.lang.Object.wait(Native Method)
- at java.lang.Object.wait(Object.java:485)
- at ThreadA.run(ThreadA.java:4)
- at java.lang.Thread.run(Thread.java:662)
- state:RUNNABLE