秋招前的积累与沉淀
研究生期间我有一件事情一直在坚持,那就是做笔记和写博客。
做笔记,就是记录学习中大大小小的事情,可能是面试问题,可能是一周的学习计划,也可能知识一个知识点,总归都是值得记录的东西,对我来说,就是一种积累。而对于博客,我从一开始只用于记录项目,到后来做转载,再到后来写原创,整理系列文章,则更像是一种沉淀。
但是在春招刚刚结束的这段时间,我发现一个问题,之前学过的东西忘记了很多,特别是那些理解的不够深的知识点,总是特别容易忘记。另外我发现,虽然我在笔记中记录了很多的知识点和面试题,但是往往我只看过一次,不会再去看第二次。
这也意味着,虽然记录的内容很多,但是真正消化吸收的内容很少,脑子里充斥着总是那些零碎的知识点和面试问题,对于完整的知识体系知之甚少。这些问题在春招期间也不断地暴露出来,让我思考了很久。
面对如此窘境,我想做出改变,趁着现在时间充裕,我想要为这些内容做一次减法,并且借此机会,推翻自己原有的知识体系,重建新的知识框架。简单说来,就是重新开始学习Java后端,这次我要用一种更高效的方式,避免走之前走的弯路,要用最高效,最合理的方式去复习。由于我之前已经有基础,所以我对完成这一目标有信心,相应地我也为此做出了明确且详细的学习计划。
我打算用几个关键词来形容这三个月的秋招复习。
“具体可靠的学习计划”
在三个月的时间里,我首先按照Java后端路线图安排好复习计划,每个知识点都会对应安排一段时间,比如我可能花一天时间复习“Java反射”,两天时间复习“设计模式”,一周的时间用于复习"JVM虚拟机”。我一般会在月初做好整个月的计划,然后根据进度做一些微调,但是基本上我都可以跟上进度,并且是在复习到位的前提下。
所以我觉得,对于秋招这一场苦战,指定计划尤为重要,一旦计划定下来,战略目标清晰,对应的战术制定也会变得清晰,执行力也会随之变强。
“写博客整合知识点”
至于复习方法,我主要通过看高质量博客,并且结合代码实践的方式巩固这部分知识点,比如今天学习“concurrenthashmap”,我会去找两三篇比较好的博客先看看,主要是源码解读方面的,然后我会把它们进行整合,如果有遗漏的知识点我会再进行补充,有时候我还会自己去看看JDK源码,以便更好地理解博客内容,完成知识整合之后,我就会对应地整理出一篇博客出来,发在我的个人博客上。
除此之外,当我完成了一整个专题的复习之后,我会把这些文章整理成一个专题,比如上面说的“concurrenthashmap”,实际上属于Java并发包,所以我会专门做一个博客专栏,用来完成Java并发系列的文章专题。对于每一个文章专题,我都会先理清这个专题一共有哪些内容,然后再开始整理。比如对于Java并发包,我会先写Java多线程基础的文章,再写JMM内存模型的文章,接着一步步着手写Java线程池,阻塞队列,工具类,原子类等等。这样一来这部分内容就复习完毕了,写系列文章的好处就在于,我可以从头到尾理清脉络,并且对于每一部分的知识点都做了比较好的总结。
对于博客的选择,我吸取了之前的教训,宁愿花半小时看一篇高质量文章,也不花10分钟看5篇烂文章。深度阅读的好处,就是可以让这部分内容更好地融入你脑内的知识体系,而不是像其他快餐文章一样转瞬即逝。
“做项目巩固实践能力”
由于之前在实习期间参加的项目都比较大,我接触的模块也比较单一,没有对整体项目有一个很好的把握,所以我决定趁这段时间再巩固一下我的项目实践能力,这里的能力主要是指的是对项目架构的把握能力,以及对业务开发的熟练度,当然也包括对各种常用后端技术的熟悉程度。
我花了大概一个月的时间完成了两个项目的开发,当然主要也是模仿两个开源项目做了,这两个项目都使用SpringBoot快速开发,并且用到一些常用的后端技术比如redis,云存储,以及一些常见Web框架,除此之外还涉及到了solr,爬虫等技术。虽然项目不算很难,但是我在这段时间里很快地熟悉了完整项目开发的流程,并且每天做迭代,通过Git来跟进版本,每个版本都会写清所做的内容,这也让我对项目的架构非常熟悉。
在项目之余,我也找一些常用的后端组件来跑一跑demo,以便让我对这些技术有一个直观的了解,比如面试常问的dubbo,zookeeper,消息队列等组件。这些尝试也让我在理解它们的原理时更加得心应手了。
“坚持刷题,注重方法”
算法题是秋招笔试面试中的重头戏,每个研发同学都免不了经历算法题的摧残,对我这么一个非科班同学来说,更是让人头大。正因为如此,我放弃了刷大量LeetCode题目的方法,选择了更加行之有效的刷题方式。
首先我重新刷了一遍剑指offer,并且对每道题目进行总结,尽量保证每一道题都可以记在脑子里,众所周知剑指offer中的题是面试时非常喜欢考的,所以先搞定这部分题目是最为关键的。
搞定剑指offer之后,当然还要刷LeetCode了,LeetCode题目这么多,怎么选择呢,我没有按照tag刷,也没有按照顺序刷,而是参考当时一个大佬的LeetCode刷题指南来进行刷题的,他把每个类型的题目都做了归纳,每部分只放一些比较经典的题目。所以我前后大概刷了100多道LeetCode的题目,并且在第二遍刷题复习的时候,我也对这些题目做了一份总结。
除了上面两个经典题库,我还着重刷了大厂的历年真题,这部分我主要是通过牛客网的历年真题题库来完成刷题的。说实话,真题是非常重要的,因为公司出的题目不像平时的那些算法题,可能会出得非常奇葩,所以你如果不提前适应的话会比较吃亏。完成这部分题目之后,我对算法题的复习也基本告一段落了。
当我完成所有内容的复习时,提前批已经开始了。终于要上战场了,因为战前准备比较充分,所以我对秋招还是比较乐观的,但事实上,秋招不仅是攻坚战,而且是持久战,要笑到最后,确实也不是那么容易的事情。
重建知识体系,对学过的东西做减法
前面提到我在秋招前完成了知识体系重建,那在这里我也想跟大家分享一下我当时大致的知识体系构成。就跟我前面说的一样,我选择重新再学一遍Java后端相关的技术内容,因为我知道大致的学习方向,并且有一定的基础,所以看很多文章变得更加得心应手,写文章和做总结也更加有底气了。
首先在Java基础方面,我写了20多篇原创博客,主要是对Java核心技术的解析,比如“Java反射”,“Java序列化和反序列化”,“Java异常体系”等等。
在Java集合类方面,我原创了部分文章,另外整合了一些比较好的技术文章,其中最主要的就是关于hashmap的文章,当时我整合的文章几乎没有遗漏任何一个知识点。
在Java并发编程方面,我主要参考了并发编程网以及一些优质博客的文章,先搞懂了Java并发原理,再一步步学习JUC并发包的组件,其中重点看了chm,并发工具类以及阻塞队列等JDK源码的解析文章,除此之外,我还会在IDE中跑JUC相关的emo,毕竟这方面的内容非常需要实践。
在Java网络编程方面,我先从最基础的socket入手,再讲到NIO,AIO,并且加入了几篇对Linux IO模型解析的文章,让整个知识体系更加完整(因为NIO是基于Linux Epoll实现的),接着我又加入了对Netty的探讨,以及Tomcat中对NIO的应用,可以说是把Java网络编程一些比较重要的部分都囊括进来了。为了更好理解这部分内容,我也在网上参考了很多客户端和服务端通信的demo,最后我分别用Socket,NIO,AIO以及Netty把C/S 通信的demo都写了一遍。
在JVM虚拟机方面,我则按照《深入理解JVM虚拟机》这本书的行文脉络进行文章的整理。在搞定JVM基本原理以后,我着重了解了JVM调优和实践中常遇到的问题,并且整理了常用的JVM调优工具,场景问题以及调优实践的案例,这也是因为面试中对JVM调优实践越来越重视了。
在JavaWeb方面,我从Java Web相关技术的发展入手,一步步了解了每种技术存在的意义,比如JSP,Servlet,JDBC,Spring等等,然后对每种技术进行了比较全面的了解,并且着重地看了Spring和SpringMVC的源码分析文章,另外一方面,我花了很多时间去研究Tomcat的工作原理。除此之外,JavaWeb项目中常用的maven,日志组件,甚至是单测试组件,也纳入了我的系列文章里。
在数据库和缓存方面,我主要学习了MySQL和Redis这两种最常用的数据库。对于Mysql,我从简单的sql开始了解,然后开始了解sql优化,MySQL的存储引擎和索引,事务及锁,还有更复杂的主从复制,分库分表等内容。对于Redis,我也是从简单的api入手,然后去了解每一种数据结构的底层实现原理,接着尝试去学习Redis的持久化方式,以及作为缓存常需要考虑的技术点,当然,也包括Redis的分布式锁实现,以及它的分布式集群方案。
最后一部分就是分布式相关的理论和技术了,这个也是困扰我很久的一块内容,我主要把这块内容分为两个部分,分别是分布式理论和分布式技术,理论方面,我先了解CAP,BASE等基本知识,然后开始学习一致性协议和算法,接着探讨分布式事务。对于分布式技术,涉及的东西就更多了,例如分布式session,负载均衡,分布式锁等内容,这些知识点我都会用一到两篇文章去总结,对于分布式缓存,消息队列,以及分布式服务等内容,我会花比较多的时间去全面学习,然后总结出一个系列的文章出来。当然,对于这些技术的学习主要还是停留在理论方面,在自己的项目中能用到的比较少。
至此,我的知识体系基本构建完成,这也是我在秋招中能够成功闯过那么多面试的原因。
作者:黄小斜
来源:CSDN
原文:https://blog.csdn.net/a724888/article/details/60866846
版权声明:本文为博主原创文章,转载请附上博文链接!
Java并发编程一直是Java程序员必须懂但又是很难懂的技术内容。
这里不仅仅是指使用简单的多线程编程,或者使用juc的某个类。当然这些都是并发编程的基本知识,除了使用这些工具以外,Java并发编程中涉及到的技术原理十分丰富。为了更好地把并发知识形成一个体系,也鉴于本人目前也没有能力写出这类文章,于是参考几位并发编程方面专家的博客和书籍,做一个简单的整理。
首先说一下我学习Java并发编程的一些方法吧。大概分为这几步:
1、先学会最基础的Java多线程编程,Thread类的使用,线程通信的一些方法等等。这部分内容需要多写一些demo去实践。
2、接下来可以去使用一些JUC的API,比如concurrenthashmap,并发工具类,原子数据类型等工具,在学习这部分内容的时候,你可以搭配一些介绍并发编程的书籍和博客一起看,书籍我当时看的是《Java并发编程艺术》,我觉得略好于《Java并发编程实践》。
我这个专栏里也整合了一些比较好的博客,所以大家可以不妨先看看。
3、接下来就要阅读源码了,读源码部分最主要的就是读JUC包的源码,比如concurrenthashmap,阻塞队列,线程池等等,当然,这些源码自己读起来会比较痛苦,所以建议跟着博客走。
4、走到这一步,你已经理解了Java并发编程原理,并且可以熟练使用JUC,应付面试已经足够了,剩下的事情就是真正把这些东西用到项目中去,我当时在网易实习的时候就用到了JUC的一些内容,不得不说还是挺有意思的。
下面先介绍一下Java并发编程的一些主要内容,我把它分六个部分,大家可以参考这几个部分的内容分别进行学习。
一:并发基础和多线程
首先需要学习的就是并发的基础知识,什么是并发,为什么要并发,多线程的概念,线程安全的概念等。
然后学会使用Java中的Thread或是其他线程实现方法,了解线程的状态转换,线程的方法,线程的通信方式等。
二:JMM内存模型
任何语言最终都是运行在处理器上,JVM虚拟机为了给开发者一个一致的编程内存模型,需要制定一套规则,这套规则可以在不同架构的机器上有不同实现,并且向上为程序员提供统一的JMM内存模型。
所以了解JMM内存模型也是了解Java并发原理的一个重点,其中了解指令重排,内存屏障,以及可见性原理尤为重要。
JMM只保证happens-before和as-if-serial规则,所以在多线程并发时,可能出现原子性,可见性以及有序性这三大问题。
下面的内容则会讲述Java是如何解决这三大问题的。
三:synchronized,volatile,final等关键字
对于并发的三大问题,volatile可以保证可见性,synchronized三种特性都可以保证。
synchronized是基于操作系统的mutex lock指令实现的,volatile和final则是根据JMM实现其内存语义。
此处还要了解CAS操作,它不仅提供了类似volatile的内存语义,并且保证操作原子性,因为它是由硬件实现的。
JUC中的Lock底层就是使用volatile加上CAS的方式实现的。synchronized也会尝试用cas操作来优化器重量级锁。
了解这些关键字是很有必要的。
四:JUC包
在了解完上述内容以后,就可以看看JUC的内容了。
JUC提供了包括Lock,原子操作类,线程池,同步容器,工具类等内容。
这些类的基础都是AQS,所以了解AQS的原理是很重要的。
除此之外,还可以了解一下Fork/Join,以及JUC的常用场景,比如生产者消费者,阻塞队列,以及读写容器等。
五:实践
上述这些内容,除了JMM部分的内容比较不好实现之外,像是多线程基本使用,JUC的使用都可以在代码实践中更好地理解其原理。多尝试一些场景,或者在网上找一些比较经典的并发场景,或者参考别人的例子,在实践中加深理解,还是很有必要的。
六:补充
由于很多Java新手可能对并发编程没什么概念,在这里放一张不错的思维导图,该图简要地提几个并发编程中比要重要的点,也是比较基本的点,在大致了解了这些基础内容以后,才能更好地开展后面详细内容的学习。
上面讲到了学习路线,建议大家先跟着这个路线去看一看本专栏的一些博客,然后再来看下面这部分内容,因为下面的内容是我基于本专栏所有博客进行归纳和总结的,主要是方便记忆和复习,也可以让你把知识点重新过一遍,如果你觉得我的总结不够好,你也可以自己做总结,这也是一种不错的学习方法,话不多少,咱们接着往下看。
这篇总结主要是基于我Java并发技术系列的文章而形成的的。主要是把重要的知识点用自己的话说了一遍,可能会有一些错误,还望见谅和指点。谢谢
更多详细内容可以查看我的专栏文章:Java并发技术指南
https://blog.csdn.net/column/details/21961.html
线程安全
线程安全一般指多线程之间的操作结果不会因为线程调度的顺序不同而发生改变。
互斥和同步
互斥一般指资源的独占访问,同步则要求同步代码中的代码顺序执行,并且也是单线程独占的。
JMM内存模型
JVM中的内存分区包括堆,栈,方法区等区域,这些内存都是抽象出来的,实际上,系统中只有一个主内存,但是为了方便
Java
多线程语义的实现,以及降低程序员编写并发程序的难度,
Java
提出了JMM内存模型,将内存分为主内存和工作内存,工作内存是线程独占的,实际上它是一系列寄存器,编译器优化后的结果。
as-if-Serial,happens-before
as
if
serial语义提供单线程代码的顺序执行保证,虽然他允许指令重排序,但是前提是指令重排序不会改变执行结果。
volatile
volatile
语义实际上是在代码中插入一个内存屏障,内存屏障分为读写,写读,读读,写写四种,可以用来避免
volatile
变量的读写操作发生重排序,从而保证了
volatile
的语义,实际上,
volatile
修饰的变量强制要求线程写时将数据从缓存刷入主内存,读时强制要求线程从主内存中读取,因此保证了它的可见性。
而对于
volatile
修饰的
64
位类型数据,可以保证其原子性,不会因为指令重排序导致一个
64
位数据被分割成两个
32
位数据来读取。
synchronized和锁优化
synchronized
是
Java
提供的同步标识,底层是操作系统的mutex
lock
调用,需要进行用户态到内核态的切换,开销比较大。
synchronized
经过编译后的汇编代码会有monitor
in
和monitor
out
的字样,用于标识进入监视器模块和退出监视器模块,
监视器模块watcher会监控同步代码块中的线程号,只允线程号正确的线程进入。
Java
在
synchronized
关键字中进行了多次优化。
比如轻量级锁优化,使用锁对象的对象头做文章,当一个线程需要获得该对象锁时,线程有一段空间叫做
lock
record,用于存储对象头的mask word,然后通过cas操作将对象头的mask word改成指向线程中的lockrecord。
如果成功了就是获取到了锁,否则就是发生了互斥。需要锁粗化,膨胀为互斥锁。
偏向锁,去掉了更多的同步措施,检查mask word是否是可偏向状态,然后检查mask word中的线程id是否是自己的id,如果是则执行同步代码,如果不是则cas修改其id,如果修改失败,则出现锁争用,偏向锁失效,膨胀为轻量级锁。
自旋锁,每个线程会被分配一段时间片,并且听候cpu调度,如果发生线程阻塞需要切换的开销,于是使用自旋锁不需要阻塞,而是忙等循环,一获取时间片就开始忙等,这样的锁就是自旋锁,一般用于并发量比较小,又担心切换开销的场景。
CAS操作
CAS操作是通过硬件实现的原子操作,通过一条指令完成比较和赋值的操作,防止发生因指令重排导致的非原子操作,在
Java
中通过
unsafe
包可以直接使用,在
Java
原子类中使用cas操作来完成一系列原子数据类型的构建,保证自加自减等依赖原值的操作不会出现并发问题。
cas操作也广泛用在其他并发类中,通过循环cas操作可以完成线程安全的并发赋值,也可以通过一次cas操作来避免使用互斥锁。
Lock类
AQS
AQS是
Lock
类的基石,他是一个抽象类,通过操作一个变量state来判断线程锁争用的情况,通过一系列方法实现对该变量的修改。一般可以分为独占锁和互斥锁。
AQS维护着一个CLH阻塞队列,这个队列主要用来存放阻塞等待锁的线程节点。可以看做一个链表。
一:独占锁
独占锁的state只有0和1两种情况(如果是可重入锁也可以把state一直往上加,这里不讨论),state = 1时说明已经有线程争用到锁。线程获取锁时一般是通过aqs的lock方法,如果state为0,首先尝试cas修改state=1,成功返回,失败时则加入阻塞队列。 非公共锁使用时,线程节点加入阻塞队列时依然会尝试cas获取锁,最后如果还是失败再老老实实阻塞在队列中。
独占锁还可以分为公平锁和非公平锁,公平锁要求锁节点依据顺序加入阻塞队列,通过判断前置节点的状态来改变后置节点的状态,比如前置节点获取锁后,释放锁时会通知后置节点。
非公平锁则不一定会按照队列的节点顺序来获取锁,如上面所说,会先尝试cas操作,失败再进入阻塞队列。
二:共享锁
共享锁的state状态可以是0到n。共享锁维护的阻塞队列和互斥锁不太一样,互斥锁的节点释放锁后只会通知后置节点,而共享锁获取锁后会通知所有的共享类型节点,让他们都来获取锁。共享锁用于countdownlatch工具类与cyliderbarrier等,可以很好地完成多线程的协调工作
锁Lock和Conditon
Lock 锁维护这两个内部类fairsync和unfairsync,都继承自aqs,重写了部分方法,实际上大部分方法还是aqs中的,Lock只是重新把AQS做了封装,让程序员更方便地使用Lock锁。
和Lock锁搭配使用的还有condition,由于Lock锁只维护着一个阻塞队列,有时候想分不同情况进行锁阻塞和锁通知怎么办,原来我们一般会使用多个锁对象,现在可以使用condition来完成这件事,比如线程A和线程B分别等待事件A和事件B,可以使用两个condition分别维护两个队列,A放在A队列,B放在B队列,由于Lock和condition是绑定使用的,当事件A触发,线程A被唤醒,此时他会加入Lock自己的CLH队列中进行锁争用,当然也分为公平锁和非公平锁两种,和上面的描述一样。
Lock和condtion的组合广泛用于JUC包中,比如生产者和消费者模型,再比如cyliderbarrier。
读写锁
读写锁也是Lock的一个子类,它在一个阻塞队列中同时存储读线程节点和写线程节点,读写锁采用state的高16位和低16位分别代表独占锁和共享锁的状态,如果共享锁的state > 0可以继续获取读锁,并且state-1,如果=0,则加入到阻塞队列中,写锁节点和独占锁的处理一样,因此一个队列中会有两种类型的节点,唤醒读锁节点时不会唤醒写锁节点,唤醒写锁节点时,则会唤醒后续的节点。
因此读写锁一般用于读多写少的场景,写锁可以降级为读锁,就是在获取到写锁的情况下可以再获取读锁。
并发工具类
1 countdownlatch
countdownlatch主要通过AQS的共享模式实现,初始时设置state为N,N是countdownlatch初始化使用的size,每当有一个线程执行countdown,则state-1,state = 0之前所有线程阻塞在队列中,当state=0时唤醒队头节点,队头节点依次通知所有共享类型的节点,唤醒这些线程并执行后面的代码。
2 cycliderbarrier
cycliderbarrier主要通过lock和condition结合实现,首先设置state为屏障等待的线程数,在某个节点设置一个屏障,所有线程运行到此处会阻塞等待,其实就是等待在一个condition的队列中,并且每当有一个线程到达,state -=1 则当所有线程到达时,state = 0,则唤醒condition队列的所有结点,去执行后面的代码。
3 samphere
samphere也是使用AQS的共享模式实现的,与countlatch大同小异,不再赘述。
4 exchanger
exchanger就比较复杂了。使用exchanger时会开辟一段空间用来让两个线程进行交互操作,这个空间一般是一个栈或队列,一个线程进来时先把数据放到这个格子里,然后阻塞等待其他线程跟他交换,如果另一个线程也进来了,就会读取这个数据,并把自己的数据放到对方线程的格子里,然后双双离开。当然使用栈和队列的交互是不同的,使用栈的话匹配的是最晚进来的一个线程,队列则相反。
原子数据类型
原子数据类型基本都是通过cas操作实现的,避免并发操作时出现的安全问题。
同步容器
同步容器主要就是concurrenthashmap了,在集合类中我已经讲了chm了,所以在这里简单带过,chm1.7通过分段锁来实现锁粗化,使用的死LLock锁,而1.8则改用synchronized和cas的结合,性能更好一些。
还有就是concurrentlinkedlist,ConcurrentSkipListMap与CopyOnWriteArrayList。
第一个链表也是通过cas和synchronized实现。
而concurrentskiplistmap则是一个跳表,跳表分为很多层,每层都是一个链表,每个节点可以有向下和向右两个指针,先通过向右指针进行索引,再通过向下指针细化搜索,这个的搜索效率是很高的,可以达到logn,并且它的实现难度也比较低。通过跳表存map就是把entry节点放在链表中了。查询时按照跳表的查询规则即可。
CopyOnWriteArrayList是一个写时复制链表,查询时不加锁,而修改时则会复制一个新list进行操作,然后再赋值给原list即可。 适合读多写少的场景。
阻塞队列
BlockingQueue 实现之 ArrayBlockingQueue
ArrayBlockingQueue
其实就是数组实现的阻塞队列,该阻塞队列通过一个
lock
和两个condition实现,一个condition负责从队头插入节点,一个condition负责队尾读取节点,通过这样的方式可以实现生产者消费者模型。
BlockingQueue 实现之 LinkedBlockingQueue
LinkedBlockingQueue
是用链表实现的阻塞队列,和arrayblockqueue有所区别,它支持实现为无界队列,并且它使用两个
lock
和对应的condition搭配使用,这是因为链表可以同时对头部和尾部进行操作,而数组进行操作后可能还要执行移位和扩容等操作。
所以链表实现更灵活,读写分别用两把锁,效率更高。
BlockingQueue 实现之 SynchronousQueue
SynchronousQueue
实现是一个不存储数据的队列,只会保留一个队列用于保存线程节点。详细请参加上面的exchanger实现类,它就是基于
SynchronousQueue
设计出来的工具类。
BlockingQueue 实现之 PriorityBlockingQueue
PriorityBlockingQueue
PriorityBlockingQueue
是一个支持优先级的无界队列。默认情况下元素采取自然顺序排列,也可以通过比较器comparator来指定元素的排序规则。元素按照升序排列。
DelayQueue
DelayQueue
是一个支持延时获取元素的无界阻塞队列。队列使用
PriorityQueue
来实现。队列中的元素必须实现
Delayed
接口,在创建元素时可以指定多久才能从队列中获取当前元素。只有在延迟期满时才能从队列中提取元素。我们可以将
DelayQueue
运用在以下应用场景:
缓存系统的设计:可以用
DelayQueue
保存缓存元素的有效期,使用一个线程循环查询
DelayQueue
,一旦能从
DelayQueue
中获取元素时,表示缓存有效期到了。
定时任务调度。使用
DelayQueue
保存当天将会执行的任务和执行时间,一旦从
DelayQueue
中获取到任务就开始执行,从比如
TimerQueue
就是使用
DelayQueue
实现的。
线程池
类图
首先看看executor接口,只提供一个run方法,而他的一个子接口executorservice则提供了更多方法,比如提交任务,结束线程池等。
然后抽象类abstractexecutorservice提供了更多的实现了,最后我们最常使用的类ThreadPoolExecutor就是继承它来的。
ThreadPoolExecutor可以传入多种参数来自定义实现线程池。
而我们也可以使用Executors中的工厂方法来实例化常用的线程池。
常用线程池
比如newFixedThreadPool
newSingleThreadExecutor newCachedThreadPool
newScheduledThreadPool等等,这些线程池即可以使用submit提交有返回结果的callable和futuretask任务,通过一个future来接收结果,或者通过callable中的回调函数call来回写执行结果。也可以用execute执行无返回值的runable任务。
在探讨这些线程池的区别之前,先看看线程池的几个核心概念。
1 任务队列:线程池中维护了一个任务队列,每当向线程池提交任务时,任务加入队列。
2 工作线程:也叫worker,从线程池中获取任务并执行,执行后被回收或者保留,因情况而定。
3 核心线程数和最大线程数,核心线程数是线程池需要保持存活的线程数量,以便接收任务,最大线程数是能创建的线程数上限。
4 newFixedThreadPool可以设置固定的核心线程数和最大线程数,一个任务进来以后,就会开启一个线程去执行,并且这部分线程不会被回收,当开启的线程达到核心线程数时,则把任务先放进任务队列。当任务队列已满时,才会继续开启线程去处理,如果线程总数打到最大线程数限制,任务队列又是满的时候,会执行对应的拒绝策略。
5 拒绝策略一般有几种常用的,比如丢弃任务,丢弃队尾任务,回退给调用者执行,或者抛出异常,也可以使用自定义的拒绝策略。
6 newSingleThreadExecutor是一个单线程执行的线程池,只会维护一个线程,他也有任务队列,当任务队列已满并且线程数已经是1个的时候,再提交任务就会执行拒绝策略。
7 newCachedThreadPool比较特别,第一个任务进来时会开启一个线程,而后如果线程还没执行完前面的任务又有新任务进来,就会再创建一个线程,这个线程池使用的是无容量的SynchronousQueue队列,要求请求线程和接受线程匹配时才会完成任务执行。 所以如果一直提交任务,而接受线程来不及处理的话,就会导致线程池不断创建线程,导致cpu消耗很大。
8 ScheduledThreadPoolExecutor内部使用的是delayqueue队列,内部是一个优先级队列priorityqueue,也就是一个堆。通过这个delayqueue可以知道线程调度的先后顺序和执行时间点。
Fork/Join框架
又称工作窃取线程池。
我们在大学算法课本上,学过的一种基本算法就是:分治。其基本思路就是:把一个大的任务分成若干个子任务,这些子任务分别计算,最后再Merge出最终结果。这个过程通常都会用到递归。
而Fork/Join其实就是一种利用多线程来实现“分治算法”的并行框架。
另外一方面,可以把Fori/Join看作一个单机版的Map/Reduce,只不过这里的并行不是多台机器并行计算,而是多个线程并行计算。
1 与ThreadPool的区别 通过上面例子,我们可以看出,它在使用上,和ThreadPool有共同的地方,也有区别点: (1) ThreadPool只有“外部任务”,也就是调用者放到队列里的任务。 ForkJoinPool有“外部任务”,还有“内部任务”,也就是任务自身在执行过程中,分裂出”子任务“,递归,再次放入队列。 (2)ForkJoinPool里面的任务通常有2类,RecusiveAction/RecusiveTask,这2个都是继承自FutureTask。在使用的时候,重写其compute算法。
2 工作窃取算法 上面提到,ForkJoinPool里有”外部任务“,也有“内部任务”。其中外部任务,是放在ForkJoinPool的全局队列里面,而每个Worker线程,也有一个自己的队列,用于存放内部任务。
3 窃取的基本思路就是:当worker自己的任务队列里面没有任务时,就去scan别的线程的队列,把别人的任务拿过来执行