进程是指运行中的应用程序,每一个进程都有自己独立的内存空间,对一个应用程序可以同事启动多个进程。例如,对于IE浏览器,每打开一个窗口就启动了一个新的浏览器进程。同样,每次执行JDK的java.exe程序,就启动了一个独立的java虚拟机进程,该进程的任务是解析并执行java程序代码。
线程是指进程中的一个执行流程,有时候也可以称之为执行情景。一个进程可以由多个线程组成,即在一个进程中可以同时运行多个不同的线程,他们分别执行不同的任务。当进程内的多个线程同时运行时,这种运行方式称之为并发运行。许多服务器程序,如数据库服务器和Web服务器,都支持并发运行,这些服务器能同时相应来自不同客户的请求。
线程与进程的主要区别在于:每个进程都需要操作系统为其分配独立的内存地址空间,而同一个进程中的所有线程在同一块地址空间中工作,这些线程可以共享同一块内存和系统资源,比如共享一个对象或者共享一个已经打开的文件。
13.1 java程序的运行机制
在java虚拟机进程中,执行程序代码的任务是由线程来完成的。每个线程都有一个独立的程序计数器和方法调用栈(method invocation stack);
程序计数器:也称为PC寄存器,当线程执行一个方法时,程序计数器指向方法区中下一条要执行的字节码指令
方法调用栈:简称方法栈,用来跟踪线程运行中一系列的方法调用过程,栈中的元素称之为栈帧。没当线程调用一个方法时,就会向方法栈压入一个新帧,帧用来存储方法的参数,局部变量和运行过程中的临时数据。
栈帧由3部分组成:
局部变量区:存放局部变量和方法参数
操作数栈:是线程的工作区,用来存放运算过程中生成的临时数据。
栈数区:为线程执行指令提供相关的信息,包括:如何定位到位于堆区和方法区的特定数据,如何正确退出方法或者异常终端方法。
每当用java命令启动一个java虚拟机进程,java虚拟机就会创建一个主线程,该线程从程序的main()方法开始执行。下面举例如下:
public class Sample{
private int a;
public int method(){
int b=0;
a++;
b=a;
return b;
}
public static void main(String[] args){
Sample s = null;
int a=0;
s = new Sample();
a = s.method();
System.out.println(a);
}
}
主线程从main()方法的程序代码开始执行,当它开始执行method()方法的“a++”操作时,运行时数据区的状态如下:(p394)
当主线程执行“a++”操作时,他能根据method()方法的栈帧的栈数据取的有关信息,正确地定位到堆区的Sample对象的实例变量a,把它的值加1。
当method方法执行完毕后,它的栈帧就会从方法栈中弹出,它的局部变量b结束声明周期。main()方法的栈帧成为当前栈,主线程继续执行main()方法。
方法区存放了线程所执行的字节码指令,堆区存放了线程所操作的数据(以对象的形式存放),java栈区则是线程的工作区,保存了线程的运行状态。
另外,计算机中机器指令的真正执行者是CPU,线程必须获得CPU的执行权,才能执行一条指令
13.2. 线程的创建和启动
上一节提到了java虚拟机的主线程,它从启动类的main()方法开始执行。此外,用户还可以创建自己的线程,它将和主线程并发运行。创建线程有两种方式:
a.扩展java.lang.Thread类
b.实现java.lang.Runnable类
13.3. 扩展java.lang.Thread类
Thread类代表线程类,它的最主要的方法是:
run():包含线程运行时所执行的代码
start():用于启动线程
用户的线程类只需要继承Thread类,覆盖Thread类的run()方法。在Thread类中,run()方法定义如下:
public void run()
该方法没有声明抛出任何异常,Thread类的子类也不能抛出任何异常。
public class Machine extends Thread
public void run(){
System.out.println("run run() ing...");
}
public static void main(String[] args){
Machine machine = new Machine();
machine.start();//启动machine进程
}
}
当运行java Machine命令时,java虚拟机首先创建并启动主线程,主线程的任务是执行main()方法,main()方法创建了一个machine对象,然后调用它的start()方法启动Machine线程。Machine线程的任务是执行它的run()方法。
1.主线程与用户自定义的线程并发运行
public class Machine extends Thread{
public void run(){
for(int a=0;a<100;a++){ system.out.println(
currentThread().getName()+":"+ a);
try{
sleep(5000);
}catch(InterruptedException e){
throw new RuntimeException(e);
}
}
}
public static void main(String[] args){
Machine machine = new Machine();
Machine machine2 = new Machine();
machine.start();
machine2.start();
machine.run();
}
}
当主线程执行main()方法时,区创建两个Machine()对象,然后启动两个Machine线程,接着主线程开始执行第一个Machine对象的run()方法。在java虚拟机中,有3个线程并发执行Machine对象的run()方法。在3个线程的各自方法栈中都有代表run()方法的栈帧,在这个帧中存放了局部变量a,可见每个线程都拥有自己的局部变量a,他们都分别从0增加到100。
Thread类的currentThread()静态方法返回当前线程的引用,Thread类的getName()实例方法则返回线程的名字。每个线程都有默认的名字,主线程默认的名字是main,用户创建的第一个线程的默认名字为Thread-0,第二个为Thread-1,以此类推,Thread类的setName()可以显示的设置线程的名字。
为了让每个线程都能得到CPU,在run()方法中还调用了sleep()静态方法,该方法让当前线程放弃CPU,并且睡眠若干时间
以上程序可能的一种运行结果如下:
main:0
Thread-0:0
Thread-1:0
main:1
Thread-0:1
Thread-1:1
......
main:49
Thread-0:49
Thread-1:49
2.多个线程共享同一个对象的实例变量
在上面的例子中,变量a是Machine类的实例变量,Machine类的run()方法使用这个实例变量a
把main()修改如下:
public static void main(String[] args){
Machine machine = new Machine();
machine.start();//启动一个Machine线程
machine.run();//主程序执行run()方法
}
运行以上程序,主线程和Machine线程都会执行Machine对象的run()方法。当主线程和Machine线程并发执行Machine对象的run()方法时,都会操作同一个实例变量a,这两个线程轮流地给变量a增加1,以上程序可能的运行结果如下:
main:0
Thread-0:0
main:1
Thread-0:1
......
Thread-0:47
Thread-0:48
main:49
下面对Machine类的main()方法做如下修改,使它创建并启动两个Machine线程:
public static void main(String[] args){
Machine m1 = new Machine();
Machine m2 = new Machine();
m1.start();
m2.start();
}
实例方法和静态方法的字节码都位于方法区,被所有线程共享。由于m1和m2线程分别执行m1和m2对象的run()方法,意味着当m1线程执行run()方法时,会把run()方法中的变量a解析为m1对象的实例变量a,同理m2线程执行run()方法时,会把run()方法中的变量a解析为m2对象的实例变量a。可见,m1线程和m2线程分别操作不同的实例变量a。
以上程序的可能运行结果为:
Thread-0:1
Thread-1:0
......
Thread-0:49
Thread-1:49
3.不要随便覆盖Thread类的start()方法
创建一个线程对象后,线程不会自动启动,必须调用它的start()方法才能启动线程。JDK为Thread类的start()方法提供了默认的实现。对于以下代码:
Machine machine = new Machine();
machine.start();
当用new语句创建Machine对象时,仅仅在堆区内出现一个包含实例变量a的Machine对象,此时Machine线程没有被启动。当主线程执行Machine对象的start()方法时,该方法会启动Machine线程,在java栈区为它创建相应的方法调用栈。
Machine类覆盖了Thread类的start()方法
public class Machine{
private int a=0;
public void start(){
run();
}
public void run(){
for(a=0;a<50;a++){
System.out.println(currentThread().getName()+":"+a);
try{
sleep(1000);
}catch(InterruptedException e){
throw new RuntimeException(e);
}
}
}
public static void main(String[] args){
Machine m = new Machine();
m.start();
}
}
当主线程执行m.start()方法时,start()方法并没有启动一个新的Machine线程,而是去调用Machine对象的run()方法了,这只是普通的方法调用。所有的方法调用都由主线程完成。
从这个例子可以看出,在Thread子类中不应该随意覆盖start()方法,假如一定要覆盖start()方法,那么应该先调用super.start()方法,确保Machine线程会被启动。
4.一个线程只能被启动一次
一个线程只能被启动一次,以下代码试图两次启动machine线程
Machine m = new Machine();
m.start();
m.start();//抛出IllegalThreadStateException异常
13.2.2. 实现Runnable接口
Java不允许一个类继承多个类,因此一旦继承了Thread类,就不能再继承其他的类,为了解决这个问题,java提供了java.lang.Runnable接口,它有一个run()方法,它的定义如下:
public void run();
public class Machine implements Runnable{
public void run(){}
public static void main(String[] args){
Machine m = new Machine();
Thread t1 = new Thread(m);
Thread t2 = new Thread(m);
t1.start();
t2.start();
}
}
主线程创建了t1和t2两个线程对象。当启动t1和t2线程时,都会执行m变量所引用的Machine对象的run()方法。t1和t2共享同一个m对象,因此在执行run()方法时操作同一个实例变量a,以上程序的打印结果为:
Thread-0:0
Thread-1:0
Thread-0:1
Thread-1:1
......
Thread-0:47
Thread-1:48
Thread-0:49
对以上程序做如下修改
public static void main(String[] args){
Machine m1 = new Machine();
Machine m2 = new Machine();
Thread t1 = new Thread(m1);
Thread t2 = new Thread(m2);
t1.start();
t2.start();
}
启动t1,t2线程后,将分别执行m1和m2变量各自引用的Machine对象的run()方法,因此t1,t2线程操纵不同的Machine对象的实例变量a,以上程序可能出现的一种运行结果为:
Thread-0:0
Thread-1:0
......
Thread-0:49
Thread-1:49
13.3. 线程的状态转换
13.3.1. 新建状态
用new语句创建的线程对象处于新建状态(New),此时它和其他Java对象一样,仅仅在堆区中分配了内存。
13.3.2. 就绪状态
当一个线程对象创建后,其他线程调用它的start()方法,该线程就进入就绪状态(Runnable),java虚拟机会为它创建方法调用栈和程序计数器。处于这个状态的线程位于可运行池中,等待CPU的使用权。
13.3.3. 运行状态
处于运行状态(Running)的线程占用CPU,执行程序代码。在并发运行环境中,如果计算机只有一个CPU,那么任何时刻只会有一个线程处于这个状态。如果计算机有多个CPU,那么同意时刻可以让几个线程占用不同的CPU,使他们都处于运行状态。只有处于就绪状态的线程才由机会转到运行状态。
13.3.4. 阻塞状态
阻塞状态(Blocked)是指线程因为某些原因放弃CPU,暂时停止运行。当线程处于阻塞状态时,java虚拟机不会给线程分配CPU,直到线程重新进入就绪状态,它才有机会转到运行状态。
阻塞状态可分为三种:
a.位于对象等待池中的阻塞状态(Blocked in object's wait pool):当线程处于运行状态时,如果执行了某个对象的wait()方法,java虚拟机就会把线程放到这个对象的等待池中
b.位于对象锁池中的阻塞状态(Blocked in object's lock pool):当线程处于运行状态,试图获取某个对象的同步锁时,如果该对象的同步锁已经被其他线程占用,java虚拟机就会把这个线程放到这个对象的锁池中
c.其他阻塞状态(otherwise Blocked):当前线程执行了sleep()方法,或者调用了其他线程的join()方法,或者发出了I/O请求时,就会进入这个状态
当一个线程执行System.out.println()或者System.in.read()方法后,就会发出一个I/O请求,该线程放弃CPU,进入阻塞状态,直到I/O处理完毕,该线程才会恢复运行。例如,以下程序中主线程启动一个Machine线程后,就等待用户的标准输入。主线程处于阻塞状态,Machine线程占用CPU,继续运行。直到用户输入数据,主线程才会恢复运行。
13.3.5. 死亡状态
当线程退出run()方法以后,就进入死亡状态(Dead),该线程结束生命周期。线程有可能是正常执行完run方法而退出的,也有可能是遇到异常而退出的。不管线程是正常退出还是异常结束,都不会对其他线程造成影响。
public class Machine extends Thread{
public void run(){
for(int a=0;a<3;a++){
throw new RuntimeException("Wrong from Machine");
}
}
public static void main(String[] args){
Machine m = new Machine();
m.setName("m1");
m.start();
m.run();
System.out.println("Is machine alive:"+m.isAlive());
System.out.println("main:end");
}
}
Thread类的isAlive()方法用于判断一个线程是否还活着,当线程处于死亡状态或者新建状态时,该方法返回false,在其余状态下,该方法返回true。
13.4. 线程的调度
计算机通常只有一个CPU,在任意时刻只能执行一条及其指令,每个线程只有获得CPU的使用权才能执行指令。所谓多线程的并发运行,实际上是从宏观上看,各个线程轮流得到CPU的使用权,分别执行各自的任务。在可运行池中,会有多个处于就绪状态的线程在等待CPU,java虚拟机的一项任务就是负责线程的调度。线程的调度是指按照特定的机制为多个线程分配CPU的使用权,有两种调度模型:分时调度模型和抢占式调度模型。
分时调度模型是指让所有线程轮流获得CPU的使用权,并且平均分配每个线程占用CPU的时间片。
网友评论