文章目录
线程概述
几乎所有的操作系统都支持进程的概念,所有运行中的任务通常对应一个进程。当一个程序进入内存运行时,即变成一个进程。进程是处于运行中的程序,并且具有一定的独立功能。进程也可以理解成程序的一次执行过程,是系统运行程序的基本单元。大部分操作系统都支持多进程并发运行,首先要注意并发和并行是两个不同的概念:
- 并发性:在同一时刻只有一条指令在执行,但多个进程指令被快速轮换执行,使得同一段时间段上来看,有多个进程同时执行的效果
- 并行性:指在同一时刻,多个指令同时执行
由于CPU不断地在进程之间轮换执行,且执行速度相对人的感觉来说,非常快,使得用户感觉多个进程在同时执行。
而多线程扩展了多进程的概念,使得同一进程可以同时并发处理多个任务。
- 线程是进程的执行单元,负责当前进程中程序的执行,一个进程可以拥有多个线程,一个线程必有一个父进程。
- 线程可以拥有自己的堆栈、自己的程序计数器和自己的局部变量,但不拥有系统资源,它与父进程的其他线程共享该进程所拥有的的全部资源。
多线程编程具有如下的优点:
- 进出之间不能共享内存,但线程之间共享内存非常容易
- 系统创建进程时需要为该进程重新分配系统资源,但创建线程则代价小得多,因此使用多线程来实现多任务并发比多进程的效率高
线程的创建和启动
Java使用Thread类代表线程,所有的线程对象都必须是Thread类或其子类的实例。
当Java程序开始运行后,程序至少会创建一个主线程,即由main()方法确定的,由main()方法的方法体代表主线程的线程执行体
继承Thread类创建线程类
通过继承Thread类来创建并启动多线程的步骤如下:
- 定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务,因此把run()方法称为线程执行体
- 创建Thread子类的实例,即创建了线程对象
- 调用线程对象的start()方法来启动该线程
- 多次启动一个线程是非法的,特别是线程已经结束执行后,不能再重新启动
- Java程序属于抢占式调度,哪个线程的优先级高,哪个线程优先执行
【Thread类】
构造方法:
- public Thread() - 分配一个新的线程对象
- public Thread(String name) - 分配一个指定名字的新的线程对象
- public Thread(Runnable target) - 分配一个带有指定目标新的线程对象
- public Thread(Runnable target,String name) - 分配一个带有指定目标新的线程对象并指定名字
常用方法:
- public String getName() - 获取当前线程名称
- public String setName(String name) - 设置线程名称
- public void start() - 导致此线程开始执行
- public void run() - 此线程要执行的任务在此处定义代码
- public static void sleep(long millis) - 使当前正在执行的线程以指定的毫秒数暂停
- public static Thread currentThread() - 返回对当前正在执行的线程对象的引用。
实现Runnable接口创建线程类
实现Runnable接口来创建并启动多线程的方法如下:
- 定义Runnable接口的实现类,并重写该接口的run()方法,该run方法的方法体同样是该线程的线程执行体
- 创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象
- 调用线程对象的start()方法来启动该线程
- Runnable对象仅仅作为Thread对象的target,Runnable实现类里包含的run()方法仅作为线程执行体,设计的线程对象依然是Thread实例,只是该Thread线程负责执行其target的run()方法
使用Callable和Future创建线程
通过是按Runnable接口创建多线程时,Thread类的作用就是把run()方法包装成线程执行体,但是Java不能直接将任意方法都包装成线程执行体
从Java 5开始,Java提供了Callble接口,可以说是Runnable接口的增强版,Callable接口提供了一个call()方法作为线程执行体,比run()方法功能更加强大
- call()方法可以有返回值
- call()方法可以声明抛出异常
因此可提供一个Callable对象作为Thread的target,而该线程的线程执行体就是该Callable对象的call()方法。
但是,Callable接口不是Runnable的子接口,不能直接作为Thread的target。而且call()方法还有一个返回值——call()方法不是直接调用,是作为线程执行体被调用,Java 5提供了Future接口来代表Callable接口里的call()方法的返回值,并为Future接口提供了一个FutureTask实现类,该实现类实现了Future接口,并实现了Runnable接口——可以作为Thread类的targer。
创建并启动返回值的线程步骤如下:
- 创建Callable接口的实现类,并实现call方法,该call()方法将作为线程执行体,且该call()方法有返回值,再创建Callable实现类的实例
- 使用FutureTask类来包装Callable对象,该FutureTask对象封装了Callable对象的call()方法的返回值
- 使用FutureTask对象作为Thread对象的target创建并启动新线程
- 调用FutureTask对象的get()方法来获得子线程执行结束后的返回值
创建线程的三种方式的对比
实现Runnable接口以实现Callable接口的方式基本相同,可以归为一种方式,采用实现Runbable、Callable接口的方式有如下优点:
- 线程类只实现了接口,还可以继承其他类,可以避免java中单继承的局限性
- 多个线程可以共享同一个target对象,适合多个相同线程来处理同一份资源
匿名内部类方式实现线程的创建
使用线程的内匿名内部类方式,可以方便的实现每个线程执行不同线程任务操作。
- 使用匿名内部类的方式实现Runnable接口,重新Runnable接口中的run()方法
线程状态
当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态,在线程的生命周期中,会出现以下几种状态:
- New - 初始状态,新建状态,线程被构建,还没有调用start()方法
- 如果直接调用线程对象的run()方法,系统把线程对象当成一个普通对象,而run()方法也是一个普通方法,不是线程执行体,程序只有一个主线程运行
- 启动线程使用的是start()方法
- Runnable、Running - 这里不同的资料有不同的说法,以《JAVA并发编程的艺术》书为例,Java将就绪和运行两种状态统称为“运行中”,线程在调用start()方法后,会进入Runnable(就绪)状态。线程得到处理器资源后,会进入Ruuning(运行)状态,反过来可以用yield()方法,后面会介绍。
- Blocked - 阻塞状态,线程失去了处理器资源,或线程在等待某个通知,用锁的概念,可以理解成当一个线程视图获得一个对象锁,而该对象锁被其他线程持有,进入阻塞状态。
- 在如下情况下,线程为进入阻塞状态
- 线程调用了一个阻塞式IO方法,在该方法返回之前,该线程被阻塞
- 线程视图获得一个同步监视器锁,但该同步监视器锁正被其他简称所持有。
- 线程在等待某个通知(notify)
- 程序调用了线程的suspend()方法将线程挂起(使用resume()方法恢复),此时易造成死锁
- 【注意】被阻塞线程的阻塞解除后,进入就绪状态,而不是直接进入运行状态。
- 在如下情况下,线程为进入阻塞状态
- Waiting - 无限等待,一个线程在等待另一个线程执行一个(唤醒)动作时,进入Waiting状态。进入这个状态后是不能自动唤醒的,需要notify或notifyAll方法才能够唤醒
- Timed_Waiting - 计时等待,跟Waiting状态类似,设置了超时参数,这一状态将一直保持到超时期满或者接受到唤醒通知。
- Terminated - 终止状态,run()方法正常退出而死亡,或者直接调用stop()方法——该方法容易导致死锁,不推荐使用
控制线程
Java的线程支持提供了一些便捷的工具方法,通过这些工具方法可以很好的控制线程的执行
join线程
Thread提供了让一个线程等待另一个线程完成的方法——join()方法
- join()方法:当在某个程序执行流中调用其他线程的join()方法时,调用线程将被阻塞,直到被join()方法加入的join线程执行完为止
- join()方法通常有使用 线程的程序调用,以将大问题划分为许多小问题,每个小问题分配一个线程,当所有的小问题都得到处理后,再调用主线程来操作
后台线程
Daemon Thread——守护线程,或称为“精灵线程”,是一种在后台运行的后台线程,为其他线程提供服务,JVM的垃圾回收线程就是典型的后台线程。
- 后台线程有个特征:如果所有的前台线程都死亡,后台线程会自动死亡。
- 调用Thread对象的setDaemon(true)方法可将指定线程设置成后台线程
- 主线程默认是前台线程
线程睡眠:sleep
如果需要让当前正在执行的线程暂停一段时间,并进入阻塞状态,可以通过调用Thread类的静态方法sleep()来实现
- 当当前线程调用sleep()方法进入阻塞状态后,在其睡眠时间段内,该线程不会获得执行的机会,即使系统中没有其他可执行的线程,处于sleep()中的线程也不会执行,因此sleep()方法常用来暂停程序的执行
线程让步:yield
yield()方法是一个和sleep()方法有点相似的办法,它也可以让当前正在执行的线程暂停,但它不会阻塞该线程,它只是将该线程转入就绪状态
- yield()只是让当前线程暂停一下,让系统的线程调度器重新执行一次,完全可能的情况是:当某个线程调用了yield()方法暂停之后,线程调度器又将其调度出来重新执行。