• 新版网站前后台即将上线,2019年将致力于提高文章质量,加大原创力度,打造一个更加舒适的阅读体验!
  • 极客文库小编@勤劳的小蚂蚁,为您推荐每日资讯,欢迎关注!
  • 新版网站前后台即将上线,2019年将致力于提高文章质量,加大原创力度,打造一个更加舒适的阅读体验!
  • 如果有任何体验不佳的地方,欢迎向客服反馈!

如何才能够系统地学习java并发技术?

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 并发技术指南

线程安全

  1. 线程安全一般指多线程之间的操作结果不会因为线程调度的顺序不同而发生改变。

     

互斥和同步

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

     

JMM 内存模型

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

     

as-if-Serial,happens-before

  1. as
    if
    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 别的线程的队列,把别人的任务拿过来执行


丨极客文库, 版权所有丨如未注明 , 均为原创丨
本网站采用知识共享署名-非商业性使用-相同方式共享 3.0 中国大陆许可协议进行授权
转载请注明原文链接:如何才能够系统地学习 java 并发技术?
喜欢 (0)
[247507792@qq.com]
分享 (0)
勤劳的小蚂蚁
关于作者:
温馨提示:本文来源于网络,转载文章皆标明了出处,如果您发现侵权文章,请及时向站长反馈删除。

欢迎 注册账号 登录 发表评论!

  • 精品技术教程
  • 编程资源分享
  • 问答交流社区
  • 极客文库知识库

客服QQ


QQ:2248886839


工作时间:09:00-23:00