最近听到一些同事在谈论java内存堆栈的事情,突发奇想的想看看自己平时用的java对象的底层实现和jvm如何管理他们的,原谅我现在才想起去看这些,应该前几年就看的,以下也纯粹是个人理解
项目中最常用的数据结构是Map
首先,Map是一个接口
这里主要讲HashMap,ConcurrentHashMap,HashTable
这几个主要平时用到的对象
1.HashMap
一个以键值对数组为存储的对象
他首先是一个数组,数组里面的元素是键值对Map.Entry
存的时候put方法
首先判断key是不是为空,为空则始终将null放在数组的第一个位置上
后执行key的hash值,再然后根据hash值计算应该存在数组的哪个位置上,如果位置上已经有其他的对象,将原来的对象往数组后面移,新的对象放在位子上,如果位置上没有对象,则直接存储
如果key的hash值相同,会执行keys.equals方法,如果相等,则覆盖,如果不相等,由于HashMap存储对象的时候是由LinkList来存储,所以会将对象放在LinkList的下一个节点上
在计算应该存储在数组的哪个位置上时,HashMap对key的hashcode进行了二次hash,然后再对数组的长度进行了取模运算,而HashTable是直接将key的hashcode对数组的长度进行取模运算,没又HashMap的二次hash过程
取得时候get
同样的判断key是不是为空,如果为空,则直接取数组的第一个位置
后面对key进行hash,计算存在数组的哪个位置
如果hash值相同,则会执行keys.equals方法,取出链表中的对象
HashMap的初始值为16,HashTable的初始长度为11,负载因子都是0.75,就是当存储长度超过总长度的0.75时,会自动执行rfush方法,扩充长度,扩充的长度为初始值
数组在内存中存的地方是堆,由JVM的GC来进行回收,链表存储的地方是栈,当超出了作用域自动删除
参考HashMap的存的实现,发现不可变的String和int这种基本数据类型比较适合做为HashMap的key,当然引用的对象也可以作为key,但是需要符合hashcode唯一的特性
2.ConcurrentHashMap和HashTable
ConcurrentHashMap和HashTable同为java中的并发容器
ConcurrentHashMap是HashMap的并发实现,但是ConcurrentHashMap的key和value都不能为null
ConcurrentHashMap除了继承了HashMap的父类AbstracMap之外还实现了ConcurrentMap接口
他的putIfAbsent方法当不存在key对应的值时将value作为key存入Map中
ConcurrentHashMap的核心就在于他默认情况下是用了16个类似HashMap 的结构,其中每一个HashMap拥有一个独占锁。也就是说最终的效果就是通过某种Hash算法,将任何一个元素均匀的映射到某个HashMap的Map.Entry上面,而对某个一个元素的操作就集中在其分布的HashMap上,与其它HashMap无关。这样就支持最多16个并发的写操作 我们把这16个类似于HashMap的操作叫做segment
首先看put操作
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());
int binCount = 0;
for (Node
Node
if (tab == null || (n = tab.length) == 0)
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null,
new Node
break; // no lock when adding to empty bin
}
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
synchronized (f) {
if (tabAt(tab, i) == f) {
if (fh >= 0) {
binCount = 1;
for (Node
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node
if ((e = e.next) == null) {
pred.next = new Node
value, null);
break;
}
}
}
else if (f instanceof TreeBin) {
Node
binCount = 2;
if ((p = ((TreeBin
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
首先就需要加锁了,同步下是要加锁的,这个毫无疑问。但是需要注意的是是Segment集成的是ReentrantLock,所以这里加的锁也就是独占锁,也就是说同一个Segment在同一时刻只有能一个put操作。
接下来来就是检查是否需要扩容,这和HashMap一样,如果需要的话就扩大一倍,同时进行rehash操作
其次是get方法
public V get(Object key) {
Node
int h = spread(key.hashCode());
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
if ((eh = e.hash) == h) {
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
else if (eh < 0)
return (p = e.find(h, key)) != null ? p.val : null;
while ((e = e.next) != null) {
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}
首先定位到segment,后面的定位其实就是和HashMap是一样的
HashTable的put
public synchronized V put(K key, V value) {
// Make sure the value is not null
if (value == null) {
throw new NullPointerException();
}
// Makes sure the key is not already in the hashtable.
Entry,?> tab[] = table;
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
@SuppressWarnings("unchecked")
Entry
for(; entry != null ; entry = entry.next) {
if ((entry.hash == hash) && entry.key.equals(key)) {
V old = entry.value;
entry.value = value;
return old;
}
}
addEntry(hash, key, value, index);
return null;
}
HashTable的get
@SuppressWarnings("unchecked")
public synchronized V get(Object key) {
Entry,?> tab[] = table;
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
for (Entry,?> e = tab[index] ; e != null ; e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
return (V)e.value;
}
}
return null;
}
所以从底层的代码来说
Hashtable的可伸缩性的主要障碍是它使用了一个 Map 范围的锁,为了保证插入、删除或者检索操作的完整性必须保持这样一个锁,而且有时候甚至还要为了保证迭代遍历操作的完整性保持这样一个锁。这样一来,只要锁被保持,就从根本上阻止了其他线程访问 Map,即使处理器有空闲也不能访问,这样大大地限制了并发性