黑马程序员——Java之多线程(上)

来源:互联网 发布:阿里云是paas 编辑:程序博客网 时间:2024/05/21 00:54

------Java培训、Android培训、iOS培训、.Net培训、期待与您交流! -------


内容提要:

        1. 进程(Process)的概念及特点

        2. 线程(Thread)的概念及特点

        3. 多线程的优势

        4. 多线程实现

        5. 线程的生命周期

        6. 多线程的运行性质

        7. 线程的控制执行

        每一个新的知识或领域都有相应的概念,某人曾经说过:概念是最难理解的!在练习实践之前深入理解基本概念,能够起到事半功倍的“疗效”。

        《疯狂XXX》有这样的例子,“单线程程序如同只雇佣一个服务员的餐厅,他必须做完一件事情后才可以做下一件事情;多线程程序则如同雇佣了多个服务员的餐厅,他们可以同时做多件事情。”意在表明:单线程程序只有一个顺序执行流,多线程程序则可以包括多个顺序执行流,多个执行流之间互不干扰。

        “就好像把一件事情分成了多个执行步骤,或者是多个人都做相同的工作,多个人分头做分配给自己的工作(每个人做的工作,与自己被分配的工作相关)。”

进程(Process)的概念及特点

        要理解多线程,就必须理解线程。要理解线程,就必须知道进程。

        从PC机运行及操作系统级来看,每个被CPU执行的程序就是一个进程;

        当一个程序运行时,内部可能包含了多个顺序执行流(任务被分配成多个小部分),每个顺序执行流就是一个线程;

        JVM从底层OS使用的调度机制轮流获得CPU,JVM像一个迷你型OS一样运行,无论底层OS如何,它调用其自己的线程。(需要了解线程在不同JVM环境中该如何运行,不同的JVM能够以完全不同的方式运行线程。一个JVM可能保证所有线程以循环方式依次得到为每个线程平均分配的时间量。但是另一个JVM中,线程可能开始运行,之后独占整个运行时间始终不离开,其他线程只有等待)

        多个进程可以同时开启,CPU在执行它们的时候实际上是在做快速地切换(CPU在某一个时刻只能执行一个进程),在客户端层面来看,好似应用程序在同时执行着;

        进程具有一定独立性,是系统进行资源分配和调度的一个独立单元。拥有自己独立的资源,例如私有的地址空间、内存等。

        在没有经过进程本身允许下,一个用户进程不可直接访问其他进程的地址空间;

        进程具有自己的生命周期和各种不同的状态;

        多个进程可以在单个处理器上并发(此处和“并行”相区别)执行,相互之间不会影响。

线程(Thread)的概念和特点:

        也被称为是轻量级进程,线程是进程的独立执行单元,一个进程可以拥有多个线程,一个线程必须有一个父进程;

        线程在程序中是独立的、并发(轮换执行)的执行流;

        完成一定任务量,与其他线程共享父进程的公共变量和环境,相互之间协同合作完成任务;

        线程具有自己特有的堆栈、程序计数器(PC)和局部变量,但不拥有系统资源。与其他线程共同分享进程所拥有的全部资源;线程的执行是一个单独的进程(一个“轻量级”进程),它有自己的调用栈。一旦创建新线程,就产生一个新的调用栈。

        线程是独立运行的,他不知道进程中是否还有其他线程存在。

        线程的运行是抢占式的(这一点需要底层操作系统的支持,也就是说创建线程并不是JVM执行的,而是底层操作系统创建的),当前执行的线程有可能在任何时候被挂起,以便另一个线程执行。

        一个线程可以创建另一个线程;线程的调度和管理由进程本身负责完成。

        从操作系统级别来看,下属级别是进程;从进程级别来看,下属级别是线程。他们之间的关系用一句话来概括:操作系统可以同时执行多个任务,每个任务就是进程;进程可以同时执行多个子任务,每个子任务就是线程。

        举个例子:迅雷的多端口数据传输,可以看到待下载的资源文件并非排队——从头排到队尾,而是分成几部分进行同时下载和资源填充,这样提高了下载效率。

        再比如:在java.exe进程在执行主线程时,如果程序代码特别多,在堆内存中产生了很多对象,而同时对象调用完后,就成了垃圾。如果垃圾过多就有可能出现堆内存不足的情况,要是只有一个线程工作的话,程序的执行将会很低效。而如果有另一个线程帮助处理的话,如垃圾回收机制线程来帮助回收内存的话,程序的运行将变得更有效。

        多线程在网络中的应用:一个浏览器同时下载多个图片;一个web服务器同时响应多个用户请求等等。

多线程的优势

        同一个进程中的线程有共性:共享一个进程虚拟空间,包括:进程代码段、进程的公有数据等。利用这些共享数据,线程很容易实现相互之间的通信,极大提高了程序的运行效率;

        多线程实现并发比多进程实现并发的性能要高很多,原因是:前者代价小。

多线程的实现

        Java提供了对线程这类事物的描述,就是Thread类。

        创建新执行线程有三种方法,其一是继承Thread 类;

        方法一步骤:

        1. 继承Thread类;

        2. 重写 Thread 类的run 方法(将线程运行的自定义代码存储在run方法中,让线程运行);

        3. 接下来可以分配(创建)并启动该子类的实例;

public class Threadtest extends Thread{ //子类Threadtest 继承了父类Threadprivate int i;//重写run()方法,就是线程执行体public void run(){for (; i < 100; i++){System.out.println(getName() + "+" + i);}}//再次重写run()方法public void run(String str){for(; i < 100; i++){System.out.println(str + getName() + "+" + i);}}public static void main(String[] args) {// TODO Auto-generated method stubfor(int i = 0; i < 100; i++){System.out.println(Thread.currentThread().getName() + "-" + i);if(i == 20){new Threadtest().start(); //调用start()方法,也就启动了线程,实际会调用无参的run()方法new Threadtest().start();}}}}

运行结果:


        需要注意的是:代码是随机、交替执行的,每次运行的结果都不同。

        主线程创建的两个子线程:Thread-0和Thread-1,运行时输出的i值均从0开始;

        start方法有两个作用,启动线程(即增加一个线程,并和主线程争夺CPU运算资源),并执行run方法,并且此时线程就处于可运行状态,是活的;相对来说,instanceThread.run()仅仅是对象调用方法,并没有运行线程。

        run()方法和start()方法对线程的不同影响:

public class ThreadRunDemo extends Thread {private int i;public void run() {for (; i < 100; i++) {System.out.println(Thread.currentThread().getName() + "+" + i);}}public static void main(String[] args) {// TODO Auto-generated method stubfor (int i = 0; i < 100; i++) {System.out.println(Thread.currentThread().getName() + "-" + i);if (i == 20) {new ThreadRunDemo().start();new ThreadRunDemo().run(); //并没有创建一个新的线程}}}}

运行结果:


        多次运行后,发现:在main线程中调用run()方法时,该线程的名字仍然是main,而不是Thread-number,也就是说此时并没有创建一个新的线程,仍然还是main主线程在执行,并且总是在main线程之前执行完毕,这种情况并不具备多线程运行的特征。

        Java程序至少会创建一个主线程,主线程中的执行体是main方法中的程序(JVM能够识别的代码部分),而其他线程执行的是run方法中的代码。

        为什么要覆盖run方法?Thread类用于描述线程,该类就定义了一个功能,用于存储线程要运行的代码,该存储功能就在run方法中,被称为线程执行体。

        创建新执行线程的第二种方法,定义实现 Runnable 接口的类。

        方法二步骤:

        1. 定义实现 Runnable 接口的类,然后实现 run 方法;

        2. 分配该类的实例。在创建线程时,将分配的实例作为参数;

        3. 启动,调用start()方法(此时就可以定义多个线程,并使用同一个Runnable子类对象,或者不同子类对象)。

public class ThreadRunnable implements Runnable{private int i;public void run(){for (; i < 100; i++){System.out.println(Thread.currentThread().getName() + "+" + i);}}public static void main(String[] args) {// TODO Auto-generated method stubfor(int i = 0; i < 100; i++){System.out.println(Thread.currentThread().getName() + "-" + i);if(i == 20){ThreadRunnable tr = new ThreadRunnable();new Thread(tr,"线程0").start();new Thread(tr,"线程1").start();}}}}

运行结果:


        需要注意:在主线程中创建两个线程时,均使用的是同一个ThreadRunnable对象,因此两个线程共享“作业区”变量i。此外注意输出结果:线程0+16;线程1+16。多线程的同步问题也出现了,两个线程在同一时间使用变量i;

        自定义的run方法所属的对象是Runnable接口的子类对象,所以要让线程去指定对象的run方法(相当于是“指定工人要去完成的工作”)。其中提到的run方法就是线程的执行体,创建的Thread线程负责执行该run()方法。

        线程的所有操作都是从run()方法开始的,可以从以下两个类之一中定义自己的“线程作业”,其中run()方法中定义的执行语句就是“线程作业”:扩展java.lang.Thread类(让特定工种的工人干特定的活);实现Runnable接口(Thread是“一般类型的工人”,Runnable是要完成的“作业”)。

        扩展Thread类最简单,但是他通常不是一个良好的OO习惯,因为创建子类应该保留给那些较通用父类的专用版本。实际上,Thread类本身也是实现了Runnable接口,Runnable接口的定义其实就是在确立线程要运行代码所存放的位置——run()方法。

        实际中更喜欢实现Runnable接口,实现Runnable的过程比喻:分配作业,指派人去完成。传递给Thread构造函数的Runnable称为目标或目标Runnable。可以把单个Runnable实例传递给多个Thread对象,这使同一个Runnable成为多个线程的目标,也意味着几个执行线程将允许完成相同的作业。

        对比以上两种方式发现:使用继承Thread类的方法来创建线程类时,多个线程之间无法共享线程类的实例变量;采用Runnable接口的方式创建的多个线程可以共享线程类的实例变量。

        每个执行线程开始都作为Thread类的一个实例,不管是扩展了Thread类,还是实现了Runnable接口。也就是说:无论run()方法在什么地方定义,都需要一个Thread对象来完成该工作。在扩展java.lang.Thread类时,可以任意重写run()方法,但是Thread类期望run()方法没有参数,并且在线程启动后,它将在不同的调用堆栈中执行该方法。但调用其他run(…)方法,只发生在与你发出调用的代码相同的调用堆栈中,就像任何其他普通的方法调用一样。

        此外,Java5还提供了使用Callable和Future创建线程的第三种方法。方式和实现Runnable相类似,只是Callable接口里定义的方法有返回值,可以声明抛出异常。

        继承和实现方式的区别:继承Thread:线程代码存放在Thread子类的run方法中;实现Runnable:线程代码存放在接口子类的run方法中。而Java只支持单继承方式,这种实现方式的好处在于:避免了单继承的局限性。

        当线程完成其run()方法时,该线程不再是一个执行线程,该线程的栈解散,我们认为该线程死去。但仍然是一个Thread对象,只是不是一个执行线程,仍然能够调用该对象上的方法,但不能再次调用start()方法。

        一旦线程启动,他就永远不能再重新启动。只有一个新的线程可以被启动,并且只能一次。一旦一个线程死去,他就不能复生。

线程的生命周期

        一旦new MyThread(),则是新建了一个控制单元,线程处于新建状态。当Thread对象存在,但还没有启动时(还没有调用start()方法),他处于新建状态,但不认为是活的。

        线程运行(即调用了start()方法)的特有特征:开辟了新的调用栈(局部变量在每一个线程的区域中,都有独立的一份);该线程从新建状态转移到可运行状态(就绪状态);当该线程获得机会执行时,其目标run()方法将运行。也就是说,一旦调用了start()方法,就创建了一个独立的线程。        

        当线程被创建后,它既不是一启动就进入执行状态,也不是一直处于执行状态,在他的生命周期中,要经过新建、就绪、运行、阻塞和死亡五个状态。

        一个线程处于阻塞状态仍然被认为是活的。就绪状态、阻塞(扩展为睡眠/阻塞/等待)、运行状态,这三个状态认为是活的线程状态。

        如下图所示:


图2 线程状态转换图

        1.使用new创建线程,处于新建状态,JVM为其配内存,初始化其他成员变量

        2.调用start()方法,线程处于就绪状态,JVM为其创建栈内存和程序计数器

        3. 处于就绪状态的线程获取CPU资源,开始执行run()方法的线程执行体,则该线程就处于运行状态;

        4. 当一个线程放弃所占用的资源时,就进入了阻塞状态。包括:调用sleep()方法;调一个阻塞式IO方法;线程企图获得一个同步监视器,但同步监视器被其他线程持有;(先前调用了同步监视器的wait()方法)等待某个通知notify;调用线程的suspend()方法将线程挂起。

        5. 被阻塞的线程会在合适的时候重新进入就绪状态,重新等待线程调度器调度。包括:调用sleep()方法经过了指定时间;阻塞式IO方法已返回;成功获取同步监视器;收到了其他线程的通知;调用了resume()恢复方法。

        6. 最后就是线程死亡。包括:run()或call()方法执行完成,线程结束;线程抛出一个未捕获的异常或错误;调用stop()方法(容易导致死锁)。

多线程的运行性质

        在大多数JVM中,(底层的操作系统或硬件)调度程序使用基于优先级的抢先调度机制(某种时间分片),但并不是指所有JVM都使用时间分片方式。在这种调度中,每个线程被分配一定量的时间,之后他们被送回可运行状态,为其他线程提供一次运行机会。

        在大多数JVM中,调度程序以这种方式使用线程优先级:当前运行线程通常不会比池中任何线程的优先级低。

        对于所有具有相同优先级的线程来说,调度程序的JVM实际是自由选择它喜欢的线程。可能执行以下操作之一:选择一个线程运行,直到他阻塞或运行完成为止;时间分片,为池内每个线程提供均等的运行机会。        

        多线程的运行结果每次都不同,因为多个线程都在获取CPU的执行权,CPU执行到谁,谁就运行。由此产生了多线程的特性:随机性。谁抢到就执行谁,至于执行多长时间由CPU决定。

        一旦子线程启动后,他就拥有和主线程相同的地位,他不会受到主线程的影响。

        在每个线程中,事情是以可预测的顺序发生的。但不同线程的操作可以以不可预测的方式混合在一起。不能保证一旦某个线程开始执行,他将连续执行到他完成为止。

        举个关于多线程在生活中体现的经典例子:火车站的多个窗口卖票。

public class TrainTicket {public static void main(String[] args) {// TODO Auto-generated method stubTrain t = new Train(); // 指定要完成的工作Thread t1 = new Thread(t); // 指派四个人去完成Thread t2 = new Thread(t);Thread t3 = new Thread(t);Thread t4 = new Thread(t);// 这四个人开始工作t1.start();t2.start();t3.start();t4.start();}}class Train implements Runnable {private int tick = 100;public void run() {while (true) {if (tick > 0) {System.out.println(Thread.currentThread().getName() + "...sale:" + tick--);}}}}

多线程的控制执行

        join线程:让一个线程等待另一个线程完成的方法。

        Thread类的非静态方法join()让一个线程“加入”到了另一个线程的尾部。代码t.join()意味着:“将我(当前线程)加入到t的结束,以便t必须在我(当前线程)可以再次运行之前完成。”该方法也是让当前线程停止,离开运行状态。

        必须是当该线程处于可运行状态,或者执行处于就绪状态。

        含义:线程 t 申请加入到运行中(在某个线程中)来,线程 t 抢夺(运行线程的)CPU执行权。

        当A线程执行到了B线程的.join()方法时,A就会等待。等B线程都执行完,A才会执行。

        join()可以用来临时加入线程执行。

public class ThreadJoin extends Thread{public ThreadJoin(String name){super(name);}public void run(){for (int i = 0; i < 100; i++){System.out.println(getName() + "+" + i);}}public static void main(String[] args) throws InterruptedException {// TODO Auto-generated method stubfor(int i = 0; i < 100; i++){if(i == 20){ThreadJoin tj = new ThreadJoin("被join的线程");tj.start();tj.join();}System.out.println(Thread.currentThread().getName() + "-" + i);}}}

运行结果:

      

        如上图所示的结果,main线程将暂停,直到被join的线程执行完毕,main线程才开始执行。

       必须在线程已经启动后(处于可运行状态时,即调用了start()方法),才能调用join()方法。

        join()方法通常由使用线程的程序调用,将大问题划分为许多小问题,每个小问题分配一个线程,当所有的小问题都得到处理后,再调用主线程来进一步操作。意为:join(加入)的线程,先执行完毕。

        守护线程或精灵线程:setDaemon(),该线程在后台运行,他的任务是为其他的线程提供服务;

        JVM就是典型的后台线程,为前台执行线程清理堆内存的垃圾。

        特征:如果所有的前台线程都死亡,后台线程会自动死亡。

        前台线程创建的子线程默认是前台线程,后台线程创建的子线程默认是后台线程。

       setDaemon()方法,必须在线程调用start()方法之前使用,类比:为工作者事先安排“工位”。

        意为:在“幕后”做服务工作。

        线程睡眠:sleep(),让当前正在执行的线程暂停一段时间,进入阻塞状态。

        当前线程调动sleep()方法进入阻塞状态后,在其睡眠时间段内,该线程不会获得执行的机会,即使系统中没有其他可执行的线程,处于sleep中的线程也不会执行。常用来暂停程序的执行,一旦sleep的时间到了,线程将转为就绪状态,而不是运行状态。

import java.util.Date;public class SleepTest {public static void main(String[] args) throws InterruptedException {// TODO Auto-generated method stubfor(int i = 1; i <= 100; i++){System.out.println(new Date());Thread.sleep(1000);}}}

运行结果:


       每1s钟执行一次输出语句。

        sleep()方法是Thread类的一个静态方法,在代码中使用它强制线程再回到就绪状态(在这里他仍然必须请求成为当前运行线程)之前进入睡眠模式。当线程睡眠时,他入睡在某个地方,在苏醒之前他不会返回到就绪状态。当线程醒来时,他只是返回到就绪状态。sleep()方法是Thread类的静态方法,可以放在任何地方,因为所有代码正被某个线程执行。当执行代码遇到sleep()调用时,它让当前正在执行的线程睡眠。

        静态方法sleep(),让当前线程睡眠。

        必须注意的是:sleep()方法是静态方法。

        线程让步:yield()可以让当前正在执行的线程暂停,但不会阻塞该线程,只是让该线程转入就绪状态,让系统的线程调度器重新调度一次

        静态Thread.yield()方法是让当前运行线程回到可运行状态,以允许具有相同优先级的其他线程获得运行机会,单词yield具有让步的含义。使用yield()方法的目的是让相同优先级线程之间能适当地轮转。但是,在实际中,并不能保证yield()达到让步的目的,即使yield()方法确实导致一个线程退出运行状态,并回到可运行状态,也无法保证让步线程不会从所有其他线程中被再次选中。

        在大多数情况下,yield()方法将导致线程从运行状态转到可运行状态。

        线程睡眠和线程让步区别:sleep()方法会给其他线程执行机会,不会理会其他线程的优先级,yield()方法只会给优先级相同,或优先级更高的线程执行机会。sleep()方法会让线程进入阻塞状态,经过阻塞时间后会转入就绪状态,而yield()方法只是强制让当前线程进入就绪状态。

        必须注意的是:yield()是Thread类的静态方法

        线程的优先级:每个线程执行时都有一定的优先级,优先级高的线程获得较多的执行机会,而优先级低的线程则获得较少的执行机会。每个线程默认的优先级都与创建他的父线程的优先级相同,在默认情况下,main线程具有普通优先级。线程优先级范围:1~10(最高),三个静态常量表示:MAX_PRIORITY:10;MIN_PRIORITY:1;NORM_PRIORITY:5。

       设置优先级必须是在调用start()方法之前。

0 0