你的位置:首页 > Java教程

[Java教程]深入浅出Java多线程


Java给多线程编程提供了内置的支持。一个多线程程序包含两个或多个能并发运行的部分。程序的每一部分都称作一个线程,并且每个线程定义了一个独立的执行路径。

多线程是多任务的一种特别的形式,但多线程使用了更小的资源开销。

这里定义和线程相关的另一个术语 - 进程:一个进程包括由操作系统分配的内存空间,包含一个或多个线程。一个线程不能独立的存在,它必须是进程的一部分。一个进程一直运行,直到所有的非守候线程都结束运行后才能结束。

多线程能满足程序员编写高效率的程序来达到充分利用CPU的目的。

1. 多线程基础概念介绍

进程是程序(任务)的执行过程,它持有资源(共享内存,共享文件)和线程。

分析:

①     执行过程 动态性的,你放在电脑磁盘上的某个eclipse或者QQ文件并不是我们的进程,只有当你双击运行可执行文件,使eclipse或者QQ运行之后,这才称为进程。它是一个执行过程,是一个动态的概念。

②     它持有资源(共享内存,共享文件)和线程:我们说进程是资源的载体,也是线程的载体。这里的资源可以理解为内存。我们知道程序是要从内存中读取数据进行运行的,所以每个进程获得执行的时候会被分配一个内存。

③     线程是什么?

 

如果我们把进程比作一个班级,那么班级中的每个学生可以将它视作一个线程。学生是班级中的最小单元,构成了班级中的最小单位。一个班级有可以多个学生,这些学生都使用共同的桌椅、书籍以及黑板等等进行学习和生活。

在这个意义上我们说:

线程是系统中最小的执行单元;同一进程中可以有多个线程;线程共享进程的资源。

④     线程是如何交互?

就如同一个班级中的多个学生一样,我们说多个线程需要通信才能正确的工作,这种通信,我们称作线程的交互

⑤     交互的方式:互斥、同步

类比班级,就是在同一班级之内,同学之间通过相互的协作才能完成某些任务,有时这种协作是需要竞争的,比如学习,班级之内公共的学习资料是有限的,爱学习的同学需要抢占它,需要竞争,当一个同学使用完了之后另一个同学才可以使用;如果一个同学正在使用,那么其他新来的同学只能等待;另一方面需要同步协作,就好比班级六一需要排演节目,同学需要齐心协力相互配合才能将节目演好,这就是进程交互。

##一个线程的生命周期

线程经过其生命周期的各个阶段。下图显示了一个线程完整的生命周期。

  • 新建状态:

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

  • 就绪状态:

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

  • 运行状态:

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

  • 阻塞状态:

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

  • 死亡状态:

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

## 线程的优先级

每一个Java线程都有一个优先级,这样有助于操作系统确定线程的调度顺序。

Java线程的优先级是一个整数,其取值范围是1 (Thread.MIN_PRIORITY ) - 10 (Thread.MAX_PRIORITY )。

默认情况下,每一个线程都会分配一个优先级NORM_PRIORITY(5)。

具有较高优先级的线程对程序更重要,并且应该在低优先级的线程之前分配处理器资源。但是,线程优先级不能保证线程执行的顺序,而且非常依赖于平台。

2. Java 中线程的常用方法介绍

# Java语言对线程的支持

      主要体现在Thread类Runnable接口上,都继承于java.lang包。它们都有个共同的方法:public void run()。

  run方法为我们提供了线程实际工作执行的代码。

 

下表列出了Thread类的一些重要方法:

序号方法描述
1public void start()
使该线程开始执行;Java 虚拟机调用该线程的 run 方法。
2public void run()
如果该线程是使用独立的 Runnable 运行对象构造的,则调用该 Runnable 对象的 run 方法;否则,该方法不执行任何操作并返回。
3public final void setName(String name)
改变线程名称,使之与参数 name 相同。
4public final void setPriority(int priority)
 更改线程的优先级。
5public final void setDaemon(boolean on)
将该线程标记为守护线程或用户线程。
6public final void join(long millisec)
等待该线程终止的时间最长为 millis 毫秒。
7public void interrupt()
中断线程。
8public final boolean isAlive()
测试线程是否处于活动状态。

测试线程是否处于活动状态。 上述方法是被Thread对象调用的。下面的方法是Thread类的静态方法。

 

序号方法描述
1public static void yield()
暂停当前正在执行的线程对象,并执行其他线程。
2public static void sleep(long millisec)
在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。
3public static boolean holdsLock(Object x)
当且仅当当前线程在指定的对象上保持监视器锁时,才返回 true。
4public static Thread currentThread()
返回对当前正在执行的线程对象的引用。
5public static void dumpStack()
将当前线程的堆栈跟踪打印至标准错误流。

# Thread常用的方法 

3. 线程初体验(编码示例)

创建线程的方法有两种:

  1.继承Thread类本身

  2.实现Runnable接口

线程中的方法比较有特点,比如:启动(start),休眠(sleep),停止等,多个线程是交互执行的(cpu在某个时刻。只能执行一个线程,当一个线程休眠了或者执行完毕了,另一个线程才能占用cpu来执行)因为这是cpu的结构来决定的,在某个时刻cpu只能执行一个线程,不过速度相当快,对于人来将可以认为是并行执行的。

 

在一个java文件中,可以有多个类(此处说的是外部类),但只能有一个public类。

这两种创建线程的方法本质没有任何的不同,一个是实现Runnable接口,一个是继承Thread类。

使用实现Runnable接口这种方法:

  1.可以避免java的单继承的特性带来的局限性;

  2.适合多个相同程序的代码去处理同一个资源情况,把线程同程序的代码及数据有效的分离,较好的体现了面向对象的设计思想。开发中大多数情况下都使用实现Runnable接口这种方法创建线程。

实现Runnable接口创建的线程最终还是要通过将自身实例作为参数传递给Thread然后执行

语法:    Thread actress=new Thread(Runnable target ,String name);

例如:    Thread actressThread=new Thread(new Actress(),"Ms.runnable");

    actressThread.start();

代码示例:

 1 package com.study.thread; 2  3 public class Actor extends Thread{ 4   public void run() { 5     System.out.println(getName() + "是一个演员!"); 6     int count = 0; 7     boolean keepRunning = true; 8  9     while(keepRunning){10       System.out.println(getName()+"登台演出:"+ (++count));11       if(count == 100){12         keepRunning = false;13       }14       if(count%10== 0){15         try {16           Thread.sleep(1000);17         } catch (InterruptedException e) {18           e.printStackTrace();19         }20       }21     }22     System.out.println(getName() + "的演出结束了!");23   }24 25   public static void main(String[] args) {26    Thread actor = new Actor();//向上转型:子类转型为父类,子类对象就会遗失和父类不同的方法。向上转型符合Java提倡的面向抽象编程思想,还可以减轻编程工作量27    actor.setName("Mr. Thread");28     actor.start();29    30    //调用Thread的构造函数Thread(Runnable target, String name)31    Thread actressThread = new Thread(new Actress(), "Ms. Runnable");32     actressThread.start();33   }34 35 }36 //注意:在“xx.java”文件中可以有多个类,但是只能有一个Public类。这里所说的不是内部类,都是一个个独立的外部类37 class Actress implements Runnable{38 39   @Override40   public void run() {41     System.out.println(Thread.currentThread().getName() + "是一个演员!");//Runnable没有getName()方法,需要通过线程的currentThread()方法获得线程名称42     int count = 0;43     boolean keepRunning = true;44 45     while(keepRunning){46       System.out.println(Thread.currentThread().getName()+"登台演出:"+ (++count));47       if(count == 100){48         keepRunning = false;49       }50       if(count%10== 0){51         try {52           Thread.sleep(1000);53         } catch (InterruptedException e) {54           e.printStackTrace();55         }56       }57     }58     System.out.println(Thread.currentThread().getName() + "的演出结束了!");59   }60   61 }62 63 /**64  *运行结果Mr. Thread线程和Ms. Runnable线程是交替执行的情况65  *分析:计算机CPU处理器在同一时间同一个处理器同一个核只能运行一条线程,66  *当一条线程休眠之后,另外一个线程才获得处理器时间67 */

运行结果:

 示例2:

ArmyRunnable 类:
 1 package com.study.threadTest1; 2  3 /** 4  * 军队线程 5  * 模拟作战双方的行为 6 */ 7 public class ArmyRunnable implements Runnable { 8  9   /* volatile关键字10    * volatile保证了线程可以正确的读取其他线程写入的值11    * 如果不写成volatile,由于可见性的问题,当前线程有可能不能读到这个值12    * 关于可见性的问题可以参考JMM(Java内存模型),里面讲述了:happens-before原则、可见性13    * 用volatile修饰的变量,线程在每次使用变量的时候,都会读取变量修改后的值14   */15   volatile boolean keepRunning = true;16 17   @Override18   public void run() {19     while (keepRunning) {20       //发动5连击21       for(int i=0;i<5;i++){22         System.out.println(Thread.currentThread().getName()+"进攻对方["+i+"]");23         //让出了处理器时间,下次该谁进攻还不一定呢!24         Thread.yield();//yield()当前运行线程释放处理器资源25       } 26     }27     System.out.println(Thread.currentThread().getName()+"结束了战斗!");28   }29 30 }

KeyPersonThread 类:
 1 package com.study.threadTest1; 2  3  4 public class KeyPersonThread extends Thread { 5   public void run(){ 6     System.out.println(Thread.currentThread().getName()+"开始了战斗!"); 7     for(int i=0;i<10;i++){ 8       System.out.println(Thread.currentThread().getName()+"左突右杀,攻击隋军..."); 9     }10     System.out.println(Thread.currentThread().getName()+"结束了战斗!");11   }12 13 }

Stage 类:
 1 package com.study.threadTest1; 2  3 /** 4  * 隋唐演义大戏舞台 6 */ 7 public class Stage extends Thread { 8   public void run(){ 9     System.out.println("欢迎观看隋唐演义");10     //让观众们安静片刻,等待大戏上演11     try {12       Thread.sleep(5000);13     } catch (InterruptedException e1) {14       e1.printStackTrace();15     }16     System.out.println("大幕徐徐拉开");17 18     try {19       Thread.sleep(5000);20     } catch (InterruptedException e1) {21       e1.printStackTrace();22     }23 24     System.out.println("话说隋朝末年,隋军与农民起义军杀得昏天黑地...");25     ArmyRunnable armyTaskOfSuiDynasty = new ArmyRunnable();26     ArmyRunnable armyTaskOfRevolt = new ArmyRunnable();27 28     //使用Runnable接口创建线程29     Thread armyOfSuiDynasty = new Thread(armyTaskOfSuiDynasty,"隋军");30     Thread armyOfRevolt = new Thread(armyTaskOfRevolt,"农民起义军");31 32     //启动线程,让军队开始作战33     armyOfSuiDynasty.start();34     armyOfRevolt.start();35 36     //舞台线程休眠,大家专心观看军队厮杀37     try {38       Thread.sleep(50);39     } catch (InterruptedException e) {40       e.printStackTrace();41     }42 43     System.out.println("正当双方激战正酣,半路杀出了个程咬金");44 45     Thread mrCheng = new KeyPersonThread();46     mrCheng.setName("程咬金");47     System.out.println("程咬金的理想就是结束战争,使百姓安居乐业!");48 49     //停止军队作战50     //停止线程的方法51     armyTaskOfSuiDynasty.keepRunning = false;52     armyTaskOfRevolt.keepRunning = false;53 54     try {55       Thread.sleep(2000);56     } catch (InterruptedException e) {57       e.printStackTrace();58     }59     /*60      * 历史大戏留给关键人物61     */62     mrCheng.start();63 64     //万众瞩目,所有线程等待程先生完成历史使命65     try {66       mrCheng.join();//join()使其他线程等待当前线程终止67     } catch (InterruptedException e) {68       e.printStackTrace();69     }70     System.out.println("战争结束,人民安居乐业,程先生实现了积极的人生梦想,为人民作出了贡献!");71     System.out.println("谢谢观看隋唐演义,再见!");72   }73 74   public static void main(String[] args) {75     new Stage().start();76   }77 78 }

运行结果:

4. Java 线程的正确停止

如何正确的停止Java中的线程?

  stop方法:该方法使线程戛然而止(突然停止),完成了哪些工作,哪些工作还没有做都不清楚,且清理工作也没有做。

       stop方法不是正确的停止线程方法。线程停止不推荐使用stop方法。

## 正确的方法---设置退出标志 

使用volatile 定义boolean running=true,通过设置标志变量running,来结束线程。

如本文:volatile boolean keepRunning=true;

这样做的好处是:使得线程有机会使得一个完整的业务步骤被完整地执行,在执行完业务步骤后有充分的时间去做代码的清理工作,使得线程代码在实际中更安全。

## 广为流传的错误方法---interrupt方法 

当一个线程运行时,另一个线程可以调用对应的 Thread 对象的 interrupt()方法来中断它,该方法只是在目标线程中设置一个标志,表示它已经被中断,并立即返回。这里需要注意的是,如果只是单纯的调用 interrupt()方法,线程并没有实际被中断,会继续往下执行。

代码示例:

 1 package com.study.threadStop; 2  3 /** 4  * 错误终止进程的方式——interrupt 5 */ 6 public class WrongWayStopThread extends Thread { 7  8   public static void main(String[] args) { 9     WrongWayStopThread thread = new WrongWayStopThread();10     System.out.println("Start Thread...");11     thread.start();12     13     try {14       Thread.sleep(3000);15     } catch (InterruptedException e) {16       e.printStackTrace();17     }18     19     System.out.println("Interrupting thread...");20     thread.interrupt();21     22     try {23       Thread.sleep(3000);24     } catch (InterruptedException e) {25       e.printStackTrace();26     }27     System.out.println("Stopping application...");28   }29   30   public void run() {31     while(true){32       System.out.println("Thread is running...");33       long time = System.currentTimeMillis();34       while ((System.currentTimeMillis()-time) <1000) {//这部分的作用大致相当于Thread.sleep(1000),注意此处为什么没有使用休眠的方法35         //减少屏幕输出的空循环(使得每秒钟只输出一行信息)36       }37     }38   }39 }

运行结果:

由结果看到interrupt()方法并没有使线程中断,线程还是会继续往下执行。 

 Java API中介绍:

但是interrupt()方法可以使我们的中断状态发生改变,可以调用isInterrupted 方法

将上处run方法代码改为下面一样,程序就可以正常结束了。

1  public void run() {2     while(!this.isInterrupted()){//interrupt()可以使中断状态放生改变,调用isInterrupted()3       System.out.println("Thread is running...");4       long time = System.currentTimeMillis();5       while ((System.currentTimeMillis()-time) <1000) {//这部分的作用大致相当于Thread.sleep(1000),注意此处为什么没有使用休眠的方法6         //减少屏幕输出的空循环(使得每秒钟只输出一行信息)7       }8     }9   }

但是这种所使用的退出方法实质上还是前面说的使用退出旗标的方法,不过这里所使用的退出旗标是一个特殊的标志“线程是否被中断的状态”。

这部分代码相当于线程休眠1秒钟的代码。但是为什么没有使用Thread.sleep(1000)。如果采用这种方法就会出现

线程没有正常结束,而且还抛出了一个异常,异常抛出位置在调用interrupt方法之后。为什么会有这种结果?

在API文档中说过:如果线程由于调用的某些方法(比如sleep,join。。。)而进入一种阻塞状态时,此时如果这个线程再被调用interrupt方法,它会产生两个结果:第一,它的中断状态被清除clear,而不是被设置set。那isInterrupted 就不能返回是否被中断的正确状态,那while函数就不能正确的退出。第二,sleep方法会收到InterruptedException被中断。

interrupt()方法只能设置interrupt标志位(且在线程阻塞情况下,标志位会被清除,更无法设置中断标志位),无法停止线程。 

 

5. 线程交互

争用条件:

1、当多个线程同时共享访问同一数据(内存区域)时,每个线程都尝试操作该数据,从而导致数据被破坏(corrupted),这种现象称为争用条件

2、原因是,每个线程在操作数据时,会先将数据初值读【取到自己获得的内存中】,然后在内存中进行运算后,重新赋值到数据。

3、争用条件:线程1在还【未重新将值赋回去时】,线程1阻塞,线程2开始访问该数据,然后进行了修改,之后被阻塞的线程1再获得资源,而将之前计算的值覆盖掉线程2所修改的值,就出现了数据丢失情况。

## 互斥与同步:守恒的能量

1、线程的特点,共享同一进程的资源,同一时刻只能有一个线程占用CPU

2、由于线程有如上的特点,所以就会存在多个线程争抢资源的现象,就会存在争用条件这种现象

3、为了让线程能够正确的运行,不破坏共享的数据,所以,就产生了同步和互斥的两种线程运行的机制

4、线程的互斥(加锁实现):线程的运行隔离开来,互不影响,使用synchronized关键字实现互斥行为,此关键字即可以出现在方法体之上也可以出现在方法体内,以一种块的形式出现,在此代码块中有线程的等待和唤醒动作,用于支持线程的同步控制

5、线程的同步(线程的等待和唤醒:wait()+notifyAll()):线程的运行有相互的通信控制,运行完一个再正确的运行另一个

6、锁的概念:比如private final Object lockObj=new Object();

7、互斥实现方式:synchronized关键字

synchronized(lockObj){---执行代码----}加锁操作

lockObj.wait();线程进入等待状态,以避免线程持续申请锁,而不去竞争cpu资源

lockObj.notifyAll();唤醒所有lockObj对象上等待的线程

8、加锁操作会开销系统资源,降低效率

## 深入剖析互斥与同步

互斥的实现(加锁):synchronized(lockObj); 保证的同一时间,只有一个线程获得lockObj.

同步的实现:wait()/notify()/notifyAll()

注意:wait()、notify()、notifyAll()方法均属于Object对象,而不是Thread对象。

notify()唤醒wait set中的一条线程,而notifyall()唤醒所有线程。

同步是两个线程之间的一种交互的操作(一个线程发出消息另外一个线程响应)

 如何理解同步:Wait Set

Critical Section(临界资源)Wait Set(等待区域)

wait set 类似于线程的休息室,访问共享数据的代码称为critical section。一个线程获取锁,然后进入临界区,发现某些条件不满足,然后调用锁对象上的wait方法,然后线程释放掉锁资源,进入锁对象上的wait set。由于线程释放释放了理解资源,其他线程可以获取所资源,然后执行,完了以后调用notify,通知锁对象上的等待线程。

Ps:若调用notify();则随机拿出(这随机拿出是内部的算法,无需了解)一条在等待的资源进行准备进入Critical Section;若调用notifyAll();则全部取出进行准备进入Critical Section。

6. 总结与展望

扩展建议:如何扩展Java并发知识

1、Java Memory Mode : JMM描述了java线程如何通过内存进行交互,了解happens-before , synchronized,voliatile & final

2、Locks % Condition:Java锁机制和等待条件的高层实现 java.util,concurrent.locks

3、线程安全性:原子性与可见性, java.util.concurrent.atomic synchronized(锁的方法块)&volatile(定义公共资源) DeadLocks(死锁)--了解什么是死锁,死锁产生的条件

4、多线程编程常用的交互模型

· Producer-Consumer模型(生产者-消费者模型)

· Read-Write Lock模型(读写锁模型)

· Future模型

· Worker Thread模型

考虑在Java并发实现当中,有哪些类实现了这些模型,供我们直接调用

5、Java5中并发编程工具:java.util.concurrent 包下的

例如:线程池ExcutorService 、Callable&Future 、BlockingQueue

6、推荐书本:CoreJava 、 JavaConcurrency In Practice