Java并发编程的艺术5之Java并发容器和框架

1 ConcurrentHashMap的实现原理与使用

Concurrent是线程安全且高效的HashMap。因为在并发编程中HashMap可能导致程序死循环,而线程安全的HashTable效率非常低下,所以产生了ConcurrentHashMap。

1.1.HashMap和HashTable

  1. 线程不安全的HashMap
    1. HashMap在并发执行put操作时会引起死循环,因为多线程会导致HashMap的Entry链表形成环形数据结构,一旦形成环形数据结构,Entry的next节点永远不为空,就会产生死循环获取Entry。
  2. 效率低下的HashTable
    1. HashTable容器使用synchronized来保证线程安全,当线程竞争激励时,效率低下。因为当多个线程访问HashTable的同步方法时,会进入轮询或阻塞状态。如线程1使用put进行元素添加,线程2不但不能使用put方法添加元素,也不能使用get方法来获取元素。
  3. ConcurrentHashMap的锁分段激素可有效提升并发访问率
    1. HashTable慢是有那位所有线程都必须竞争同一把锁,假如容器里有多把锁,每一把锁都用于锁容器中其中一部分数据,那么当多线程访问容器里不同数据段数据时,线程间就不会存在锁竞争,从而可以有效提高并发访问效率,这就是ConcurrentHashMap的锁分段技术。首先将数据分成一段一段的存储,然后给每一段数据配一把锁。

1.2.ConcurrentHashMap的结构

由Segment数组结构和HashEntry数组结构组成。Segment是可重入锁(ReentrantLock),HashEntry用于储存键值对数据。

segment结构与HashMap结构类似,是一种数组和链表的结构。一个Segmant里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素。当对一个HashEntry数组中的数据进行修改时,必须首先获取与它对应的Segment锁。

1.3.ConcurrentHashMap的初始化

通过几个参数:initialCapacity,LoadFactor和concurrencyLevel等来初始化segment数组、偏移量segmentShift、段掩码segmentMask和每个segment里的HashEntry数组实现ConcurrentHashMap的初始化。

3.1 初始化Segments数组

为了能够通过按位与的散列算法来定位segment数组的索引,必须保证segments数组的长度是2的N次方。

3.2 初始化segmentShift和segmentMask

这两个是全局变量,需要在定位segment时的散列算法里使用。segmentShift用于定位参与散列运算的位数,segmentMask是散列运算的掩码。

3.3 初始化每个segment

initialCapacity是ConcurrentHashMap的初始化容量,LoadFactor是每个segment的负载因子,构造方法通过这两个参数来初始化每个segment。

1.4 定位segment

在插入和获取元素的时候,必须先通过散列算法定位到segment。

1.5 ConcurrentHashMap的操作

  • 1.get操作

先经过一次再散列,然后使用这个散列值通过散列运算定位到segment,再通过散列算法定位到元素。

高效之处在于整个过程不需要加锁,除非读到空值才会加锁重读。它的get方法将每个使用的共享变量都定位为volatile类型,能够保证在线程之间保持可见性,能够被多线程同时读,并且保证不回读到过期的值,但是只能被单线程写,在get操作中只需要读不需要写共享变量count和value,所以可以不用加锁。之所以不会读到过期的值,是因为根据Java内存模型的happen before 原则,对volatile字段的写入操作咸鱼读操作,即使两个线程同时修改和获取volatile变量,get操作也能拿到最新的值,这个月哦那个与volatile替换锁的经典场景。

  • 2.put操作

因为put方法里需要对共享变量进行写入操作,所以为了线程安全,在操作共享变量时必须加锁。put方法首先定位到Segment,然后在segment里进行插入操作。插入操作需要经历两个步骤:

第一:判断是否需要对segment里的hashentry数组进行扩容

是否需要扩容 ?在插入之前判断Segment的HashEntry数组是否超过容量,如果超过,则扩容。

如何扩容?  首先创建一个容量是原来两倍的数组,然后将原数组里的元素进行再散列后插入到新的数组里。为了高效,ConcurrentHashMap不会对整个容器进行扩容,只会对某个segment进行扩容。

第二:定位添加元素的位置,然后将其放在hashEntry数组里。

  • 3.size操作

统计ConcurrentHashMap里元素的大小,最安全的做法是再统计size的时候把所有的put、remove、clean方法锁住,但是十分低效。现在的做法是:先尝试2次不锁住segment的方式来统计各个segment的大小,如果统计过程中,modCount的大小发生了变化,则再采用加锁的方式来 统计所有segment的大小。

2 ConcurrentLinkedQueue

实现线程安全的队列有两种方法:一种是通过阻塞算法,一种是非阻塞算法。阻塞通过锁,非阻塞通过循环CAS算法。

ConcurrentLinkedQueue是一个基于链接节点的无界线程安全队列,它采用先进先出的规则对节点进行排序,当我们添加一个元素的时候,他会添加到队列的尾部;当我么获取一个元素的时候,它会返回队列头部的元素。

2.1 ConcurrentLinkedQueue的结构

ConcurrentLinkedQueue由节点head和tail节点组成

2.2 入队列

2.2.1 入队列的过程

入队列就是将入队节点添加到队列的尾部。 多线程中,如果有线程插队,那么队列的尾节点就会发生变化,这时当前线程要暂停入队操作,重新获取尾节点。

2.2.2 定位尾节点

tail节点并不总是尾节点,为节点可能是tail节点,也可能是tail节点的next节点。获取tail节点的next节点需要注意的是p节点等于p的next节点的情况,只可能是他们都为空,表示这个队列刚刚初始化,正准备添加节点,所以需要返回head节点。

2.2.3 设置入队节点为尾节点

p.casNext(null,n)将入队节点设置为当前队列尾节点的next节点,如果p是null,表示p是当前队列的尾节点,如果不为null,表示其他线程更新了尾节点,则需要重新获取尾节点。

2.2.4 HOPS的设计示意图

使用常量HOPS=1来控制并减少tail节点的更新频率,并不是每次节点如对后都将tail节点更新成尾节点,而是当太了解点和尾节点的距离大于等于HOPS时,才更新tail节点,tail和尾节点的距离越长,使用CAS更新tail节点的次数越少,但是负效果是每次入队时定位尾节点的时间就越长,因为循环体需要多循环一次来定位出尾节点。但仍旧能够提升效率,因为从本质上来说,它通过增加对volatile变量的读操作来减少对volatile变量的写操作,而对volatile写的开销比读的开销大得多,所以还是会提升效率。

2.3 出队列

出队列就是从队列里返回一个节点元素,并清空该节点对元素的引用。当head节点里面有元素时,直接弹出元素,而不是更新。只有当head节点里没有元素时,出队操作才会更新head节点。这种做法也是通过hops变量来减少使用CAS更新head节点的消耗。

首先获取头节点的元素,然后判断头节点元素是否为空,如果为空,表示另外一个县城已经进行了一次出队操作将该节点的元素取走,如果不为空,则使用CAS将头节点的引用设置为null,如果CAS成功,则直接返回头节点的元素,如果不成功,说明另一个线程进行了一次出队操作更新了头节点,导致元素发生了变化,需要重新获取头节点。

3.Java中的阻塞队列

阻塞队列是一个支持两个附加操作的队列。包括支持阻塞的插入和移除方法。阻塞队列常用于生产者和消费者的情景,生产者是向队列添加元素的线程,消费者是从队列取元素的线程。

抛出异常:当队列满时,如果再次插入元素,会抛出IllegalStateException('Queue full')异常,当队列空时,从队列获取元素会抛出NoSUchElemetnException异常。

超时退出:当阻塞队列满时,队列会阻塞生产者一段时间,如果超过了指定的时间,生产者线程就会退出。

3.2 Java的阻塞队列

1. ArrayBlockQueue 一个由数组结构组成的有界阻塞队列。 支持FIFO原则,默认不公平。

2.LinkedBlockQueue 一个链表实现的有界阻塞队列 FIFO 默认长度Integer.Max_Value

3.PriorityBlockingQueue 支持优先级的无界阻塞队列。默认升序排列

4.DelayQueue 支持延时获取元素的无界阻塞队列。使用PriorityQueue实现,队列中的元素必须实现Delayed接口,在创建元素时

可以指定多久才能从队列中获取当前元素。

  应用场景:缓存系统的设计,定时任务调度。

  (1)Delayed接口的实现  一共有三步:1.在对象创建的时候初始化基本数据 2.实现getDelay()方法 3.实现CompareTo方法来指定元素的顺序。

 (2)实现延时阻塞队列 

5.SynchronousQueue  不存储元素的阻塞队列。每一个put操作必须等待一个take操作,否则不能添加元素。非常适合传递性场景。

6.LinkedTransferQueue 由链表结构组成的无界阻塞队列,相比于其他阻塞队列多了tryTransfer和transfer方法。

        (1)transfer方法

如果当前由消费者正在等待接受元素(消费者使用take()方法或带有时间限制的poll()方法)时,transfer方法可以把生产者传入的元素立刻transfer给消费者 。如果没有消费者在等待接收元素,transfer方法会将元素存放在队列的tail节点,并等到元素被消费者消费了才返回。

        (2)tryTransfer方法

tryTransfer用来试探生产者传入的元素是否能直接传给消费者。如果没有消费者等待接收元素,则返回false。和transfer方法的区别是tryTransfer方法无论消费者是否接受,方法立刻返回,ttansfer方法是必须等到消费者消费了才返回。

7.LinkedBlockingQueue 是一个由链表结构组成的双向阻塞队列。双向队列指的是可以从队列的两端插入和移出元素,因为多了一个操作队列的入口,在多线程同时入队时,也就减少了一半的竞争。在初始化次方法时可以设置容量防止其过度膨胀。双向阻塞队列可以运用在‘工作窃取’模式中

3.3 阻塞队列的实现原理

使用通知模式实现

4 Fork/Join框架

Fork/Join框架时Java7 提供的一个用于执行并行任务的框架,是一个把大人物分割为若干小任务,最后ui总每个小任务结果后得到大任务结果的框架。

4.2 工作窃取算法

工作窃取时指某个线程从其他队列里窃取任务来执行。为了减少窃取任务线程和被窃取任务线程之间的竞争,通常会使用双端队列,被窃取任务永远从双端队列的头部拿任务执行,而窃取任务的线程永远从双端队列的尾部拿任务执行。

优点:充分利用线程进行并行计算,减少了线程的竞争。

缺点:在某些情况下还是存在警长,比如双端队列里只有一个任务时。这种算法会消耗更多的系统资源,比如创建多个线程和多个双端队列。

4.3 Fork/Join框架的设计

步骤1 :分割任务

步骤2:执行任务并合并结果

ForkJoinTask:要使用ForkJoin框架,首先要创建一个ForkJoin任务。它提供在任务中执行fork和join操作的机制。通常情况下,不需要直接继承ForkJoinTask类,只需要继承它的子类。RecursiveAction类:用于没有返回结果的任务;RecursiveTask:用于有返回结果的任务。

FrokJoinPool:ForkJoinTask需要通过FrokJoinPool来执行。

4.4 使用Fork/Join框架

4.5 异常处理 使用isCompletedAbnormally()方法来检查任务是否已经抛出异常或已经被取消了。通过ForkJoinTask的getException方法获取异常。

4.6 实现原理

FrokJoinPool由ForkJoinTask数组和ForkJoinWorkerThread数组组成。ForkJoinTask数组负责将存放程序提交给FrokJoinPool的任务,ForkJoinWorkerThread负责执行这些任务。

(1)ForkJoinTask的fork方法实现原理

当我们调用ForkJoinTask方法时,程序会调用ForkJoinWorkerThread的pushTask方法异步执行任务,然后立刻返回结果。pushTask方法把当前任务存放在ForkJoinTask数组队列中,然后再调用ForkJoinPool的signalWork()方法唤醒或创建一个工作线程来执行任务。

(1)ForkJoinTask的join方法实现原理

Join方法的主要作用是阻塞当前线程并等待获取结果。它首先调用doJoin方法,得到当前任务的状态来判断返回什么结果。

  • 如果任务的状态是已完成(NORMAL),则直接返回任务结果。
  • 如果任务状态是被取消(CANCELLED),则直接抛出CancellationException。
  • 如果任务状态是抛出异常(EXCEPTION),则直接抛出对应异常。
  • 如果任务状态是信号(SIGNAL)

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值