第6章 Java并发容器和框架

6.1  ConcurrentHashMap(线程安全的HashMap、锁分段技术)

6.1.1 为什么要使用ConcurrentHashMap

  在并发编程中使用HashMap可能导致程序死循环,而线程安全的HashTable效率又非常低下。基于以上两个原因,便有了ConcurrentHashMap的登场机会。

  (1)线程不安全的HashMap

  在多线程环境下,使用HashMap进行put操作会引起死循环(因为多线程会导致HashMap的Entry链表形成环形数据结构,Entry的next节点永远不为空,就会产生死循环获取Entry。),导致CPU利用率接近100%,所以在并发情况下不能使用HashMap。

  (2)效率低下的HashTable

  HashTable容器使用sychronized来保证线程安全,但在线程竞争激烈的情况下HashTable的效率非常低下。因为当一个线程访问HashTable的同步方法,其他线程也访问HashTable的同步方法时,会进入阻塞或轮询状态。如线程1使用put进行元素添加,线程2不但不能用put方法添加元素,也不能使用get方法来获取元素,所以竞争越激烈效率越低。

  (3)ConcurrentHashMap的锁分段技术可有效提升并发访问率

  HashTable容器在竞争激烈的并发环境下表现效率低下的原因是所有访问HashTable的线程都必须竞争同一把锁。假如容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效提高并发访问效率,这就是ConcurrentHashMap所使用的锁分段技术。首先将数据分成一段一段地存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一段数据的时候,其他段的数据也能被其他线程访问

ConcurrentHashMap的结构:

  ConcurrentHashMap由Segment数组结构HashEntry数组结构组成。Segment是可重入锁,扮演锁的角色;HashEntry存储键值对数据
  一个ConcurrentHashMap里包含一个Segment数组,Segment的结构与HashMap类似,是一种数组和链表结构。一个Segment包含一个HashEntry数组,每个HashEntry是一个链表结构的元素,每个Segment守护一个HashEntry数组里的元素。当对HashEntry数组的数据进行修改的时候,必须首先获得与它对应的Segment锁

ConcurrentHashMap的操作:

  get操作get过程不需要加锁,只有值为空值的时候才加锁重读。(如何做到不加锁的?get方法里将要使用的共享变量都定义为volatile类型。)

  put操作put过程必须加锁(由于put方法里需要对共享变量进行写入操作,所以为了线程安全,在操作共享变量时必须加锁)。put方法首先定位到Segment,然后在segment里进行插入操作

  插入操作步骤:第一步判断是否需要对Segment里的HashEntry数组进行扩容,第二步定位添加元素的位置,然后将其放到HashEntry数组里。

  • 是否需要扩容? 在插入元素前先判断Segment里的HashEntry数组是否超过容量(threadshold),如果超过阈值,则对数组进行扩容。
  • 如何扩容? 在扩容时,首先会创建一个容量为原来容量2倍的数组,然后将原数组里的元素进行再散列后插入到新的数组里。为了高效,ConcurrentHashMap不会对整个容器进行扩容,而只对某个segment进行扩容。

size操作:先尝试2次通过不锁住Segment的方式来统计各个Segment大小,如果统计过程中count发生了变化,则再采用加锁的方式(统计size的时候把所有Segment的put、remove、clean方法全部锁住)来统计所有Segment的大小。

  • ConcurrentHashMap如何判断在统计的时候容器是否发生了变化呢? 使用modCount变量,在put、remove和clean方法里操作元素前都会将变量modCount进行加1,那么在统计size前后比较modCount是否发生变化,从而得知容器的大小是否发生变化。

6.2  ConcurrentLinkedQueue(非阻塞的线程安全队列)

  实现一个线程安全的队列有两种方式:

  • 使用阻塞方法:用一个锁(入队和出队用同一把锁)或者用两个锁(入队和出队用不同的锁)等方式实现。
  • 使用非阻塞的方法:使用循环CAS。

  ConcurrentLinkedQueue是一个基于链接结点的无界线程安全队列,采用“先进先出”规则对节点进行排序。它采用了“wait-free”算法(即CAS算法)来实现。

6.3  阻塞队列

  阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。这两个附加的操作支持阻塞的插入和移除方法

  • 支持阻塞的插入方法:当队列满时,队列会阻塞插入元素的线程,直到队列不满。
  • 支持阻塞的移除方法:当队列空时,获取元素的线程会等待队列变为非空。

  阻塞队列常用于生产者和消费者的场景:生产者是向队列里添加元素的线程,消费者是从队列里取元素的线程。阻塞队列就是生产者用来存放元素、消费者用来获取元素的容器

  在阻塞队列不可应时,这两个附加操作(插入和移除)的4种处理方式:

方法/处理方式 抛出异常 返回特殊值 一直阻塞 超时退出
插入方法 add(e) offer(e) put(e) offer(e, time, unit)
移除方法 remove() poll() take() poll(time, unit)
检查方法 element() peek() 不可用 不可用
  • 抛出异常    :当队列满时,如果再往队列里插入元素会抛出IllegalStateException("Queue full")异常。当队列空时,从队列里获取元素会抛出NoSuchElementException异常。
  • 返回特殊值:当往队列插入元素时,会返回元素是否插入成功,成功返回true。如果是移除方法,则从队列里取出一个元素,如果没有则返回null。
  • 一直阻塞   :当阻塞队列满时,如果生产者线程往队列里put元素,队列会一直阻塞生产者线程,直到队列可用或者响应中断退出。当队列空时,如果消费者线程从队列列take元素,队列会阻塞消费者线程,直到队列不为空。
  • 超时退出   :当阻塞队列满时,如果生产者线程往队列里插入元素,队列会阻塞生产者线程一段时间,如果超时则退出。

【注】:如果是无界阻塞队列,队列不可能会出现满的情况,所以使用put或offer方法永远不会被阻塞,而且使用offer方法时,该方法永远返回true。

JDK7提供了7个阻塞队列:

  • ArrayBlockingQueue   :数组结构组成的有界阻塞队列,按FIFO原则对元素进行排序。
  • LinkedBlockingQueue :链表结构组成的有界阻塞队列,默认和最大长度为Integer.MAX_VALUE,按FIFO原则对元素进行排序。
  • PriorityBlockingQueue:支持优先级的无界阻塞队列,默认情况下元素采取自然顺序升序排列。不保证同优先级元素的顺序
  • DelayQueue                  :支持延时获取元素的无界阻塞队列。队列使用PriorityQueue实现,队列中的元素必须实现Delayed接口,在创建元素时可以指定多久才能从队列中获取当前元素。只有在延迟期满时才能从队列中提取元素。可应用于:
    • 缓存系统的设计:用DelayQueue保存缓存元素的有效期,使用一个线程循环查询DelayQueue,一旦能从DelayQueue中获取元素表示缓存有效期到了。
    • 定时任务调度:使用DelayQueue保存当天将会执行的任务和执行时间,一旦从DelayQueue中获取到任务就开始执行,比如TimerQueue就是使用DelayQueue实现的。
  • SynchronousQueue     :不存储元素的阻塞队列。每一个put操作必须等待一个take操作,否则不能继续添加元素。
  • LinkedTransferQueue  :链表结构组成的无界阻塞TransferQueue队列。相对于其他阻塞队列,LinkedTransferQueue多了tryTransfer和transfer方法。
    • transfer方法:如果当前有消费者正在等待接收元素,transfer方法可以把生产者传入的元素立刻传给消费者。如果没有消费者在等待接收元素,则将元素存放在队列tail节点并等到钙元素被消费者消费了才返回。
    • tryTransfer方法:如果没有消费者等待接收元素,则立即返回false。
  • LinkedBlockingDeque :链表结构组成的双向阻塞队列。可以从队列的两端插入和移出元素。

阻塞队列的实现原理:

  如果队列是空的,消费者会一直等待,当生产者添加元素时,消费者是如何知道当前队列有元素的呢?

  使用通知模式实现。就是当生产者往满的队列里添加元素时会阻塞住生产者,当消费者消费了一个队列中的元素后,会通知生产者当前队列可用。

6.4  Fork/Join框架

  • Fork/Join框架是一个用于并行执行任务的框架,是一个把大任务分隔成若干个小任务,最终汇总每个小任务结果后得到大任务结果的框架
  • 工作窃取算法:是指某个线程从其他队列里窃取任务拉执行。假如我们需要做一个比较大的任务,我们可以把这个任务分割为若干互不依赖的子任务,为了减少线程间的竞争,于是把这些子任务分别放到不同的队列里,并为每个队列创建一个单独的线程来执行队列里的任务,线程和队列一一对应,比如A线程负责处理A队列里的任务。但是有的线程会先把自己队列里的任务干完,而其他线程对应的队列里还有任务等待处理。干完活的线程与其等着,不如去帮其他线程干活,于是它就去其他线程的队列里窃取一个任务来执行。而在这时它们会访问同一个队列,所以为了减少窃取任务线程和被窃取任务线程之间的竞争,通常会使用双端队列,被窃取任务线程永远从双端队列的头部拿任务执行,而窃取任务的线程永远从双端队列的尾部拿任务执行。

    • 工作窃取算法的优点是充分利用线程进行并行计算,并减少了线程间的竞争,其缺点是在某些情况下还是存在竞争,比如双端队列里只有一个任务时。并且消耗了更多的系统资源,比如创建多个线程和多个双端队列。

Fork/Join框架的设计:

  步骤1:分割任务。首先需要有一个fork类来把大任务分割成子任务,有可能子任务还是很大,所以还需要不停地分割,直到分割出的子任务足够小。

  步骤2:执行任务并合并结果。分割的子任务分别放在双端队列里,然后几个启动线程分别从双端队列里获取任务执行。子任务执行完的结果都统一放在一个队列里,启动一个线程从队列里拿数据,然后合并这些数据。

  Fork/Join使用两个类来完成以上两件事情:

  • ForkJoinTask:我们要使用ForkJoin框架,必须首先创建一个ForkJoin任务。它提供在任务中执行fork()和join()操作的机制,通常情况下我们不需要直接继承ForkJoinTask类,而只需要继承它的子类,Fork/Join框架提供了以下两个子类:

    • RecursiveAction:用于没有返回结果的任务。
    • RecursiveTask :用于有返回结果的任务。
  • ForkJoinPool :ForkJoinTask需要通过ForkJoinPool来执行。
    • ForkJoinPool由ForkJoinTask数组和ForkJoinWorkerThread数组组成,ForkJoinTask数组负责将存放程序提交给ForkJoinPool的任务,而ForkJoinWorkerThread数组负责执行这些任务。

  任务分割出的子任务会添加到当前工作线程所维护的双端队列中,进入队列的头部。当一个工作线程的队列里暂时没有任务时,它会随机从其他工作线程的队列的尾部获取一个任务。

Fork/Join框架的异常处理:

  ForkJoinTask在执行的时候可能会抛出异常,但是我们没办法在主线程里直接捕获异常,所以ForkJoinTask提供了isCompletedAbnormally()方法来检查任务是否已经抛出异常或已经被取消了,并且可以通过ForkJoinTask的getException方法获取异常。其中,getException方法返回Throwable对象,如果任务被取消了则返回CancellationException,如果任务没有完成或者没有抛出异常则返回null。

第7章  Java中的13个原子操作类

  当程序更新一个变量时,如果多线程同时更新这个变量,可能得到期望值之外的值。通常我们使用sychronized来解决这个问题,sychronized会保证多线程不会同时更新同一个变量。

  而Java从JDK1.5开始提供了java.util.concurrent.atomic包,包中的原子操作类提供了一种用法简单、性能高效、线程安全地更新一个变量的方式。一共提供了13个类,属于4种类型的原子更新方式,分别是原子更新基本类型、原子更新数组、原子更新引用和原子更新属性(字段)。

(1)原子更新基本类型

  • AtomicBoolean :原子更新布尔类型
  • AtomicInteger: 原子更新整型
  • AtomicLong: 原子更新长整型

(2)原子更新数组

  • AtomicIntegerArray :原子更新整型数组里的元素
  • AtomicLongArray :原子更新长整型数组里的元素
  • AtomicReferenceArray : 原子更新引用类型数组的元素

(3)原子更新引用类型

  • AtomicReference :原子更新引用类型
  • AtomicReferenceFieldUpdater :原子更新引用类型里的字段
  • AtomicMarkableReference:原子更新带有标记位的引用类型。可以原子更新一个布尔类型的标记位和应用类型

(4)原子更新字段类

  • AtomicIntegerFieldUpdater:原子更新整型的字段的更新器
  • AtomicLongFieldUpdater:原子更新长整型字段的更新器
  • AtomicStampedReference:原子更新带有版本号的引用类型。该类将整型数值与引用关联起来,可用于原子的更新数据和数据的版本号,可以解决使用CAS进行原子更新时可能出现的ABA问题。

【注】要想原子地更新字段类需要2步。第一步:因为原子更新字段类都是抽象类,每次使用的时候必须使用静态方法newUpdate()创建一个更新器,并且需要设置想要更新的类和属性。第二步:更新类的字段(属性)必须使用public volatile修饰符。

第8章 Java中的并发工具类

(1)等待多线程完成的CountDownLatch

  CountDownLatch允许一个或多个线程等待其他线程完成操作

  要实现主线程等待所有线程完成某个操作,最简单的做法是使用join()方法。join()用于让当前执行线程等待join线程执行结束。其实现原理是不停检查join线程是否存活,如果join线程存活则让当前线程永远等待。直到join线程中止后,线程的this.notifyAll()方法会被调用。

  在JDK1.5之后的并发包中提供的CountDownLatch也可以实现join的功能。

  • CountDownLatch的构造函数接收一个int类型的参数作为计数器,如果你想等待N个点(N个线程)完成,就传入N。  static CountDownLatch c = new CountDownLatch(2);
  • 每次调用CountDownLatch的countDown方法时,N就减1,CountDownLatch的await方法会阻塞当前线程,直到N变成0。

(2)同步屏障CyclicBarrier

  CyclicBarrier的作用是:让一组线程到达一个屏障(也可以称之为同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才能继续运行。

  • CyclicBarrier(int parties)构造函数接收一个int参数用来设置拦截线程的数量,还有一个更高级的构造函数CyclicBarrier(int parties, Runnable barrierAction),用于在线程到达屏障时,优先执行barrierAction。  static CyclicBarrier c = new CyclicBarrier(2,new A);

CountDownLatch与CyclicBarrier的区别:

  CountDownLatch的计数器只能使用1次,而CyclicBarrier的计数器可以使用reset()方法重置。(所以CyclicBarrier能处理更为复杂的业务场景。例如,如果计算发生错误,可以重置计数器,并让线程重新执行1次。)

(3)控制并发线程数的Semaphore

  Semaphore(信号量)用来控制同时访问特定资源的线程数量,通过协调各个线程以保证合理的使用公共资源。

  应用场景:可以用于做流量控制,特别是共有资源有限的应用场景,比如数据库连接。

  • Semaphore(int permits)构造方法接收一个int参数,表示可用的许可证数量。
  • 每次线程使用Semaphore的acquire()方法获取一个许可证,用完后调用release()方法归还

(4)线程间交换数据的Exchanger

  Exchanger(交换者)是一个用于线程间协作的工具类。用于进行线程间的数据交换。 它提供一个同步点,在这个同步点,两个线程可以交换彼此的数据。这两个线程通过exchange方法交换数据,如果第一个线程先执行exchange()方法,它会一直等待第二个线程也执行exchange()方法,当两个线程到达同步点时,这两个线程就可以交换数据,将本线程生产出来的数据传递给对方。

  如果两个线程有一个没有执行exchange()方法,则会一直等待,如果担心有特殊情况发生,避免一直等待,可以使用exchange(V x, longtimeout , TimeUnit unit)设置最大等待时长。

《Java并发编程的艺术》第6/7/8章 Java并发容器与框架/13个原子操作/并发工具类的更多相关文章

  1. Java并发编程的艺术(一、二章) ——学习笔记

    第一章  并发编程的挑战 需要了解的一些概念 转自 https://blog.csdn.net/TzBugs/article/details/80921351 (1) 同步VS异步 同步和异步通常用来 ...

  2. Java 并发编程实践基础 读书笔记: 第一章 JAVA并发编程实践基础

    1.创建线程的方式: /** * StudySjms * <p> * Created by haozb on 2018/2/28. */ public class ThreadDemo e ...

  3. 读《Java并发编程的艺术》(一)

    离开博客园很久了,自从找到工作,到现在基本没有再写过博客了.在大学培养起来的写博客的习惯在慢慢的消失殆尽,感觉汗颜.所以现在要开始重新培养起这个习惯,定期写博客不仅是对自己学习知识的一种沉淀,更是在督 ...

  4. Java并发编程的艺术读书笔记(2)-并发编程模型

    title: Java并发编程的艺术读书笔记(2)-并发编程模型 date: 2017-05-05 23:37:20 tags: ['多线程','并发'] categories: 读书笔记 --- 1 ...

  5. Java并发编程的艺术读书笔记(1)-并发编程的挑战

    title: Java并发编程的艺术读书笔记(1)-并发编程的挑战 date: 2017-05-03 23:28:45 tags: ['多线程','并发'] categories: 读书笔记 --- ...

  6. 那些年读过的书《Java并发编程实战》和《Java并发编程的艺术》三、任务执行框架—Executor框架小结

    <Java并发编程实战>和<Java并发编程的艺术>           Executor框架小结 1.在线程中如何执行任务 (1)任务执行目标: 在正常负载情况下,服务器应用 ...

  7. Java并发编程的艺术(六)——线程间的通信

    多条线程之间有时需要数据交互,下面介绍五种线程间数据交互的方式,他们的使用场景各有不同. 1. volatile.synchronized关键字 PS:关于volatile的详细介绍请移步至:Java ...

  8. Java并发编程的艺术(三)——volatile

    1. 并发编程的两个关键问题 并发是让多个线程同时执行,若线程之间是独立的,那并发实现起来很简单,各自执行各自的就行:但往往多条线程之间需要共享数据,此时在并发编程过程中就不可避免要考虑两个问题:通信 ...

  9. java并发编程的艺术(一)---锁的基本属性

    本文来源于翁舒航的博客,点击即可跳转原文观看!!!(被转载或者拷贝走的内容可能缺失图片.视频等原文的内容) 若网站将链接屏蔽,可直接拷贝原文链接到地址栏跳转观看,原文链接:https://www.cn ...

随机推荐

  1. DevOps知识点——3C知多少

    CI / CD是任何DevOps操作的两大基石,这是一种开发软件的方式,旨在生产快速而强大的软件,随时以可持续的方式发布更新. 当例行更改代码时,开发周期会更加频繁.更有意义且更快速.通过此过程,我们 ...

  2. lin-cms-dotnetcore.是如何方法级别的权限控制的?

    方法级别的权限控制(API级别) Lin的定位在于实现一整套 CMS的解决方案,它是一个设计方案,提供了不同的后端,不同的前端,而且也支持不同的数据库 目前官方团队维护 lin-cms-vue,lin ...

  3. 写给程序员的机器学习入门 (五) - 递归模型 RNN,LSTM 与 GRU

    递归模型的应用场景 在前面的文章中我们看到的多层线性模型能处理的输入数量是固定的,如果一个模型能接收两个输入那么你就不能给它传一个或者三个.而有时候我们需要根据数量不一定的输入来预测输出,例如文本就是 ...

  4. 【书签】连续型特征的归一化和离散特征的one-hot编码

    1. 连续型特征的常用的归一化方法.离散型特征one-hot编码的意义 2. 度量特征之间的相关性:余弦相似度和皮尔逊相关系数

  5. Elasticsearch系列---生产集群部署(上)

    概要 本篇开始介绍Elasticsearch生产集群的搭建及相关参数的配置. ES集群的硬件特性 我们从开始编程就接触过各种各样的组件,而每种功能的组件,对硬件要求的特性都不太相同,有的需要很强的CP ...

  6. Hello, CTF

    0x01 拿到题目后查壳,发现什么也没有,32位vc++ 0x02 放到IDA里,F5反编译,得到下图 很容易我们就看到了比较的函数,以及出现wrong和success的字符串,所以接下来就是仔细分析 ...

  7. 02 . Mysql基础操作及增删改查

    SQL简介 SQL(Structured Query Language 即结构化查询语言) SQL语言主要用于存取数据.查询数据.更新数据和管理关系数据库系统,SQL语言由IBM开发. SQL语句四大 ...

  8. day07 作业

    作业(必做题):#1. 使用while循环输出1 2 3 4 5 6 8 9 10count=0while count<11: if count==7: count+=1 continue pr ...

  9. MySQL不香吗,清华架构师告诉你为什么还要有noSQL?

    强烈推荐观看: 阿里P8架构师谈(数据库系列):NoSQL使用场景和选型比较,以及与SQL的区别_哔哩哔哩 (゜-゜)つロ 干杯~-bilibili​www.bilibili.com noSQL的大概 ...

  10. Java实现 蓝桥杯 算法训练 出现次数最多的整数

    算法训练 出现次数最多的整数 时间限制:1.0s 内存限制:512.0MB 提交此题 问题描述 编写一个程序,读入一组整数,这组整数是按照从小到大的顺序排列的,它们的个数N也是由用户输入的,最多不会 ...