美文网首页
13章.多线程

13章.多线程

作者: 99度蓝99 | 来源:发表于2018-01-31 10:29 被阅读0次

           进程是指运行中的应用程序,每一个进程都有自己独立的内存空间,对一个应用程序可以同事启动多个进程。例如,对于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的时间片。

    相关文章

      网友评论

          本文标题:13章.多线程

          本文链接:https://www.haomeiwen.com/subject/mbcczxtx.html