如何才可以系统地学习Java并发技术?

微信公众号【Java技术江湖】一位阿里Java工程师的技术小站html

Java并发编程一直是Java程序员必须懂但又是很难懂的技术内容。程序员

这里不只仅是指使用简单的多线程编程,或者使用juc的某个类。固然这些都是并发编程的基本知识,除了使用这些工具之外,Java并发编程中涉及到的技术原理十分丰富。为了更好地把并发知识造成一个体系,也鉴于本人目前也没有能力写出这类文章,因而参考几位并发编程方面专家的博客和书籍,作一个简单的整理。面试

首先说一下我学习Java并发编程的一些方法吧。大概分为这几步:算法

一、先学会最基础的Java多线程编程,Thread类的使用,线程通讯的一些方法等等。这部份内容须要多写一些demo去实践。编程

二、接下来能够去使用一些JUC的API,好比concurrenthashmap,并发工具类,原子数据类型等工具,在学习这部份内容的时候,你能够搭配一些介绍并发编程的书籍和博客一块儿看,书籍我当时看的是《Java并发编程艺术》,我以为略好于《Java并发编程实践》。设计模式

我这个专栏里也整合了一些比较好的博客,因此你们能够不妨先看看。数组

三、接下来就要阅读源码了,读源码部分最主要的就是读JUC包的源码,好比concurrenthashmap,阻塞队列,线程池等等,固然,这些源码本身读起来会比较痛苦,因此建议跟着博客走。 缓存

四、走到这一步,你已经理解了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/...

线程安全

  1. 线程安全通常指多线程之间的操做结果不会由于线程调度的顺序不一样而发生改变。

互斥和同步

  1. 互斥通常指资源的独占访问,同步则要求同步代码中的代码顺序执行,而且也是单线程独占的。

JMM内存模型

  1. JVM中的内存分区包括堆,栈,方法区等区域,这些内存都是抽象出来的,实际上,系统中只有一个主内存,可是为了方便Java多线程语义的实现,以及下降程序员编写并发程序的难度,Java提出了JMM内存模型,将内存分为主内存和工做内存,工做内存是线程独占的,实际上它是一系列寄存器,编译器优化后的结果。

as-if-Serial,happens-before

  1. asif serial语义提供单线程代码的顺序执行保证,虽然他容许指令重排序,可是前提是指令重排序不会改变执行结果。

volatile

  1. volatile语义其实是在代码中插入一个内存屏障,内存屏障分为读写,写读,读读,写写四种,能够用来避免volatile变量的读写操做发生重排序,从而保证了volatile的语义,实际上,volatile修饰的变量强制要求线程写时将数据从缓存刷入主内存,读时强制要求线程从主内存中读取,所以保证了它的可见性。
  2. 而对于volatile修饰的64位类型数据,能够保证其原子性,不会由于指令重排序致使一个64位数据被分割成两个32位数据来读取。

synchronized和锁优化

  1. synchronized是Java提供的同步标识,底层是操做系统的mutex lock调用,须要进行用户态到内核态的切换,开销比较大。
  2. synchronized通过编译后的汇编代码会有monitor in和monitor out的字样,用于标识进入监视器模块和退出监视器模块,
  3. 监视器模块watcher会监控同步代码块中的线程号,只允线程号正确的线程进入。
  4. Java在synchronized关键字中进行了屡次优化。
  5. 好比轻量级锁优化,使用锁对象的对象头作文章,当一个线程须要得到该对象锁时,线程有一段空间叫作lock record,用于存储对象头的mask word,而后经过cas操做将对象头的mask word改为指向线程中的lockrecord。
  6. 若是成功了就是获取到了锁,不然就是发生了互斥。须要锁粗化,膨胀为互斥锁。
  7. 偏向锁,去掉了更多的同步措施,检查mask word是不是可偏向状态,而后检查mask word中的线程id是不是本身的id,若是是则执行同步代码,若是不是则cas修改其id,若是修改失败,则出现锁争用,偏向锁失效,膨胀为轻量级锁。
  8. 自旋锁,每一个线程会被分配一段时间片,而且听候cpu调度,若是发生线程阻塞须要切换的开销,因而使用自旋锁不须要阻塞,而是忙等循环,一获取时间片就开始忙等,这样的锁就是自旋锁,通常用于并发量比较小,又担忧切换开销的场景。

CAS操做

  1. CAS操做是经过硬件实现的原子操做,经过一条指令完成比较和赋值的操做,防止发生因指令重排致使的非原子操做,在Java中经过unsafe包能够直接使用,在Java原子类中使用cas操做来完成一系列原子数据类型的构建,保证自加自减等依赖原值的操做不会出现并发问题。
  2. cas操做也普遍用在其余并发类中,经过循环cas操做能够完成线程安全的并发赋值,也能够经过一次cas操做来避免使用互斥锁。

Lock类

AQS

  1. AQS是Lock类的基石,他是一个抽象类,经过操做一个变量state来判断线程锁争用的状况,经过一系列方法实现对该变量的修改。通常能够分为独占锁和互斥锁。
  2. 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

  1. ArrayBlockingQueue其实就是数组实现的阻塞队列,该阻塞队列经过一个lock和两个condition实现,一个condition负责从队头插入节点,一个condition负责队尾读取节点,经过这样的方式能够实现生产者消费者模型。   

BlockingQueue 实现之 LinkedBlockingQueue

  1. LinkedBlockingQueue是用链表实现的阻塞队列,和arrayblockqueue有所区别,它支持实现为无界队列,而且它使用两个lock和对应的condition搭配使用,这是由于链表能够同时对头部和尾部进行操做,而数组进行操做后可能还要执行移位和扩容等操做。
  2. 因此链表实现更灵活,读写分别用两把锁,效率更高。

BlockingQueue 实现之 SynchronousQueue

  1. SynchronousQueue实现是一个不存储数据的队列,只会保留一个队列用于保存线程节点。详细请参加上面的exchanger实现类,它就是基于SynchronousQueue设计出来的工具类。

BlockingQueue 实现之 PriorityBlockingQueue

PriorityBlockingQueue

  1.     PriorityBlockingQueue是一个支持优先级的无界队列。默认状况下元素采起天然顺序排列,也能够经过比较器comparator来指定元素的排序规则。元素按照升序排列。

DelayQueue

  1.     DelayQueue是一个支持延时获取元素的无界阻塞队列。队列使用PriorityQueue来实现。队列中的元素必须实现Delayed接口,在建立元素时能够指定多久才能从队列中获取当前元素。只有在延迟期满时才能从队列中提取元素。咱们能够将DelayQueue运用在如下应用场景:
  2.     缓存系统的设计:能够用DelayQueue保存缓存元素的有效期,使用一个线程循环查询DelayQueue,一旦能从DelayQueue中获取元素时,表示缓存有效期到了。
  3.     定时任务调度。使用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别的线程的队列,把别人的任务拿过来执行

▼更多精彩内容这些喜闻乐见的Java面试知识点,你都掌握了吗?
有关秋招面试的一些小技巧
设计模式常见面试知识点总结
Java基础知识点总结
Java集合类常见面试知识点总结
[](http://mp.weixin.qq.com/s?__b...


微信公众号【程序员江湖】

做者黄小斜,斜杠青年,某985硕士,阿里研发工程师,于2018 年秋招拿到 BAT 头条、网易、滴滴等 8 个大厂 offer

我的擅长领域 :自学编程、技术校园招聘、软件工程考研(关注公众号后回复”资料“便可领取 3T 免费技术学习资源)

相关文章
相关标签/搜索