美文网首页
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的时间片。

相关文章

  • iOS多线程 NSOperation

    系列文章: 多线程 多线程 pthread、NSThread 多线程 GCD 多线程 NSOperation 多线...

  • iOS多线程 pthread、NSThread

    系列文章: 多线程 多线程 pthread、NSThread 多线程 GCD 多线程 NSOperation 多线...

  • iOS多线程: GCD

    系列文章: 多线程 多线程 pthread、NSThread 多线程 GCD 多线程 NSOperation 多线...

  • iOS多线程运用

    系列文章: 多线程 多线程 pthread、NSThread 多线程 GCD 多线程 NSOperation 多线...

  • iOS多线程基础

    系列文章: 多线程 多线程 pthread、NSThread 多线程 GCD 多线程 NSOperation 多线...

  • 多线程介绍

    一、进程与线程 进程介绍 线程介绍 线程的串行 二、多线程 多线程介绍 多线程原理 多线程的优缺点 多线程优点: ...

  • iOS进阶之多线程管理(GCD、RunLoop、pthread、

    深入理解RunLoopiOS多线程--彻底学会多线程之『GCD』iOS多线程--彻底学会多线程之『pthread、...

  • iOS多线程相关面试题

    iOS多线程demo iOS多线程之--NSThread iOS多线程之--GCD详解 iOS多线程之--NSOp...

  • 多线程之--NSOperation

    iOS多线程demo iOS多线程之--NSThread iOS多线程之--GCD详解 iOS多线程之--NSOp...

  • iOS多线程之--NSThread

    iOS多线程demo iOS多线程之--NSThread iOS多线程之--GCD详解 iOS多线程之--NSOp...

网友评论

      本文标题:13章.多线程

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