多线程好处和应用场景
多线程的相关概念和术语
Java线程创建方式
Thread类详解,线程的常用方法
线程5种状态和6种状态,两种版本解释
线程状态之间转换
Java设计者写过一个很有影响力的白皮书,用来解释设计的初衷,并发布了一个简短的摘要,分为11个术语:
- 简单性
- 面向对象
- 分布式
- 健壮性
- 安全性
- 体系结构中立
- 可移植性
- 解释型
- 高性能
- 多线程
- 动态性
其中多线程就是本次要接触的,白皮书中对多线程的解释:
多线程可以带来更好的交互响应和实时行为。
如今,我们非常关注并发性,因为摩尔定律行将完结。我们不再追求更快的处理器,而是着眼于获得更多的处理器,而且要让它们一直保持工作。不过,可以看到,大多数编程语言对于这个问题并没有显示出足够的重视。Java在当时很超前。它是第一个支持并发程序设计的主流语言。从白皮书中可以看到,它的出发点稍有些不同。当时,多核处理器还很神秘,而Web编程才刚刚起步,处理器要花很长时间等待服务器响应,需要并发程序设计来确保用户界面不会"冻住"。并发程序设计绝非易事,不过Java在这方面表现很出色,可以很好地管理这个工作。
在操作系统中有多任务【multitasking】,在同一刻运行多个程序【应用】的能力。例如,在听音乐的同时可以边打游戏,边写代码。如今我们的电脑大多都是多核CPU,但是,并发执行的进程【正在执行的应用】数目并不是由CPU数目制约的。操作系统将CPU的时间片分配给每一个进程,给人并行处理的感觉。
相关概念
程序【program】:为了完成特定任务,用某种语言编写的一组指令的集合。程序就是一堆代码,一组数据和指令集,是一个静态的概念。就说我们程序员写的那玩意。比如:安装在电脑或者手机上的各种软件,今日头条、抖音、懂车帝等,如果一个程序支持多线程,这个程序就是一个多线程程序
进程【Process】:是程序的一次执行过程或者说是正在运行的程序,是一个动态概念,进程存在生命周期,也就是说程序随着程序的终止而销毁
线程【Thread】:线程是进程中的实际运作的单位,是进程的一条流水线,是程序的实际执行者,是最小的执行单位。通常在一个进程中可以包含若干个线程,当然一个进程中至少有一个线程。线程是CPU调度和执行的最小单位
CPU时间片:时间片即CPU分配给各个程序的时间,每个进程被分配一个时间段,称作它的时间片,即该进程允许运行的时间,使各个程序从表面上看是同时进行的,如果在时间片结束时进程还在运行,则CPU将被剥夺并分配给另一个进程。如果进程在时间片结束前阻塞或结束,则CPU当即进行切换。而不会造成CPU资源浪费
并行【parallel】:多个任务同时进行,并行必须有多核才能实现,否则只能是并发,比如:多名学生有问题,同时有多名老师可以辅导解决
串行【serial】:一个程序处理完当前进程,按照顺序接着处理下一个进程,一个接着一个进行,比如:多名学生有问题,只有一名老师,需要挨个解决
并发【concurrency】:同一个对象被多个线程同时操作。(这是一种假并行。即一个CPU的情况下,在同一个时间点,CPU只能执行一个代码,因为切换的很快,所以就有同时执行的错觉),比如:多名学生有问题,只有一个老师,他一会处理A同学,一会处理B同学,一会处理C同学,频繁切换,看起来好似在同时处理学生问题
多线程意义
实际应用中,多线程非常有用,例如,QQ音乐就是一个多线程程序,我们可以一边听音乐,一般下载音乐,还可以同时播放MV等非常方便。一个Web服务器通过多线程同时处理多个请求,比如Tomcat就是多线程的。
注意:程序会因为引入多线程而变的复杂,多线程同时会带来一些问题,需要我们解决
多线程应用场景
- 程序需要同时执行两个或多个任务。
- 程序需要实现一些需要等待的任务时,如用户输入、文件读写操作、网络操作、搜索等。
- 需要一些后台运行的程序时。
多线程多数在浏览器、Web服务器、数据库、各种专用服务器【如游戏服务器】、分布式计算等场景出现。
在使用Java编写后台服务时,如果遇到并发较高、需要后台任务、需要长时间处理大数据等情况都可以创建线程单独的线程处理这些事项,多线程的目的就在于提高处理速度,减少用户等待时间
- 后台记录日志,创建额外线程来记录日志
- 大量用户请求,创建多个线程共同处理请求
- 下载大文件,可以创建单独的线程,不影响正常的业务流畅度
- ......
多线程创建方式
线程创建有4种方式:
方式1:继承Thread类
方式2:实现Runnable接口
方式3:实现Callable接口
方式4:使用线程池【这块后边单独说,它更像是管理线程的手段】
继承Thread
步骤:
- 自定义类继承Thread类
- 重写run方法,run方法就是线程的功能执行体
- 创建线程对象,调用start方法启动线程
- 启动线程之后,线程不一定会立即执行,需要得到CPU分配的时间片,也就是拿到CPU执行权限才会执行
JDK源码中,Thread类定义实现了Runnable接口
所以知道重写的run方法从哪来的了吧!就是从Runnable接口中来的
需求:创建线程计算10以内的偶数
线程类:
测试类:
测试类中输出了一句话:主线程
打印结果:
实现Runnable接口
步骤:
- 自定义类实现Runnable接口
- 实现run方法
- 创建实现类对象
- 创建Thread对象,在构造方法中传入实现类对象作为参数
- 调用Thread对象的start方法启动线程
同样的需求打印10以内的偶数
实现类:
测试类:
Callable接口
FutureTask类:
RunnableFuture接口:
步骤:
- 新建类实现Callable接口,并指定泛型类型,类型就是线程计算之后的结果的类型
- 实现call方法,call方法跟run方法类似,不过该方法有返回值和异常抛出,都是来封装线程功能体的
- 在测试类中创建实现类对象,并且创建 FutureTask 对象将实现类对象当做构造方法参数传入
- 创建Thread线程对象,将 FutureTask 对象当做构造方法参数传入,并调用start方法启动线程
- 可以通过 FutureTask 对象的get方法获取线程的运算结果
案例:还是计算10以内的偶数,这一次将计算结果返回,因为有多个数据所以返回数据用集合存储,则Callable接口的泛型类型应该是集合
实现类:
测试类:
三种实现方式区别
- Java单继承的特性决定,使用实现接口的方式创建线程更灵活
- Callable实现call方法有返回值和异常抛出,方便定位问题,配合FutureTask可以获取线程运算结果,而run方法没有返回值,没有异常抛出
- 如果线程运行结束后不需要返回值,则推荐使用实现Runnable接口方式
小贴士:有不少文章中写到【实现的方式更适合用来处理多个线程有共享数据的情况】,很多小伙伴也拿去背,这句话怎么看都不对吧,多线程共享数据不加锁,不同步怎么着也不能避免线程安全问题!
线程开辟
- 开辟线程需要通过Thread类创建对象
- 启动线程需要Thread对象调用start方法
- 线程的功能可以装在Thread类的run方法或者Runnable接口实现类的run方法类中
- 线程开辟需要在另一个线程中开启,新开辟的线程称为子线程
小贴士:对于Java应用程序java.exe来讲,至少会存在三个线程:
main主线程
gc垃圾回收线程
异常处理线程,如果发生异常会影响主线程。
线程状态
线程的状态网上有 5种状态 和 6种状态 两个版本
五种状态版本:是基于现代操作系统线程状态角度解释的
新建:当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态
就绪:处于新建状态的线程被start后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没分配到CPU资源
运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态,run方法定义了线程的操作和功能
阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出CPU并临时终止自己的执行,进入阻塞状态
死亡:线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束
在JDK5的时候Thread类中定义了一个State枚举类,其中定义了6种线程状态,这是Java官方定义的Java线程的6种状态
1)NEW:处于NEW状态的线程此时尚未启动。只是通过new Thread()创建了线程对象,并未调用start()方法
2)RUNNABLE:Java线程的 RUNNABLE 状态其实是包括了传统操作系统线程的 就绪(ready) 和 运行(running) 两个状态的。处于 RUNNABLE 状态的线程可能在 Java 虚拟机中运行,也有可能在等待 CPU 分配资源
3)BLOCKED:阻塞状态。处于 BLOCKED 状态的线程正等待锁的释放以进入同步区,就好比你去食堂打饭,只有一个窗口你就得排队,等前边的人结束之后你完成打饭
4)WAITING :等待状态。处于等待状态的线程变成 RUNNABLE 状态需要其他线程唤醒
可以通过调用一下三个方法进入等待状态:
- Object.wait():使当前线程处于等待状态直到另一个线程唤醒它;
- Thread.join():使当前线程等待另一个线程执行完毕之后再继续执行,底层调用的是 Object 实例的 wait() 方法;
- LockSupport.park():除非获得调用许可,否则禁用当前线程进行线程调度
5)TIMED_WAITING:超时等待状态。线程等待一个具体的时间,时间到后会被自动唤醒。
调用如下方法会使线程进入超时等待状态:
- Thread.sleep(long millis):使当前线程睡眠指定时间,sleep() 方法不会释放当前锁,但会让出 CPU,所以其他不需要争夺锁的线程可以获取 CPU 执行;
- Object.wait(long timeout):线程休眠指定时间,等待期间可以通过 notify() / notifyAll() 唤醒;
- Thread.join(long millis):等待当前线程最多执行 millis 毫秒,如果 millis 为 0,则会一直执行;
- LockSupport.parkNanos(long nanos): 除非获得调用许可,否则禁用当前线程进行线程调度指定时间;
- LockSupport.parkUntil(long deadline):同上,也是禁止线程进行调度指定时间;
6)TERMINATED:终止状态。此时线程已执行完毕。
其实等待和锁定状态可以被笼统的称为阻塞状态,就是停着不动了嘛,在回答面试题时建议回答6种状态版本,就是是JDK源码中定义的,一来有官方支持,二来证明咱看过一点源码。
状态转换
- 新建状态的线程调用start方法进入到运行状态
- 运行状态线程如果遇到Object.wait()、Thread.join()或者LockSupport.park()方法则会放弃CPU执行权进入等待状态,这个装需要被唤醒之后才会再次进入就绪状态获得到CPU时间片进入运行状态
- 运行状态的线程遇到Thread.sleep(long)、Object.wait(long)、Thread.join(long)等方法,也就是可以传入时间的,就会进入超时等待状态,达到时间之后就会自动进入就绪状态,当CPU执行就进入运行状态
- 运行状态的线程如果被同步代码块或者同步方法包裹,执行时如果释放锁资源,就会进入阻塞状态或者叫锁定状态,只有再次获取到锁资源时才会进入就绪状态,等到CPU时间片后进入运行状态
- 执行完的线程就会进入终止状态,线程结束
线程之间的状态转换可以参考下图
Thread类详解
成员变量
变量名 | 类型 | 作用 |
name | volatile String | 线程名称 |
priority | int | 线程的优先级,默认为5,范围1-10 |
threadQ | Thread | |
eetop | long | |
single_step | boolean | 是否单步执行 |
daemon | boolean | 守护线程状态,默认为false |
stillborn | boolean | JVM状态,默认为false |
target | target | 将被执行的Runnable实现类 |
group | ThreadGroup | 当前线程的线程组 |
contextClassLoader | ClassLoader | 这个线程上下文的类加载器 |
inheritedAccessControlContext | AccessControlContext | 该线程继承的AccessControlContext |
threadInitNumber | static int | 用于匿名线程的自动编号 |
threadLocals | ThreadLocal.ThreadLocalMap | 属于此线程的ThreadLocal,这个映射关系通过ThreadLocal维持 |
inheritableThreadLocals | ThreadLocal.ThreadLocalMap | 这个线程的InheritableThreadLocal,其映射关系通过InheritableThreadLocal维持 |
stackSize | long | 此线程的请求的堆栈的大小,如果创建者的请求堆栈大小为0,则不指定堆栈大小,由jvm来自行决定。一些jvm会忽略这个参数。 |
nativeParkEventPointer | long | 在本机线程终止后持续存在的jvm私有状态。 |
tid | long | 线程的ID |
threadSeqNumber | static long | 用于生成线程的ID |
threadStatus | volatile int | java线程状态,0表示未启动 |
parkBlocker | volatile Object | 提供给LockSupport调用的参数 |
blocker | volatile Interruptible | 此线程在可中断的IO操作中被阻塞的对象,阻塞程序的中断方法应该在设置了这个线程中断状态之后被调用 |
常量
常量名 | 数据类型 | 作用 |
MIN_PRIORITY | int | 线程最低优先级 |
NORM_PRIORITY | int | 分配给线程的默认优先级 |
MAX_PRIORITY | int | 线程最大优先级 |
Thread构造方法
从源码看出Thread类一共有9个构造方法,除第三个为default修饰【同包可用】,其他都是public
构造方法 | 作用 |
Thread() | 分配新的Thread对象 |
Thread(Runnable target) | 传入Runnable接口实现类,之后由JVM启动线程 |
Thread(Runnable target, AccessControlContext acc) | 在传入Runnable的时候还可以指定AccessControlContext |
Thread(ThreadGroup group, Runnable target) | 指定线程组和Runnable接口 |
Thread(String name) | 指定线程名字,默认是【Thread-下一个线程编号,从0开始】 |
Thread(ThreadGroup group, String name) | 指定线程组和线程名字 |
Thread(Runnable target, String name) | 指定Runnable接口和线程名 |
Thread(ThreadGroup group, Runnable target, String name) | 指定线程组,Runnable接口和线程名 |
Thread(ThreadGroup group, Runnable target, String name,long stackSize) | 指定线程组,Runnable接口,线程名和此线程请求的堆栈大小,默认为0 |
Thread常用方法
方法 | 返回值类型 | 作用 |
start() | void | 启动线程 |
run() | void | 重写的Runnable接口方法,封装线程的功能体 |
currentThread() | Thread | 静态方法,获取当前线程 |
getName() | String | 获取线程名 |
setName(String name) | void | 设置线程名 |
yield() | void | 主动释放当前线程的执行权 |
join() | void | 在线程中插入执行另一个线程,该线程被阻塞,直到插入执行的线程完全执行完毕以后,该线程才继续执行下去 |
sleep(long millis) | void | 线程休眠一段时间 |
isAlive() | boolean | 判断线程是否还存活 |
isDaemon() | boolean | 判断是否为守护线程 |
stop() | void | 过时方法。当执行此方法时,强制结束当前线程,因过于粗暴,会引发很多问题所以弃用 |
setDaemon(boolean on) | void | 设置为守护线程 |
getPriority() | int | 获取线程优先级 |
setPriority(int newPriority) | void | 设置线程优先级 |
设置线程名
实现类:
测试类:
运行结果:
或者通过setName()方法设置线程名
如果不设置线程名,默认为【"Thread-" + nextThreadNum()】,nextThreadNum方法使用 threadInitNumber静态变量,默认从0开始,每次+1
不设置线程名运行效果如下
sleep方法
sleep方法可以让线程阻塞指定的毫秒数。时间到了后,线程进入就绪状态。sleep可用来研模拟网络延时,倒计时等。每一个对象都有一个锁,sleep不会释放锁,锁的概念后边会详细讲解
实现类:
测试类:
运行结果:
"善用"sleep年入百万不是梦:
yield方法
提出申请释放CPU资源,至于能否成功释放取决于JVM决定,调用yield()方法后,线程仍然处于RUNNABLE状态,线程不会进入阻塞状态,保留了随时被调用的权利
实现类:
测试类:
运行结果:
第五次执行是线程2执行开始结束后输出的线程1开始结束,这就说明CPU并没有切换到别的线程,说明并没有释放CPU资源
join方法
将当前的线程挂起,当前线程阻塞,待其他的线程执行完毕,当前线程才能执行,可以把join()方法理解为插队,谁插到前面,谁先执行
在很多情况下,主线程创建并启动子线程,如果子线程中要进行大量的耗时运算,主线程将可能早于子线程结束。如果主线程需要知道子线程的执行结果时,就需要等待子线程执行结束了。主线程可以sleep(xx),但这样的xx时间不好确定,因为子线程的执行时间不确定,join()方法比较合适这个场景
运行结果:
设置优先级
改变、获取线程的优先级。Java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器按照优先级决定应该调度哪个线程来执行。线程的优先级用数据表示,范围1~10。线程的优先级高只是表示他的权重大,获取CPU执行权的几率大。先设置线程的优先级,在执行start()方法
结束线程
JDK提供的【stop()、destroy()】两种方法已废弃,不推荐再使用。推荐线程自动停止下来,就比如上边的所有案例,都是执行完了run方法中的所有代码之后线程就自然结束了。如果线程需要循环执行,建议使用一个标识位变量进行终止,当flag=false时,则终止线程运行
比如:定义一个名为【线程1】的子线程,当主线程执行3次循环之后,线程1停止运行
实现类:
测试类:
总结
- 掌握多线程的使用场景和术语
- 熟练创建和启动线程
- 掌握线程状态和状态之间转换
- 掌握Thread类中的常用方法如:join、sleep、yield等
文章出自:石添的编程哲学,如有转载本文请联系【石添的编程哲学】今日头条号。