码迷,mamicode.com
首页 > 编程语言 > 详细

Java容器(三)

时间:2019-08-18 19:29:16      阅读:97      评论:0      收藏:0      [点我收藏+]

标签:内存   code   c++   动态   java容器   actor   nal   步骤   线程并发   

一,在Java中有普通集合、同步(线程安全)集合、并发集合。

普通集合通用性能最高,但是不保证多线程的安全性和并发的可靠性。

线程安全集合仅仅是给集合加了synchronized同步锁,严重牺牲了性能,而且对并发的效率就更低了。

并发集合则通过复杂的策略不仅保证了多线程的安全由提高了并发的效率

 

并发集合常见有ConcurrentHashMap、ConcurrentLinkedQueue、ConcurrentLinkedDeque,并发集合位于java.util.concurrent包下,是jdk1.5之后才有的

 

二,ConcurrentHashMap 是线程安全的 HashMap 的实现,默认构造同样有 initialCapacity 和 loadFactor 属性,
不过还多了一个 concurrencyLevel 属性,三属性默认值分别为 16、0.75 及 16。其内部使用锁分段技术,维持这锁
Segment 的数组,在 Segment 数组中又存放着 Entity[]数组,内部 hash 算法将数据较均匀分布在不同锁中。
put 操作:

并没有在此方法上加上 synchronized,首先对 key.hashcode 进行 hash 操作,得到 key 的 hash 值。
hash操作的算法和map也不同,根据此hash值计算并获取其对应的数组中的Segment对象(继承自ReentrantLock),
接着调用此 Segment 对象的 put 方法来完成当前操作。
ConcurrentHashMap 基于 concurrencyLevel 划分出了多个 Segment 来对 key-value 进行存储,从而避免每
次 put 操作都得锁住整个数组。在默认的情况下,最佳情况下可允许 16 个线程并发无阻塞的操作集合对象,尽可能地
减少并发时的阻塞现象。
get(key):

首先对 key.hashCode 进行 hash 操作,基于其值找到对应的 Segment 对象,调用其 get 方法完成当前操
作。而 Segment 的 get 操作首先通过 hash 值和对象数组大小减 1 的值进行按位与操作来获取数组上对应位置的
HashEntry。在这个步骤中,可能会因为对象数组大小的改变,以及数组上对应位置的 HashEntry 产生不一致性,那
么 ConcurrentHashMap 是如何保证的?
对象数组大小的改变只有在 put 操作时有可能发生,由于 HashEntry 对象数组对应的变量是 volatile 类型
的,因此可以保证如 HashEntry 对象数组大小发生改变,读操作可看到最新的对象数组大小。
在获取到了 HashEntry 对象后,怎么能保证它及其 next 属性构成的链表上的对象不会改变呢?这点
ConcurrentHashMap 采用了一个简单的方式,即 HashEntry 对象中的 hash、key、next 属性都是 final 的,这也就
意味着没办法插入一个HashEntry对象到基于next属性构成的链表中间或末尾。这样就可以保证当获取到HashEntry
对象后,其基于 next 属性构建的链表是不会发生变化的。
ConcurrentHashMap 默认情况下采用将数据分为 16 个段进行存储,并且 16 个段分别持有各自不同的锁
Segment,锁仅用于 put 和 remove 等改变集合对象的操作,基于 volatile 及 HashEntry 链表的不变性实现了读取
的不加锁。这些方式使得 ConcurrentHashMap 能够保持极好的并发支持,尤其是对于读远比插入和删除频繁的 Map
而言,而它采用的这些方法也可谓是对于 Java 内存模型、并发机制深刻掌握的体现。

 

三,数组和链表

1,数组和链表简介

在计算机中要对给定的数据集进行若干处理,首要任务是把数据集的一部分(当数据量非常大时,可能只能一部
分一部分地读取数据到内存中来处理)或全部存储到内存中,然后再对内存中的数据进行各种处理。
例如,对于数据集 S{1,2,3,4,5,6},要求 S 中元素的和,首先要把数据存储到内存中,然后再将内存中的
数据相加。
当内存空间中有足够大的连续空间时,可以把数据连续的存放在内存中,各种编程语言中的数组一般都是按这种
方式存储的(也可能有例外),如图 1(b);当内存中只有一些离散的可用空间时,想连续存储数据就非常困难了,
这时能想到的一种解决方式是移动内存中的数据,把离散的空间聚集成连续的一块大空间,如图 1(c)所示,这样做
当然也可以,但是这种情况因为可能要移动别人的数据,所以会存在一些困难,移动的过程中也有可能会把一些别人
的重要数据给丢失。另外一种,不影响别人的数据存储方式是把数据集中的数据分开离散地存储到这些不连续空间中,
如图(d)。这时为了能把数据集中的所有数据联系起来,需要在前一块数据的存储空间中记录下一块数据的地址,这
样只要知道第一块内存空间的地址就能环环相扣地把数据集整体联系在一起了。C/C++中用指针实现的链表就是这种
存储形式。

由上可知,内存中的存储形式可以分为连续存储和离散存储两种。因此,数据的物理存储结构就有连续存储和离
散存储两种,它们对应了我们通常所说的数组和链表,

2,数组和链表的区别

数组是将元素在内存中连续存储的;它的优点:因为数据是连续存储的,内存地址连续,所以在查找数据的时候效
率比较高;它的缺点:在存储之前,我们需要申请一块连续的内存空间,并且在编译的时候就必须确定好它的空间的大
小。在运行的时候空间的大小是无法随着你的需要进行增加和减少而改变的,当数据两比较大的时候,有可能会出现
越界的情况,数据比较小的时候,又有可能会浪费掉内存空间。在改变数据个数时,增加、插入、删除数据效率比较低
链表是动态申请内存空间,不需要像数组需要提前申请好内存的大小,链表只需在用的时候申请就可以,根据需
要来动态申请或者删除内存空间,对于数据增加和删除以及插入比数组灵活。还有就是链表中数据在内存中可以在任
意的位置,通过应用来关联数据(就是通过存在元素的指针来联系)

3,数组和链表使用场景

数组应用场景:数据比较少;经常做的运算是按序号访问数据元素;数组更容易实现,任何高级语言都支持;构建
的线性表较稳定。
链表应用场景:对线性表的长度或者规模难以估计;频繁做插入删除操作;构建动态性比较强的线性表。

Java容器(三)

标签:内存   code   c++   动态   java容器   actor   nal   步骤   线程并发   

原文地址:https://www.cnblogs.com/noperx/p/11373438.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!