引言
【因内容较多,将拆分为两篇博文,此为篇一】
本篇博文为 Java 的一些高级特性的常见概念及相关细节梳理,意在重学 Java 查漏补缺。
博文随时会进行更新,补充新的内容并修正错漏,该系列博文旨在帮助自己巩固扎实 Java 基础。
毕竟万丈高楼,基础为重,借此督促自己时常温习回顾。
一、多线程
1.1、概念
1.1.2、程序、进程、线程
程序(Program): 为完成特定任务,使用某种编程语言编写的一组指令的集合。即指一段静态的代码,静态对象
进程(Process): 指程序的一次执行过程,或是正在运行的一个程序。是一个动态的过程:有它自身的产生、存在和消亡的过程 —— 生命周期
- 如:运行中的 Edge 浏览器、Chrome 浏览器
- 程序是静态的,进程是动态的
- 进程作为资源分配的单位,系统在运行时会为每个进程分配不同的内存区域
线程(Thread): 进程可进一步细化为线程,是一个程序内部的一条执行路径
- 若一个进程同一时间并行执行多个线程,就是支持多线程的
- 线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器(pc),线程切换的开销小
- 一个进程中的多个线程共享相同的内存单元/内存地址空间(它们从同一堆中分配对象,可以访问相同的变量和对象,使得线程间通信更便捷、高效。但多个线程操作共享的系统资源可能会带来安全隐患)
1.1.3、单核 CPU、多核 CPU
单核 CPU 中其实是一种假的多线程,因为在一个时间单元内,只能执行一个线程的任务
- 多个任务只能由一个 CPU 进行处理,将正在执行中的任务"挂起"之后再去执行其他的任务,如此轮转。因为 CPU 时间单元特别短,因此感觉不出任务的切换
- 多核 CPU 才能更好的发挥多线程的效率
- 一个 Java 应用程序其实至少有三个线程:main() 主线程,gc() 垃圾回收线程,异常处理线程(如果发生异常,会影响主线程)
1.1.4、并行与并发
- 并行: 多个 CPU 同时执行多个任务(多个人同时做不同的事)
- 并发: 一个 CPU(采用时间片轮转)同时执行多个任务(多个人做同一件事)
1.1.5、多线程的优点与使用场景
优点:
- 提高应用程序的响应
- 提高计算机系统 CPU 的利用率
- 改善程序结构。将既长又复杂的进程分为多个线程,独立运行,利于理解和修改
使用场景:
- 程序需要同时执行两个或多个任务
- 程序需要实现一些需要等待的任务时
- 需要一些后台运行的程序时
1.1.6、线程的分类
Java 中的线程分为两类:一种是守护线程,一种是用户线程
- 它们几乎每方面都是相同的,唯一的区别是判断 JVM 何时离开
- 守护线程是用来服务用户线程的,通过在 start() 方法前调用 thread.setDaemon(true) 可以把一个用户线程变成一个守护线程
- Java 垃圾回收就是一个典型的守护线程
- 若 JVM 中都是守护线程,当前 JVM 将退出
1.2、线程的创建和使用
多线程的创建
1.2.1、创建线程方式一:继承于 Thread 类
- 创建一个继承于 Thread 类的子类
- 重写 Thread 类的 run() 方法
- 创建 Thread 类的子类的对象
- 通过此子类对象调用 start() 方法:
- 启动当前线程
- 调用当前线程的 run() 方法
注意:
- 不能通过直接调用 run() 的方式启动线程
- 不可以让已经 threadSubObj.start() 的线程再去执行启动线程( threadSubObj.start() )的操作。会报错:IllegalThreadStateException
1.2.2、创建线程方式二:实现 Runnable 接口
- 创建一个实现了 Runnable 接口的类
- 实现类去实现 Runnable 中的抽象方法:run() 方法
- 创建实现类的对象
- 将此对象作为参数传递到 Thread 类的构造器中,创建 Thread 类的对象
- 通过这个 Thread 类的对象调用 start() 方法:
- 启动当前线程
- 调用当前线程的 run() 方法(调用了 Runnable 类型的 target 的 run() 方法)
创建线程的两种方式的比较:
- 开发中:优先选择实现 Runnable 接口的方式
- 实现的方式没有类的单继承的局限性
- 实现的方式更适合用来处理多个线程有共享数据的情况
- 两种实现方式的联系:( Thread 类也是 Runnable 的实现类 ) public class Thread implements Runnable
- 相同点:两种方式都需要重写 run() 方法,将线程要执行的逻辑声明在 run() 方法中
1.2.3、创建线程方式三:实现 Callable 接口 (JDK5.0新增)
import java.util.concurrent.Callable;
- 创建一个实现 Callable 接口的实现类
- 实现 call() 方法,将此线程需要执行的操作声明在 call() 方法中
- 创建 Callable 接口实现类的对象
- 将此 Callable 接口实现类的对象作为参数传递到 FutureTask 构造器中,创建 FutureTask 的对象
- 将 FutureTask 的对象作为参数传递到 Thread 类的构造器中,创建 Thread 对象,并调用 start() 方法
- (可选)获取 Callable 中 call() 方法的返回值
get() 返回值即为 FutureTask 构造器参数 —— Callable 实现类重写的 call() 的返回值
与使用 Runnable 相比,Callable 功能更强,针对 Callable:
- 相比 run() 方法,call() 方法可以有返回值
- call() 方法可以抛出异常,被外面的操作捕获,获取异常的信息
- Callable 支持泛型的返回值
- 需要借助 FutureTask 类,比如获取返回结构
Future 接口:
- 可以对具体 Runnable、Callable 任务的执行结果进行取消、查询是否完成、获取结果等
- FutureTask 是 Future 接口的唯一的实现类
- FutureTask 同时实现了 Runnable,Future 接口。它既可以作为 Runnable 被线程执行,又可以作为 Future 得到 Callable 的返回值
1.2.4、创建线程方式四:使用线程池 (JDK5.0新增)
开发中线程是经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。若提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。
JDK5.0起提供了线程池相关 API:ExecutorService 和 Executors
- ExecutorService:真正的线程池接口。常见子类 ThreadPoolExecutor
- void execute(Runnable command):执行任务/命令,没有返回值,一般用来执行 Runnable
- <T> Future<T> submit(Callable<T> task):执行任务,有返回值,一般用来执行 Callable
- void shutdown():关闭连接池
- Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池
- Executors.newCachedThreadPool():创建一个可根据需要创建新线程的线程池
- Executors.newFixedThreadPool(n):创建一个可重用的固定线程数的线程池
- Executors.newSingleThreadExecutor():创建一个只有一个线程的线程池
- Executors.newScheduledThreadPool(n):创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行
创建及使用:
-
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
-
提供指定线程数量的线程池(ExecutorService service = Executors.newFixedThreadPool(nThreads: 10); )
-
执行指定的线程的操作。需要提供实现 Runnable 接口或 Callable 接口实现类的对象
- service.executer(Runnable runnable); 适用于 Runnable
- service.submit(Callable callable); 适用于 Callable
-
关闭连接池(service.shutdown(); )
线程池的优点:
- 提高响应速度(减少了创建新线程的时间)
- 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
- 便于线程管理
- corePoolSize:核心池的大小
- maximumPoolSize:最大线程数
- keepAliveTime:线程没有任务时最多保持多长时间后会终止
1.2.5、Thread 类的常见方法
- void start():启动线程,并执行对象的 run() 方法
- run():线程在被调度时执行的操作
- 通常需要重写 Thread 类中的此方法,将创建的线程要执行的操作声明在此方法中
- static Thread currentThread():返回当前线程。在 Thread 子类中就是 this,通常用于主线程和 Runnable 实现类
- 静态方法,返回执行当前代码的线程
- String getName():返回该线程的名称
- 获取当前线程的名称
- void setName(String name):设置该线程名称
- 设置当前线程的名称
- static void yield():线程让步
- 释放当前 CPU 的执行权
- 暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程
- 若队列中没有同优先级的线程,忽略此方法
- join():当某个程序执行流中调用其他线程的 join() 方法时,调用线程将被阻塞,直到 join() 方法加入的 join 线程执行完为止
- 在线程 thread1 中调用线程 thread2 的 join(),此时线程 thread1 就进入阻塞状态,直到线程 thread2 完全执行完成后,线程 thread1 才结束阻塞状态
- 低优先级的线程也可以获得执行
- static void sleep(long millis):(指定时间:毫秒)
- 让当前线程"睡眠"指定的毫秒。在指定的毫秒时间内,当前线程是阻塞状态
- 令当前活动线程在指定时间段内放弃对 CPU 控制,使其他线程有机会被执行,时间到后重新排队
- 抛出 InterruptedException 异常(需要使用 try-catch)
- stop():强制线程生命期结束,不推荐使用(已过时)
- 当执行此方法时,强制结束当前线程
- boolean isAlive():判断线程是否还活着
1.2.6、线程的调度
调度策略:
- 时间片
- 抢占式:高优先级的线程抢占 CPU
Java 的调度方法:
- 同优先级线程组成先进先出队列(先到先服务),使用时间片策略
- 对高优先级,使用优先调度的抢占式策略
线程的优先级:
- 现成的优先级等级:
- MAX_PRIORITY:10
- MIN_PRIORITY:1
- NORM_PRIORITY:5 (默认优先级)
- 涉及的方法:
- getPriority():返回线程优先值
- setPriority(int newPriority):改变线程的优先级
- 说明:
- 线程创建时继承父线程的优先级
- 低优先级只是获得调度的概率低,并非一定是在高优先级线程之后才被调用
- 高优先级的线程要抢占低优先级线程 CPU 的执行权。但是只是从概率上讲,高优先级的线程高概率的情况下被执行。并不意味着只有当高优先级的线程执行完以后低优先级的线程才执行
1.3、线程的生命周期
JDK 中用 Thread.State 类定义了线程的几种状态
要想实现多线程,必须在主线程中创建新的线程对象。Java 语言使用 Thread 类及其子类的对象来表示线程,在它的一个完整的生命周期中通常要经历如下的五种状态:
- 新建(new):当