Java基础之(三十七)Java多线程编程<一>

来源:互联网 发布:淘宝专业版一钻以上 编辑:程序博客网 时间:2024/05/21 06:00

说明
几乎所有的操作系统都支持同时运行多个任务,一个任务就是一个程序,每个运行中的程序就是一个进程。当一个程序运行时,内部可能包含了多个顺序执行流,每个顺序执行流就是一个线程。

线程和进程

一个操作系统里可以有多个进程,而一个进程里可以有多个线程。

进程

几乎所有的操作系统都支持进程的概念,所有运行中的任务通常对应一个进程(Process)。当一个程序进入内存运行后,即变成一个进程。进程是出于运行过程中的程序,并且具有一定的独立功能,进程是系统进行资源分配和调度的一个独立单位。

一般而言,进程包含如下3个特征:

  • 独立性:进程是系统中独立存在的实体,他可以拥有自己独立的资源,每一个进程都拥有自己私有的地址空间。在没有经过进程本身允许的情况下,一个用户进程不可以直接访问其他进程的地址空间;

  • 动态性:进程和程序的区别在于,程序只是一个静态的指令集合,而进程是一个正在系统中活动的指令集合。在进程中加入了时间的概念。进程具有自己的生命周期和各种不同的状态,这些概念在程序中都是不具备的。

  • 并发性:多个进程可以在单个处理器上并发执行,多个进程之间不会相互影响;

注意:并发性(concurrency)和并行性(parallel)是两个概念,并行指在同一时刻,有多条指令在多个处理器上同时执行;并发指在同一时刻只能有一条指令执行,但多个进程指令被快速轮换执行,使得在宏观上具有多个进程同时执行的效果。

现代的操作系统几乎都支持同时运行多个任务:一边开着ide写代码,一边开着网页在查API,同时还在听音乐,用markdown作笔记…这些进程开上去像是在同时工作。

但是真相是:对于一个CPU而言,它在某一个时间点上只能执行一个程序,也就是说只能运行一个进程。CPU不断的在这些进程之间快速轮换执行,那么为什么感觉不到任何中断现象呢?这是因为CPU的执行速度相对我们的感觉来说实在是太快了。*如果启动的程序(进程)足够多,我们依然可以感觉程序的运行速度下降(电脑变卡)。

线程

多线程扩展了多进程的概念,使得同一个进程(注意这里是限于一个进程里!)可以同时并发处理多个任务。

线程(Thread)也被称作轻量级进程(Lightweight Process)==。线程(Thread)是进程(Process)的执行单元。就像进程在操作系统中的地位一样,线程在程序中是独立的、并发的执行流。当进程被初始化之后,主线程就被创建了。对于大多数的应用程序来说,通常仅要求有一个主线程,但我们也可以在该进程内创建多条顺序执行流,这些顺序执行流就是Thread,每条Thread也是互相独立的。

  • 线程是进程的组成部分,一个进程可以有多个线程,一个线程必须有一个父进程。

  • 一个线程可以拥有自己的堆、栈、自己的程序计数器(PC)和自己的局部变量,但不再拥有系统资源,它与父进程的其他线程共享该进程(Process)所拥有的全部资源。

  • 因为多个线程共享父进程的全部资源,因此编程更加方便;但必须注意的是:必须确保一个线程不会妨碍同一进程里的其他线程!

  • 线程可以完成一定的任务,可与其他线程共享父进程中的共享变量及部分环境,相互之间协同来完成进程所要完成的任务。

  • 线程是独立运行的,它并不知道进程中是否还有其他线程的存在。线程的运行是抢占式的 ———> 当前运行的线程在任何时候都可能被挂起,以便另一个线程可以运行。

  • 一个线程可以创建和撤销另一个线程(例如在main方法这个主线程里创建另一个线程),同一个进程(Process)的多个线程(Thread)之间可以并发执行(concurrency,多个线程之间快速切换,快到让人感觉是在同时执行)

从逻辑角度来看:多线程存在于一个应用程序中,让一个应用程序中可以有多个执行部分同时执行;但操作系统无需将多个线程看做多个独立的应用,对多线程实现调度和管理、资源分配由进程本身负责完成

总结:一个程序运行后至少有一个进程,一个进程里可以包含多个线程,但至少要有一个线程(主线程)。

多线程的优势

线程在程序中是独立的、并发的执行流,与分隔的进程相比,进程中的线程之间的隔离程度要小。他们共享内存、文件句柄和其他每个进程应用的状态。

总结起来,使用多线程编程有如下几个优势:

  • 进程间不能共享内存,但线程之间共享内存非常容易;

  • 系统创建进程需要为该进程重新分配系统资源,但创建线程则代价小得多,因此使用多线程来实现多任务并发比多进程的效率高;

  • Java给多线程编程提供了内置的支持,从而简化了多线程编程。

线程的生命周期

当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。

在线程的生命周期中,它要经过

  1. 新建(new)

  2. 就绪(runnable)

  3. 运行(running)

  4. 阻塞(blocked)

  5. 死亡(dead)

五种状态。尤其是线程启动以后,它不能一直”霸占“着CPU独自运行,CPU需要在多条线程之间切换,所以线程状态也会多次在运行、阻塞之间切换。

下图显示了一个线程完整的生命周期。

这里写图片描述

线程存在于好几种状态。线程可以正在运行(running)。只要获得CPU时间它就可以运行。运行的线程可以被挂起(suspend),并临时中断它的执行。一个挂起的线程可以被恢复(resume,允许它从停止的地方继续运行。一个线程可以在等待资源时被阻塞(block)。

  • 新建状态:
    使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序 start() 这个线程。

  • 就绪状态:
    当线程对象调用了start()方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。

注意:
启动线程使用start()方法,而不是run()方法!

1) 调用start方法来启动线程,系统会把该run方法当成线程执行体来处理。多个线程之间可以并发执行

2)
但是如果直接调用线程对象的run方法,run方法会被立刻执行,而且在run方法返回之前其他线程无法并发执行。(变成了普通的方法调用!!!)

不要对已经启动的线程再次调用start方法,否则会引发IllegalThreadStateException

线程调度器切换线程由底层平台控制,具有一定随机性

如果希望调用子线程的strat方法立刻执行子线程,可以使用Thread.sleep(1)来让当前运行的线程(主线程)睡眠一个毫秒,这一毫秒内CPU不会空闲,它会立刻去执行一条就绪的线程。

  • 运行状态:
    如果就绪状态的线程获取 CPU 资源,就可以执行 run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。

注意:

当一条线程开始运行后,它不可能一直处于运行状态(除非线程执行体足够短,瞬间就执行完了),线程在运行过程中需要被中断,目的是使其他线程获得执行的机会,线程调度的细节取决于底层平台所采用的策略。

系统会给每个可执行的线程一小段的时间来处理任务;当该时间段使用完,系统就会剥夺该线程所占据的资源,让其他线程获得执行的机会。在选择下一个线程时,系统会考虑线程的优先级。

就绪和运行状态之间的转换通常不受程序控制,而是由系统线程调度所导致。

  • 阻塞状态:
    如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。

  • 死亡状态:
    一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态。

线程会以以下三种方式之一结束,结束后处于死亡状态:

run方法执行完成,线程正常结束

线程抛出一个未捕获的Exception或Error

直接调用该线程的stop方法来结束该线程(容易导致死锁,不推荐使用!)

注意:

当主线程结束的时候,其他线程不受任何影响,并不会随之结束。一旦子线程启动起来后,它就拥有和主线程相同的地位,不会受主线程的影响(如前面所说,线程之间是相互独立的)。

为了测试某条线程是否已经死亡,可以调用线程对象的isAlive方法。

当线程处于就绪、运行、阻塞三种状态时,该方法返回true

处于新建、死亡两种状态时,返回false。

不可以对一个已经死亡的线程调用start方法,否则会引发IllegalThreadStateException(不能对已经死亡或者已经启动的线程调用start方法,只能对新建状态的线程调用)

创建一个线程

java使用Thread类代表线程,所有的线程都必须是Thread类或其子类。
每条线程的作用是:完成一定的任务,实际上就是执行一段程序流。java使用run方法来封装这样一段程序流,run方法也被成为线程执行体

大多数情况,通过实例化一个Thread对象来创建一个线程。Java定义了两种方式:

  • 可以继承Thread类。

  • 实现Runnable 接口;

通过继承Thread来创建线程

  1. 定义Thread类的子类,重写该类run方法。run方法的方法体代表了该线程需要完成的任务(想要它做什么,就往run方法里面写什么)。

  2. 创建线程对象

  3. 用线程对象的start方法来启动线程。

public class FirstThread extends Thread {    //重写run方法    public void run() {        for (int i = 0; i < 100; i++) {            // 使用getName()方法来返回当前线程的名字            System.out.println(this.getName() + " " + i);        }    }    public static void main(String[] args) {       //创建线程对象       FirstThread ft = new FirstThread();       //用线程对象的start方法来启动线程。       ft.start();         for (int i = 0; i < 100; i++) {                // 使用Thread类的静态方法 currentThread() 来获取当前线程                System.out.println(Thread.currentThread().getName()                     + " " + i);            }    }}
输出结果:main 0Thread-0 0main 1Thread-0 1main 2Thread-0 2main 3Thread-0 3main 4...main 38Thread-0 40Thread-0 41Thread-0 42main 39...

上面的程序只显式的启动了一条线程,但实际上有2条线程,因为还有包含main方法的主线程。主线程的线程体不是有run方法确定的,而是由main方法的方法体来确定。

上面还用到了线程类的两个方法:

  • Thread.currentThread():返回当前正在执行的线程对象

  • getName():返回调用该方法的线程的名字。

程序可以通过setName(String name)方法来为线程设置名字。默认情况下下主线程的名字为main,用户启动的多条线程名字依次为Thread-0, Thread-1…

从结果可以看出,主线程和子线程交替运行。它的运行结果是这样的:
执行了ft.start()时,线程进入就绪状态,run方法不会立即得到执行;但是程序继续向下运行,下一行代码的for循环得到执行,因此主线程抢到CPU,输出主线程的名字main 0,但这个时候处于绪状态的子线程获取 CPU 资源,就可以执行 run(),此时线程便处于运行状态。因此输出Thread-0 0。如输出结果所示,交替运行并不代表总是挨个挨个的运行,它是没有规律的。

我们再稍微改进一下上述代码,为它再增加一条线程:

 public static void main(String[] args) {        for (int i = 0; i < 100; i++) {            // 使用Thread类的静态方法 currentThread() 来获取当前线程            System.out.println(Thread.currentThread().getName()                 + " " + i);            if (i == 20) {                new FirstThread().start();            new FirstThread().start();            }        }    }
输出结果:main 0...main 21...Thread-1 17Thread-0 30main 40...

上面程序Thread-0和Thread-1输出的i并不连续,这是因为i是实例属性,程序每次创建线程对象都需要创建一个FirstThread对象,Thread-0和Thread-1不能共享i。(但如果把i设成static就可以)

使用继承Thread类的方法来创建线程,多条线程之间无法共享实例变量

通过实现Runnable接口来创建线程

  1. 定义实现Runnable接口的类,重写run方法
public class SecondThread implements Runnable
public void run()
  1. 创建Runnable实现类的对象,并以此作为Thread的target来创建Thread对象,这个Thread对象才是真正的线程对象。代码如下所示:
//创建Runnable实现类的对象SecondThread st = new SecondThread();//以Runnable实现类的对象作为Thread的target来创建Thread对象new Thread(st, "NewThread");

Runnable对象仅仅作为Thread对象的Target(在创建Thread对象时作为参数传进构造方法,Runnale实现类里包含的run方法仅仅作为线程执行体。==而实际的线程对象依然是Thread类的实例,只是该Thread线程负责执行其Target的run方法而已==)

  1. 调用线程对象的start方法来启动该线程

下面是一个创建线程并开始让它执行的实例:

public class SecondThread implements Runnable {    //run()方法同样是线程执行体    public void run() {        for (int i = 0; i < 20; i++) {            //当线程类实现Runnable接口时,            //如果想获取当前进程,只能用Thread.currentThread()方法            System.out.println(Thread.currentThread().getName()                 + " " + i);        }    }    public static void main(String[] args) {        for (int i = 0; i < 20; i++) {            System.out.println(Thread.currentThread().getName()                 + " " + i);            if (i == 10) {                SecondThread st = new SecondThread();                //通过new Thread(target,name)方法创建新线程                new Thread(st, "newThread1").start();                new Thread(st, "newThread2").start();            }        }    }}
输出结果:main 0...main 10main 11main 12main 13newThread1 0main 14newThread1 1main 15...

这时两个线程的i变量是连续的,因为程序所创建的Runnable对象只是线程的target,而多条线程可以共享同一个target,也就是说可以共享同一个target的所有实例变量。

两种方式对比

这里写图片描述

几乎所有的多线程应用都采用实现Runnable接口的方式。

0 0