java的多线程
java的多线程的概念,向来都是很复杂、笼统、抽象的。现实世界只有将知识点抽象过后才能有效的传播,但是传播的过程中,只有将抽象的知识点具象化,我们才能习得。所以我们会将个别内容点进行一个具象化进而解剖。当我们理解完了之后最终将其抽象成一个个名词:多线程、资源、锁等。
本文仅从以下的范围内容来谈谈java的多线程。
- 何为线程,线程的作用
- 资源的控制,锁的介绍
- 线程池的作用
- 多线程的常用工具和方法
1.何为线程
1.1.线程的定义
官方解释:线程是一个单一的顺序控制流程。
线程分主线程、子线程。由主线程来创建子线程来执行各种任务。
举例说明:以动漫“火影忍者”举例说明,主线程就好比每一个忍者,他们构成了最基本的忍者世界,一个忍者可以按照任务的缓急、难易程度同时执行多个任务。同时忍者也能分身(调用自身查克拉)来分担自身的任务,这就好比,忍者世界观中的忍者主体,基本等同于程序中的主线程。主线程没了,分身则主观上不可控,就消失了。
由上图可见,主线程与子线程的关系和忍者与分身的是很相似,也就是说,主线程能做的事,我们都能让子线程帮我们做。忍者自己能做的事也能去靠分身去做。接下来,我们来看两段代码。
//代码一
public static void main(String[] args) {
System.out.println("Hello World!");
}
//代码二
public static void main(String[] args) {
Thread thread = new Thread(()->{
System.out.println("Hello World!");
});
thread.start();
}
代码一和代码二最终的结果都是只做了一件事,向控制台输出“Hello World!”。但是代码一是由主线程去做的;代码二是由主线程创建的子线程去做的。这里我们可以看出,主线程和子线程的本质上区分并不大,因为它们都执行相同的逻辑,这一点上并没有进行区分。
1.2.多线程的作用
程序如果都是按照单个线程的话,那么所有任务的执行均是按照顺序来进行(串行执行)。
而多线程的作用是可以安排不同的线程执行不同的任务。
上图是一个理论值,我们在某些任务密集的场景下,多线程的执行效率多数情况下可能高于单线程的执行。
为什么说是可能,因为这里创建子线程是会消耗性能的,也带有时间消耗,如果设置不合理,单单创建子线程的时间成本就远大于执行任务的时间成本,这一点要结合实际场景进行考虑。
举例说明:火影里的忍者也不会接到一批任务就马上分身去一个个的做,他们也得结合任务的实际情况来考虑使用分身。
1.3.关于线程的小结
- 线程可以执行正确的逻辑代码
- 线程的创建也伴随着性能消耗,并不是无消耗
2.资源的控制
java中的资源可以理解为,一个实例或基础数据类型的变量的任何操作。实例或变量在这里不能完全算做资源,因为根据面向对象编程中的封装性,代码中直接将实例暴露出来给非本类的实例进行操作是一个大忌。
这里我们从以下资源和线程的关系进行解剖。
- 一个线程可以执行多个资源(串行)
- 多个线程可以执行多个资源(并行)
- 单个资源可以被一个线程执行(串线)
- 单个资源可以被多个线程执行(并行)
从这里看,1和3没什么问题。因为这种机制下我们确保了一个资源被一个线程执行(等同于一个任务被一个忍者(本体或分身均可)执行)。但是2和4就出现了一个现象,同一个资源被多个线程所操作,如果不加以控制,则会出现指定之外的执行结果或者直接产生死锁。
举例说明:两个忍者都执行了同一个任务,去杀死邻国的头目,我们假设忍者A过去杀死了头目,忍者B后去的,发现头目死了,那他接下来怎么办?算任务失败还是算完成了?
当然忍者B最后肯定还是回去复命了,也算他任务成功,这是任务本身的规则和秩序所决定的,但是程序的世界是无秩序,需要程序员通过代码去打造这个无序的世界从而形成秩序。
资源自身一定要包含约束性和规则性才能被正确的使用。
2.1.常见的控制方式
java本身提供了资源被多个线程调度的控制方式。
- synchronized关键字
- Atomic包
- ReentrantLock
- Semaphore
- CountDownLatch
- CyclicBarrier
- Phaser
我们通过一张图表来概况了解一下。
对于资源的控制的方式无非就是一个“锁” 字。现实当中到处充斥着这样的例子,例如一个城市的市长,按照规定只能有一个,谁上任,那么市长这个资源位就被谁“锁”住了。但是程序世界中的“锁”和现实世界中的“锁”差别很大。
- 现实世界的锁,是可见,它控制着某一样可见的物体,比如:门、箱子等,而再由这些具有隔绝性质的物体去控制级别更高的资源,例如:门里的东西、箱子里的钱。也就是说现实世界的锁是间接的控制资源。
- 程序世界里的锁,则是一种更为高级的抽象,它包含的对资源的各种维度的控制,我们可以将其理解为“规则”,比如:某类资源在同一时刻只能有一个线程进行操作(同步性)、某类资源必须由多个线程同时操作(同步协作)、某类资源最多只能有N个线程进行操作(资源调度许可证)。这些都是“规则”的运用。
2.2.锁的种类
java中有关于锁的内容非常多,我们这里先用一张图来简要介绍一下,以后再着重篇幅去介绍每个锁的相关特性。
2.3.关于资源的小结
- 任何实例对外提供的方法(尽量避免对变量的直接操作)
- 我们需要在对外提供的方法内用“锁”去控制方法内的被调度的规则。
- 实行第二条之前一定要确定当前的编程环境,是单线程的还是多线程
3.线程池
用一段话形容线程和资源的关系那就是。某个人(线程)去做(调度)某件有要求和规则(锁策略)的事(资源);根据这件事(资源)的要求和规则(锁策略)去约束做这个事人(线程)的做法。
我们用各种锁策略去保证资源能被正确的使用。这里我们还缺一个角度,那就是从线程的角度去调度资源。
我们用一个问题开头来展开对话。
- 问:我们能根据资源的数量去创建线程的数量吗?
- 答:不能,因为创建线程的开销大,受机器的配置的限制。
- 问:那么能不能创建一定数量的线程,去循环的调度资源。
- 答:这么做是可以的,但是资源数一般来说肯定是多于线程数,我们要控制资源的调度顺序,还未来得及调度的资源可以按先来后到原则存放到队列。
- 问:那资源数少于线程数时候,该怎么样去处理。
- 答:我们可以保留一定量的线程,为未来可能调度的资源做预备。
这就是一个线程池的雏形,线程池的雏形具有以下的基本特性
- 具有最大的线程数限制
- 有若干常备线程(核心线程)
- 资源数若多超过了最大线程数的限制则会放入队列中。
我们来解刨线程池的最全的配置信息:
- corePoolSize:核心线程数
- maximumPoolSize:最大的线程数
- keepAliveTime:无资源调度的线程回收的时间(默认单位:毫秒)
- TimeUnit:时间单位
- BlockingQueue:多余的资源放入的队列
- ThreadFactory:线程的工厂类
- RejectedExecutionHandler:线程池调度资源的策略
按照我们常规的设置
BlockingQueue > maximumPoolSize ≥ corePoolSize
- corePoolSize会随着资源调度数增加至maximumPoolSize
- 当线程空闲时,会根据keepAliveTime来回收线程数(maximumPoolSize-corePoolSize)
- BlockingQueue分无界Queue和有界Queue。
-
当资源调度大于maximumPoolSize时会放入BlockingQueue中
- 当BlockingQueue是有界队列则存入
- 当BlockingQueue是无界队列则根据策略调整
-
当资源调度数大于BlockingQueue的长度,则根据RejectedExecutionHandler的策略来调整资源调度情况。
- AbortPolicy:默认策略,舍弃最新的资源调度,并抛出异常
- DiscardPolicy:舍弃最新的资源调度,不会有异常
- DiscardOldestPolicy:舍弃在队列中队头的资源
- CallerRunsPolicy:交由主线程去执行(慎用)
- 自定义拒绝策略:实现RejectedExecutionHandler接口,编写特殊业务的拒绝策略。
总结:线程池就是多个线程来调度多个资源时所优化的一种多维度的策略,它的核心就是线程的复用以及资源的缓冲存储。
常用的方法和工具
类或关键字 | 简要介绍 | 使用的对象 |
---|---|---|
synchronized | 作用于方法或代码块中实现线程的同步性 | 资源 |
Atomic包 | 原子性控制变量或实例 | 资源 |
ReentrantLock | 作用于方法中,实现线程的同步性 | 资源 |
Semaphore | 作用于方法中,限制方法的线程最大调度数 | 资源 |
Phaser | 作用于方法中,设置线程必须调度的数量 | 资源 |
Object.wait() | 作用于同步的方法中,使当前调度的进程等待 | 资源 |
Object.notifyAll()或notify() | 作用于同步的方法中,唤起当前处于等待状态的调度线程 | 资源 |
Runable | 线程中调度无返回结果的资源 | 线程 |
Callable | 线程中调度有返回结果的资源 | 线程 |
Future | 线程执行有返回结果的资源的接受方 | 线程 |
Executor | 创建线程池的工厂类(慎用) | 线程 |
ThreadPoolExecutor | 线程池的实现类 | 线程 |
ExecutorService | 线程池的基础类 | 线程 |
CompletionService | 异步处理带返回结果的线程池 | 线程 |
ScheduledExecutorService | 处理定时任务的线程池 | 线程 |
Fork-Join | 分治思想处理批量任务 | 线程 |