线程概述
线程与进程
进程
每个运行中的任务(通常是程序)就是一个进程。当一个程序进入内存运行时,即变成了一个进程。每一个进程都有一定的独立功能,进程是系统进行资源分配与调度的一个独立单元。
- 独立性:进程是系统中独立存在的实体,它可以拥有自己独立的资源,每一个进程都有自己私有的地址空间。在没有进程本身允许的条件下,一个用户进程不可以直接访问其他进程的地址空间。
- 动态性:进程与程序的区别在于,程序只是一序列静态指令的集合,而进程则是正在执行中的程序,拥有自己的生命周期和各种不同的状态,是动态产生、变化以及消亡的。
- 并发性:多个进程可以在单个处理器上并发执行,并且不会互相影响。
- 异步性:由于进程的相互制约,使进程具有执行的间断性,即进程按各自独立的、 不可预知的速度向前推进。异步性会导致执行结果的不可再现性,为此,在操作系统中必须配置相应的进程同步机制。
- 结构性:进程包含程序及其相关数据结构。进程的实体包含进程控制块(PCB),程序块、数据块和堆栈,又称为进程映像。
线程
线程是进程的执行单元,是进程的组成部分,一个进程可以拥有多个线程,一个线程必须有一个父进程。线程可以拥有自己的堆栈,自己的程序计数器以及自己的局部变量,但不拥有系统资源,它与父进程的其它线程共享该进程的全部资源。
一个线程可以创建和销毁另一个线程,同一个进程中的多个线程之间可以并发执行。
线程是程序中一个单一的顺序控制流程。进程内一个相对独立的、可调度的执行单元,是系统独立调度和分派CPU的基本单位。在单个程序中同时运行多个线程完成不同的工作,称为多线程。
- 进程之间不能共享内存,但线程之间共享内存非常容易。
- 系统创建一个进程需要为该进程分配独立的内存空间,并分配大量的相关资源,但创建线程则代价小得多,因此使用多线程来实现多任务比使用多进程的效率高。
- 因为线程划分的尺度小于进程,使得多线程程序的并发性高。
线程实现
继承Thread类
- 定义Thread类的子类,并重写run()方法,该run()方法的方法体就代表了线程要完成的任务即线程的执行体;
- 创建Thread子类的实例,即创建线程对象;
-
调用线程对象的start()方法来启动该线程。(注意,不是直接调用对象的run()方法,调用对象的run()方法,其实就相当于普通的方法调用,并不会创建线程);
从图中可以看到,程序创建了三个线程,包含一个主线程和两个子线程。并且可以看到,线程输出并不是连续的,这是因为线程的执行是基于系统资源调度执行的。
从图中可以看出,sum变量不是连续的。使用继承Thread的方法来创建线程类时,多个线程之间无法共享线程类的实例变量;因为每次创建线程对象时都是需要创建一个MyThread对象,每个对象都包含自己的实例变量。
实现Runnable接口
- 定义实现Runnable接口的实现类,并重写接口的run()方法,该run()方法的方法体就代表了线程要完成的任务即线程的执行体;
- 创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象;
-
调用Thread对象的start()方法来启动该线程。(注意,不是直接调用Runnable对象的run()方法,调用Runnable对象的run()方法,其实就相当于普通的方法调用,并不会创建线程);
从图中可以看出,sum变量时连续的。使用实现Runnable的方法来创建线程类时,多个线程之间可以共享线程类的实例变量;因为在这种方式下,程序所创建的Runnable对象只是线程的target,而多个线程可以共享同一个target,所以多个线程可以共享同一个线程类(线程的target类)的实例变量。
- String getName() : 获取当前线程的名称;
- void run() : 线程的执行体,线程需要完成的任务都在该方法中实现;
- void start() :线程对象通过调用此方法来启动线程;
- Thread Thread.currentThread() : 返回当前正在执行的线程;
通过Callable和Future创建线程
- 创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值;
- 创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值;
- 使用FutureTask对象作为Thread对象的target创建并启动新线程;
-
调用FutureTask对象的get()方法来获得子线程执行结束后的返回值,注意不是直接调用Callable对象的call()方法获取返回值,Callable对象的call()方法为线程的执行体被调用;
从图中可以看到当主线程的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而意外死亡。
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调度相关)具有更好的可移植性。
整理文章主要为了自己日后复习用,文章中可能会引用到别的博主的文章内容,如涉及到博主的版权问题,请博主联系我。