Java多线程

来源:互联网 发布:千语淘客助手类似软件 编辑:程序博客网 时间:2024/06/03 19:42

线程概述

线程和进程

线程可以拥有自己的堆栈、自己的程序计数器和自己的局部变量,但不拥有系统资源,它与父进程的其他线程共享该进程所拥有的全部资源。 因此编程更加方便,但必须更加小心,因为需要确保线程不会妨碍同一进程里的其他线程。
一个线程可以创建和撤销另一个线程。

多线程的优势

  1. 进程之间不能共享内存,但线程之间共享内存非常容易。
  2. 系统创建进程时需要为该进程重新分配系统资源,但创建线程则代价小得多,因此使用多线程来实现多任务开发比多进程的效率高。(线程共享的环境包括:进程代码段、进程的公有数据等。利用这些共享的数据,线程很容易实现相互之间的通信。)
  3. Java语言内置了多线程概念支持,而不是单纯地作为底层操作系统的调度方式,从而简化了Java的多线程编程。

线程的创建和启动

Java使用Thread类代表线程,所有线程对象都必须是Thread类或其子类的实例。

继承Thread类创建线程类继承Thread类创建线程类

通过继承Thread类来创建并启动多线程的步骤如下。

  1. 定义Thread类的子类,并重写改类的run()方法,该run()方法体就代表了线程需要完成的任务,因此把run()方法称为线程执行体。
  2. 创建Thread子类的实例,即创建了线程对象。
  3. 调用线程对象的start()方法来启动该线程。

实现Runnable接口创建线程类

实现Runnable接口来创建并启动线程的步骤如下。

  1. 定义Runnable接口的实现类,并重写改接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
  2. 创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
  3. 调用线程对象的start()方法启动该线程。

Runnable对象仅仅作为Thread的target,Runnable实现类里包含的run()方法作为线程执行体。而实际的线程对象依然是Thread实例,只是该Thread线程负责执行其target的run()方法。

通过继承Thread类来获得当前线程对象比较简单,直接使用this就可以了;但通过实现Runnable接口来获得当前线程对象,则必须使用Thread.currentThread()方法。

Runnable是函数式接口,可以使用Lambda表达式创建Runnable接口。

采用Runnable接口的方式创建的多个线程可以共享线程类的实例变量。这是因为在这种方式下,程序所创建的对象只是线程的target,而多个线程可以共享同一个target,而多个线程可以共享同一个线程类(实际上应该是线程的target)的实例变量。

使用Callable和Future创建线程

Callable接口提供了一个call()方法可以作为线程的执行体,但call()方法比run()方法功能更强大。

  1. call()方法可以有返回值
  2. call()方法可以声明抛出异常

Java 5提供了Future接口来代表Callable接口里call()方法的返回值,并为Future接口提供了一个FutureTask实现类,该实现类实现了Future接口,并实现了Runnable接口——可以作为Thread的target。

boolean cancel(boolean mayInterruptIfRunning):试图取消该Future里关联的Callable任务。V get():返回Callable任务里call()方法的返回值。调用该方法将导致程序阻塞,必须等到子线程结束后才会得到返回值V get(long timeout,TimeUnit unit):返回Callable任务里的call()方法的返回值。该方法让程序最多阻塞timeout和unit指定时间,如果再指定时间Callable任务依然没有返回值,将会抛出TimeoutException异常。boolean isCancelled():如果再Callabvle任务正常完成前被取消,则返回trueboolean isDone():如果Callable任务已完成,则返回true

创建并启动有返回值的线程的步骤如下。

  1. 创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,且该call()方法有返回值,再创建Callable实现类的实例。从Java 8开始,可用直接使用Lambda表达式创建Callable对象.
  2. 使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值
  3. 使用FutureTask对象作为Thread对象的target创建并启动新线程

创建线程的三种方式对比

通过继承Thread类或实现Runnable、Callable接口都可以实现多线程,不过实现Runnable接口与实现Callable接口方式基本相同,只是Callable接口里定义的方法有返回值,可以声明抛出异常而已。因此可以将实现Runnable接口和实现Callable接口归为一种方式。这种方式与继承Thread方式之间的主要差别如下。

采用实现Runnable、Callable接口的方式创建多线程的优点:

  1. 线程类只是实现了Runnable接口或Callable接口,还可以继承其他类。
  2. 在这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。

劣势是:
编程稍稍复杂,如果需要访问当前线程,则必须使用Thread.currentThread()方法。

采用继承Thread()类的方式创建多线程的优缺点:

劣势是:线程已经继承了Thread类,不能再继承其他父类。
优势是:编程简单,如果需要访问当前线程,则无需使用Thread.currentThread()方法,直接使用this即可获得当前线程。

基于上面分析,因此一般推荐采用Runnable接口、Callable接口的方式来创建多线程。

Java线程的生命周期

新建和就绪状态

当程序使用new关键字创建了一个线程之后,该线程就处于新建状态,此时它和其他的Java对象一样,仅仅由Java虚拟机为其分配内存,并初始化其成员变量的值。此时线程对象没有表现出任何线程的动态特征,程序也不会执行线程的线程执行体。

当线程调用了start()方法之后,该线程处于就绪状态,Java虚拟机 会为其创建方法调用栈和程序计数器处于这个状态中的线程并没有开始运行,只是表示该线程可以运行了。至于该线程何时开始运行,取决于JVM里线程调度器的调度。

注意:启动线程使用start()方法,而不是run()方法,永远不要调用线程的run()方法!如果调用start()方法来启动线程,系统会把该run()方法当成线程执行体来处理;但如果直接调用线程的run()方法,则run()立即被执行,而且在run()方法返回之前其他线程无法并发执行——也就是说,如果直接调用线程对象的run()方法,系统会把线程对象当初一个普通对象,而run()方法也是一个普通方法,而不是线程执行体。

只能对处于新建状态的线程调用start()方法,否则将会引发IllegalThreadStateException异常。

运行和阻塞状态

程序从阻塞状态只能进入就绪状态,无法直接进入运行状态而就绪状态和运行状态之间的转换通常不受程序控制,而是由系统线程调度决定,当处于就绪状态的线程获得处理器资源时,该线程进入运行状态。当运行状态的线程失去处理器资源时,程序就进入就绪状态。但有个方法例外,调用yield()方法可以让运行状态的线程转入就绪状态。

线程死亡

线程会以如下三种方式结束:

  1. run()或者call()方法执行完成,线程处于死亡状态
  2. 线程抛出一个未捕获的Exception或Error
  3. 直接调用该线程的stop()方法结束该线程——该方法容易导致死锁。

注意:当主线程结束时,其他线程不受影响,并不会随之结束。一旦子线程启动起来后,它就拥有和主线程相同的地位,他不会受主线程的影响。

为了测试某个线程是否已经死亡,可以调用线程对象的isAlive()方法,当线程处于就绪、运行、阻塞三种状态时,该方法将会返回true;当线程处于新建、死亡两种状态时,该方法将返回false。

不要试图对一个已经死亡的线程调用start()方法使它重新启动,死亡就是死亡,该线程不可再次作为线程执行。

控制线程

join线程

让一个线程等待另一个线程完成的方法——join()方法。当在某个程序执行流中调用其他线程的join()方法时,调用线程将被阻塞,知道被join()方法加入join线程执行完为止。

后台线程

有一种线程,它是在后台运行的,它的任务是为其他线程提供服务,这种线程被称为“后台线程(Demon Thread)”,又称为“守护线程”或“精灵线程”。JVM的垃圾回收线程就是典型的后台线程。后台线程有个特征:如果所有的前台线程都死亡,后台线程会自动死亡。调用Thread对象的setDaemon(true)方法可将指定线程设置成后台线程。
前台线程死亡后,JVM会通知后台线程死亡,但从它接收指令到做出相应,需要一定时间,而且要将某个线程设置为后台线程,必须在该线程启动之前设置,也就是说,setDaemon(true)必须在start()方法之前调用,否则会引发IlleaglThreadStateException异常。

线程睡眠:sleep

如果需要让当前正在执行的线程暂停一段时间,并进入阻塞状态,则可以通过调用sleep()方法来实现。
当前调用sleep()方法进入阻塞状态后,在其睡眠时间段内,该线程不会获得执行的机会,即使系统中没有其他可执行的线程,处于sleep()中的线程也不会执行,因此sleep()方法常用来暂停程序的执行。

线程让步:yield

yield()方法也可以让当前正在执行的线程暂停,但它不会阻塞该进程,它只是将该线程转入就绪状态。
yield()只是让当前线程暂停一下,让系统的线程调度器重新调度一次,完全可能的情况是:当某个线程调用了yield()方法暂停之后,线程调度器又将其调度出来重新执行。

实际上,当某个线程调用了yield()方法暂停之后,只有优先级与当前线程相同,或者优先级逼当前线程更高的处于就绪状态的线程才会获得执行的机会。

关于sleep()方法和yield()方法的区别如下。

  1. sleep()方法暂停当前线程后,会给其他线程执行机会,不会理会其他线程的优先级;但yield()方法只会给优先级相同,或优先级更高的线程执行机会
  2. sleep()方法会将线程转入阻塞状态,直到经过阻塞时间才会转入就绪状态;而yield()不会将线程转入到阻塞状态,它只是强制当前线程进入就绪状态。因此完全有可能某个线程调用yield()方法暂停之后,立即再次获得处理器资源被执行
  3. sleep()方法声明抛出了InterruptedException异常,所以调用sleep()方法时要么捕捉该异常,要么显式声明抛出该异常。而yield()方法则没有声明抛出任何异常。
  4. sleep()方法比yield()方法有更好的可移植性,通常不建议使用yield()方法来控制并发线程的执行。

线程池

理解Executor接口的设计思路
戏(细)说Executor框架线程池任务执行全过程(上)
戏(细)说Executor框架线程池任务执行全过程(下)
Java多线程编程中Future模式的详解
Java多线程编程中Master-Worker模式的详解
Java多线程编程中生产者-消费者模式的详解
Java多线程编程中不变模式的详解
Guarded Suspension 模式
FutureTask 深度解析
Executor实现—-AbstractExecutorService实现分析

0 0