Java多线程(一)基础知识

96
闽越布衣
2017.11.07 23:47* 字数 5555

线程概述

线程与进程

进程

 每个运行中的任务(通常是程序)就是一个进程。当一个程序进入内存运行时,即变成了一个进程。每一个进程都有一定的独立功能,进程是系统进行资源分配与调度的一个独立单元。

  • 独立性:进程是系统中独立存在的实体,它可以拥有自己独立的资源,每一个进程都有自己私有的地址空间。在没有进程本身允许的条件下,一个用户进程不可以直接访问其他进程的地址空间。
  • 动态性:进程与程序的区别在于,程序只是一序列静态指令的集合,而进程则是正在执行中的程序,拥有自己的生命周期和各种不同的状态,是动态产生、变化以及消亡的。
  • 并发性:多个进程可以在单个处理器上并发执行,并且不会互相影响。
  • 异步性:由于进程的相互制约,使进程具有执行的间断性,即进程按各自独立的、 不可预知的速度向前推进。异步性会导致执行结果的不可再现性,为此,在操作系统中必须配置相应的进程同步机制。
  • 结构性:进程包含程序及其相关数据结构。进程的实体包含进程控制块(PCB),程序块、数据块和堆栈,又称为进程映像。

线程

 线程是进程的执行单元,是进程的组成部分,一个进程可以拥有多个线程,一个线程必须有一个父进程。线程可以拥有自己的堆栈,自己的程序计数器以及自己的局部变量,但不拥有系统资源,它与父进程的其它线程共享该进程的全部资源。
 一个线程可以创建和销毁另一个线程,同一个进程中的多个线程之间可以并发执行。
 线程是程序中一个单一的顺序控制流程。进程内一个相对独立的、可调度的执行单元,是系统独立调度和分派CPU的基本单位。在单个程序中同时运行多个线程完成不同的工作,称为多线程。

  • 进程之间不能共享内存,但线程之间共享内存非常容易。
  • 系统创建一个进程需要为该进程分配独立的内存空间,并分配大量的相关资源,但创建线程则代价小得多,因此使用多线程来实现多任务比使用多进程的效率高。
  • 因为线程划分的尺度小于进程,使得多线程程序的并发性高。

线程实现

继承Thread类

  1. 定义Thread类的子类,并重写run()方法,该run()方法的方法体就代表了线程要完成的任务即线程的执行体;
  2. 创建Thread子类的实例,即创建线程对象;
  3. 调用线程对象的start()方法来启动该线程。(注意,不是直接调用对象的run()方法,调用对象的run()方法,其实就相当于普通的方法调用,并不会创建线程);


    通过继承Thread类实现线程.png

    线程执行结果.png

     从图中可以看到,程序创建了三个线程,包含一个主线程和两个子线程。并且可以看到,线程输出并不是连续的,这是因为线程的执行是基于系统资源调度执行的。
     从图中可以看出,sum变量不是连续的。使用继承Thread的方法来创建线程类时,多个线程之间无法共享线程类的实例变量;因为每次创建线程对象时都是需要创建一个MyThread对象,每个对象都包含自己的实例变量。

实现Runnable接口

  1. 定义实现Runnable接口的实现类,并重写接口的run()方法,该run()方法的方法体就代表了线程要完成的任务即线程的执行体;
  2. 创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象;
  3. 调用Thread对象的start()方法来启动该线程。(注意,不是直接调用Runnable对象的run()方法,调用Runnable对象的run()方法,其实就相当于普通的方法调用,并不会创建线程);


    通过实现Runnable接口实现线程.png

    线程执行结果.png

     从图中可以看出,sum变量时连续的。使用实现Runnable的方法来创建线程类时,多个线程之间可以共享线程类的实例变量;因为在这种方式下,程序所创建的Runnable对象只是线程的target,而多个线程可以共享同一个target,所以多个线程可以共享同一个线程类(线程的target类)的实例变量。

  • String getName() : 获取当前线程的名称;
  • void run() : 线程的执行体,线程需要完成的任务都在该方法中实现;
  • void start() :线程对象通过调用此方法来启动线程;
  • Thread Thread.currentThread() : 返回当前正在执行的线程;

通过Callable和Future创建线程

  1. 创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值;
  2. 创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值;
  3. 使用FutureTask对象作为Thread对象的target创建并启动新线程;
  4. 调用FutureTask对象的get()方法来获得子线程执行结束后的返回值,注意不是直接调用Callable对象的call()方法获取返回值,Callable对象的call()方法为线程的执行体被调用;


    通过Callable和Futrue创建线程.png

    线程执行结果.png

     从图中可以看到当主线程的sum变量循环到20时,程序启动以FutureTask对象为target的线程,然后通过调用FutureTask对象的get()方法来获得call()方法的返回值。该方法将导致程序的主进程被阻塞,直到call()方法结束并返回为止。

Callable 接口方法

  • V call() : 线程的执行体,线程需要完成的任务都在该方法中实现,并带有返回值;

Future接口方法

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

创建线程三种方式的比较

采用继承Thread类创建线程的优缺点

  • 劣势:因为线程类已经继承了Thread类,不能在继承别的父类;
  • 优势:编写简单,如果访问当前线程,则无需使用Thread.currentThrend()方法,直接使用this就可获得当前线程;

采用实现Runnable,Callable接口创建线程的优缺点

  • 优势:线程类只实现了Runnable接口或Callable接口,还可以继续继承其他类;
  • 优势:在这种情况下,多个进程可以共享同一个target对象,所以非常适合多个相同的线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好的体现了面向对象的思想;
  • 劣势:编程稍微复杂,如果需要访问当前线程,需要使用Thread.currentThrend()方法获取;

线程状态

  • 新建状态(NEW)
  • 可运行状态(RUNNABLE)
  • 阻塞状态(BLOCKED)
  • 等待状态(WAITING)
  • 计时等待状态(TIMED_WAITING)
  • 终止状态(TERMINATED)

新建状态(NEW)

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

可运行状态(RUNNABLE)

 当线程对象调用了start()方法后,线程处于runnable状态,JVM为其创建方法调用栈和程序计数器。处于runnable状态的线程,可能正在运行也可能没有运行,这取决于JVM里线程调度器的调度,当线程获得CPU时间片时,线程执行。
 当一个线程开始运行时,它不可能处于一直运行的状态(除非它的线程执行体足够短,瞬间就执行结束了)。线程在运行过程中需要被中断,目的是让其他线程获得执行的机会,线程调度的细节依赖于操作系统提供的服务。

阻塞与(计时)等待状态(BLOCKED,WAITING,TIMED_WAITING)

 当线程处于阻塞或者等待状态时,它暂时不活动。直到线程调度器重新激活它。细节取决于它是怎么达到非活动状态的。

  • 当一个线程试图获取一个内部的对象锁(而不是java.util.concurrent库中的锁),而该锁被其他线程持有时,则该线程进入阻塞状态。当所有其他线程释放该锁,并且线程调度器允许本线程持有它的时候,该线程将变成非阻塞状态。
  • 当线程等待另一个线程通知调度器一个条件时,它自己进入等待状态。在调用Object.wait方法或Thread.join方法,或者是等待java.util.concurrent库中的Lock或Condition时,就会出现这种情况。
  • 有几个方法有一个超时参数。调用它们导致线程进入计时等待状态。这一状态将一直保持到超时期满或者接收到适当的通知。带有超时参数的方法有Thread.sleep和Object.wait、Thread.join、Lock.tryLock以及Condition.await的计时版。

终止状态(TERMINATED)

 线程因如下原因之一而被终止:(当然还有一种调用stop()方法,不过该方法已过时,不建议调用)

  • 因为run()方法或call()方法执行完成,结束后线程就自然死亡。
  • 因为一个没有捕获的Exception或Error而意外死亡。


    线程状态转换图(来自一哥们).png

Thread 方法

  • void join() : 等待终止指定的线程;
  • void join(long millis) : 等待指定的线程死亡或者经过指定的毫秒数;
  • Thread.State getState() : 得到这一线程的状态;NEW,RUNNABLE,BLOCKED,WAITING,TIMED_WAITING或者TERMINATED之一;

线程属性

线程优先级

 每个线程执行都有一定的优先级,优先级高的获得较多的执行机会,优先级低的执行机会先对较少。默认情况下,一个线程继承它的父线程优先级;默认情况下,main线程具有普通优先级,由main创建的线程也具普通优先级。
 Thread类提供setPriority(int newPriority),getPriority()方法来设置和获取指定线程的优先级。可以将优先级设置为MIN_PRIORITY(Thread类中定义为1)与MAX_PRIORITY(定义为10)之间的任何值。默认NORM_PRIORITY被定义5。
 每当线程调度器有机会选择新线程时,他首先选择具有高优先级的线程。但是线程优先级是高度依赖于操作系统的。不同操作系统上的优先级并不相同,而且也不能很好的和Java的10个优先级对应。(例如Windows有7个优先级。一些Java优先级将映射到相同的操作系统优先级。在Oracle为Linux提供的Java虚拟机中,线程的优先级被忽略——所有的线程具有相同的优先级。(来自Java核心技术 卷I))。所以应尽量避免直接为线程指定优先级。

Thread 方法

  • void setPriority(int newPriority) : 设置线程的优先级。优先级必须在Thread.MIN_PRIORITY与Thread.MAX_PRIORITY之间,一般使用Thread.MIN_PRIORITY优先级。
  • static int MIN_PRIORITY : 线程的最小优先级,最小优先级的值为1。
  • static int NORM_PRIORITY : 线程的默认优先级,默认优先级的值为5。
  • static int MAX_PRIORITY : 线程的最大优先级,最大优先级的值为10。
  • static native void yield() : 导致当前执行线程处于让步状态。如果其他的可运行线程具有至少与此线程同样高的优先级,那么这些线程接下来会被调度。

守护线程

 可以通过调用setDaemon(true)将线程转换为守护线程。守护线程的唯一用途就是为其它线程提供服务。当只剩下守护线程时虚拟机就退出了,由于如果只剩下守护线程,就没必要继续运行程序了。
 守护线程应该永远不去访问固有资源,如文件、数据库,因为它会在任何时候甚至在一个操作的中间发生中断。

Thread 方法

  • void setDaemon(boolean isDaemon) : 标示该线程为守护线程或者用户线程。这一方法必须在线程启动之前调用。
  • boolean isDaemon() : 判断线程是否是守护线程。

常用方法

  • public synchronized void start() : 使该线程开始执行;Java 虚拟机调用该线程的 run 方法。
  • public void run() : 线程的执行体,线程的执行任务在该方法完成。
  • public final synchronized void setName(String name) : 设置线程的名称。
  • public final void setPriority(int newPriority) : 设置线程的优先级。
  • public final void setDaemon(boolean on) : 将该线程设置为守护线程或者用户线程。守护线程和用户线程的区别在于:守护线程依赖于创建它的线程,而用户线程则不依赖。必须在thread.start()之前设置,否则会抛出一个IllegalThreadStateException异常
  • public final void join() : 等待该线程终止。
  • public final synchronized void join(long millis) : 等待该线程终止的时间最长为 millis 毫秒。
  • public final synchronized void join(long millis, int nanos) : 等待该线程终止,当 999999 > nanos > 500000 时,最长等待时间为 millis + 1;当 millis = 0 && nanos != 0 ,最长等待时间为1毫秒。
  • public void interrupt() : 向线程发送中断请求。线程的中断状态将被设置为true。如果目前该线程被一个sleep调用阻塞,那么将抛出InterruptedException异常。
  • public static boolean interrupted() : 测试当前线程(即正在执行这一命令的线程)是否被中断。这一调用会产生副作用——它将当前线程的中断状态设置为false。
  • public boolean isInterrupted() : 测试线程是否被终止。这一调用不会改变线程的中断状态。
  • public final native boolean isAlive() : 测试线程是否处于活动状态(线程处于正在运行或准备开始运行的状态)。
  • public static native void sleep(long millis) : 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。有一点要非常注意,sleep方法不会释放锁,也就是说如果当前线程持有对某个对象的锁,则即使调用sleep方法,其他线程也无法访问这个对象。如果调用了sleep方法,必须捕获InterruptedException异常或者将该异常向上层抛出。当线程睡眠时间满后,不一定会立即得到执行,因为此时可能CPU正在执行其他的任务。所以说调用sleep方法相当于让线程进入阻塞状态。
  • public static void sleep(long millis, int nanos) : 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),当 999999 > nanos >= 500000 时,暂停时间为 millis + 1 毫秒,当 nanos != 0 && millis == 0 暂停时间为1毫秒。
  • public static native void yield() : 导致当前执行线程处于让步状态。如果有其他可运行的线程具有至少与此线程同样高的优先级,那么这些线程接下来会被调度。调用yield方法会让当前线程交出CPU权限,让CPU去执行其他的线程。它跟sleep方法类似,同样不会释放锁。但是yield不能控制具体的交出CPU的时间。调用yield方法并不会让线程进入阻塞状态,而是让线程重回就绪状态,它只需要等待重新获取CPU执行时间,这一点是和sleep方法不一样的。
  • public static native Thread currentThread() : 返回当前执行线程的Thread对象。

线程面试

什么是线程?

 线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。

线程与进程的区别?

 进程是一个独立的运行环境,它可以被看作是一个程序或者一个应用。而线程是在进程中执行的一个任务。线程是进程的子集,一个进程可以有很多线程,每条线程并行执行不同的任务。不同的进程使用不同的内存空间,而所有的线程共享一片相同的内存空间。别把它和栈内存搞混,每个线程都拥有单独的栈内存用来存储本地数据。

如何在Java中实现线程?

  • 继承Thread类;
  • 实现Runnable接口;
  • 通过Callable和Future创建线程;

Thread 类中的start() 和 run() 方法有什么区别?

 start()方法被用来启动新创建的线程,而且start()内部调用了run()方法,这和直接调用run()方法的效果不一样。当你调用run()方法的时候,只会是在原来的线程中调用,没有新的线程启动,start()方法才会启动新线程。

Runnable和Callable有什么不同?

 Runnable和Callable都代表那些要在不同的线程中执行的任务。Runnable从JDK1.0开始就有了,Callable是在JDK1.5增加的。它们的主要区别是Callable的 call() 方法可以返回值和抛出异常,而Runnable的run()方法没有这些功能。Callable可以返回装载有计算结果的Future对象。

Thread类的sleep()方法和对象的wait()方法都可以让线程暂停执行,它们有什么区别?

 sleep()方法是线程类(Thread)的静态方法,调用此方法会让当前线程暂停执行指定的时间,将执行机会(CPU)让给其他线程,但是对象的锁依然保持,因此休眠时间结束后会自动恢复(线程回到就绪状态)。wait()是Object类的方法,调用对象的wait()方法导致当前线程放弃对象的锁(线程暂停执行),进入对象的等待池,只有调用对象的notify()方法(或notifyAll()方法)时才能唤醒等待池中的线程进入等锁池,如果线程重新获得对象的锁就可以进入就绪状态。

线程的sleep()方法和yield()方法有什么区别?

  • sleep()方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会;yield()方法只会给相同优先级或更高优先级的线程以运行的机会;
  • 线程执行sleep()方法后转入阻塞状态,而执行yield()方法后转入就绪状态;
  • sleep()方法声明抛出InterruptedException,而yield()方法没有声明任何异常;
  • sleep()方法比yield()方法(跟操作系统CPU调度相关)具有更好的可移植性。

    整理文章主要为了自己日后复习用,文章中可能会引用到别的博主的文章内容,如涉及到博主的版权问题,请博主联系我。

Java多线程
Web note ad 1