为了账号安全,请及时绑定邮箱和手机立即绑定

java多线程的入门小记

标签:
Java
java的多线程

java的多线程的概念,向来都是很复杂、笼统、抽象的。现实世界只有将知识点抽象过后才能有效的传播,但是传播的过程中,只有将抽象的知识点具象化,我们才能习得。所以我们会将个别内容点进行一个具象化进而解剖。当我们理解完了之后最终将其抽象成一个个名词:多线程、资源、锁等。

本文仅从以下的范围内容来谈谈java的多线程。

  1. 何为线程,线程的作用
  2. 资源的控制,锁的介绍
  3. 线程池的作用
  4. 多线程的常用工具和方法
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.关于线程的小结

  1. 线程可以执行正确的逻辑代码
  2. 线程的创建也伴随着性能消耗,并不是无消耗
2.资源的控制

java中的资源可以理解为,一个实例或基础数据类型的变量的任何操作。实例或变量在这里不能完全算做资源,因为根据面向对象编程中的封装性,代码中直接将实例暴露出来给非本类的实例进行操作是一个大忌。

图片描述

这里我们从以下资源和线程的关系进行解剖。

  1. 一个线程可以执行多个资源(串行)
  2. 多个线程可以执行多个资源(并行)
  3. 单个资源可以被一个线程执行(串线)
  4. 单个资源可以被多个线程执行(并行)

从这里看,1和3没什么问题。因为这种机制下我们确保了一个资源被一个线程执行(等同于一个任务被一个忍者(本体或分身均可)执行)。但是2和4就出现了一个现象,同一个资源被多个线程所操作,如果不加以控制,则会出现指定之外的执行结果或者直接产生死锁。

举例说明:两个忍者都执行了同一个任务,去杀死邻国的头目,我们假设忍者A过去杀死了头目,忍者B后去的,发现头目死了,那他接下来怎么办?算任务失败还是算完成了?

当然忍者B最后肯定还是回去复命了,也算他任务成功,这是任务本身的规则和秩序所决定的,但是程序的世界是无秩序,需要程序员通过代码去打造这个无序的世界从而形成秩序

资源自身一定要包含约束性和规则性才能被正确的使用。

2.1.常见的控制方式

java本身提供了资源被多个线程调度的控制方式。

  • synchronized关键字
  • Atomic包
  • ReentrantLock
  • Semaphore
  • CountDownLatch
  • CyclicBarrier
  • Phaser

我们通过一张图表来概况了解一下。

图片描述

对于资源的控制的方式无非就是一个“” 字。现实当中到处充斥着这样的例子,例如一个城市的市长,按照规定只能有一个,谁上任,那么市长这个资源位就被谁“锁”住了。但是程序世界中的“锁”和现实世界中的“锁”差别很大。

  • 现实世界的锁,是可见,它控制着某一样可见的物体,比如:门、箱子等,而再由这些具有隔绝性质的物体去控制级别更高的资源,例如:门里的东西、箱子里的钱。也就是说现实世界的锁是间接的控制资源。
  • 程序世界里的锁,则是一种更为高级的抽象,它包含的对资源的各种维度的控制,我们可以将其理解为“规则”,比如:某类资源在同一时刻只能有一个线程进行操作(同步性)、某类资源必须由多个线程同时操作(同步协作)、某类资源最多只能有N个线程进行操作(资源调度许可证)。这些都是“规则”的运用。

2.2.锁的种类

java中有关于锁的内容非常多,我们这里先用一张图来简要介绍一下,以后再着重篇幅去介绍每个锁的相关特性。

图片描述

2.3.关于资源的小结

  1. 任何实例对外提供的方法(尽量避免对变量的直接操作)
  2. 我们需要在对外提供的方法内用“锁”去控制方法内的被调度的规则。
  3. 实行第二条之前一定要确定当前的编程环境,是单线程的还是多线程
3.线程池

用一段话形容线程和资源的关系那就是。某个人(线程)去做(调度)某件有要求和规则(锁策略)的事(资源);根据这件事(资源)的要求和规则(锁策略)去约束做这个事人(线程)的做法

我们用各种锁策略去保证资源能被正确的使用。这里我们还缺一个角度,那就是从线程的角度去调度资源。

我们用一个问题开头来展开对话。

  • 问:我们能根据资源的数量去创建线程的数量吗?
  • 答:不能,因为创建线程的开销大,受机器的配置的限制。
  • 问:那么能不能创建一定数量的线程,去循环的调度资源。
  • 答:这么做是可以的,但是资源数一般来说肯定是多于线程数,我们要控制资源的调度顺序,还未来得及调度的资源可以按先来后到原则存放到队列。
  • 问:那资源数少于线程数时候,该怎么样去处理。
  • 答:我们可以保留一定量的线程,为未来可能调度的资源做预备。

这就是一个线程池的雏形,线程池的雏形具有以下的基本特性

  1. 具有最大的线程数限制
  2. 有若干常备线程(核心线程)
  3. 资源数若多超过了最大线程数的限制则会放入队列中。

我们来解刨线程池的最全的配置信息:

  1. corePoolSize:核心线程数
  2. maximumPoolSize:最大的线程数
  3. keepAliveTime:无资源调度的线程回收的时间(默认单位:毫秒)
  4. TimeUnit:时间单位
  5. BlockingQueue<Runnable>:多余的资源放入的队列
  6. ThreadFactory:线程的工厂类
  7. 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 分治思想处理批量任务 线程
点击查看更多内容
11人点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
JAVA开发工程师
手记
粉丝
18
获赞与收藏
100

关注作者,订阅最新文章

阅读免费教程

感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消