1.hashmap如果要保证同步,该如何实现
此方法,所以数组大小默认也为16,
但是在concurrenthashmap中
,第二个表示默认初始化负载因子0.75,第三个参数表示默认并发级别,也就是concurrenthashmap有多少段,默认16段。
public ConcurrentHashMap(int initialCapacity,
float loadFactor, int concurrencyLevel) {
if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
throw new IllegalArgumentException();
if (concurrencyLevel > MAX_SEGMENTS)
concurrencyLevel = MAX_SEGMENTS;
// Find power-of-two sizes best matching arguments
int sshift = 0;
int ssize = 1; // 1
while (ssize < concurrencyLevel) {
++sshift;
ssize <<= 1; // 2
}
this.segmentShift = 32 - sshift;
this.segmentMask = ssize - 1;
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
int c = initialCapacity / ssize;
if (c * ssize < initialCapacity)
++c;
int cap = MIN_SEGMENT_TABLE_CAPACITY; //11
while (cap < c) //22
cap <<= 1;
// create segments and segments[0]
Segment<K,V> s0 =
new Segment<K,V>(loadFactor, (int)(cap * loadFactor),
(HashEntry<K,V>[])new HashEntry[cap]); //33
Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize]; //3
UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
this.segments = ss;
}
查看1、2、3可以发现,segment的值是根据concurrencyLevel来设定的,ssize值为大于等于concurrencyLevel,并且是2的幂
查看11、22、33可以发现,cap值初始为2(MIN_SEGMENT_TABLE_CAPACITY值为2),查看22可以发现,cap的值和c有关。
而c的值是concurrentHashmap的初始化大小/段数组大小,如果有余数,则将c值再加1.例如假设初始化容量为33.段数组大小为16,33/16=2…1,那么c的值就为3.结合实际也可以容易分析出来。再回到22中,cap的值最终被设定为大于等于c的2的幂。所以cap的值为4。所以假设初始化容量为33.段数组大小为16时,每段中的数组大小为4.
看ConcurrentHashMap可以发现,它是数组的初始大小只设置了一个,如图所示为2,其他segment下的数组大小根据这个s0即第一个段大小设置为相同的值2.如果某一段下的数组满了并且来了一个新元素那么只需要将这段下的数组进行扩容就好了
unsafe方法
Java和C++语言的一个重要区别就是Java中我们无法直接操作一块内存区域,不能像C++中那样可以自己申请内存和释放内存。Java中的Unsafe类为我们提供了类似C++手动管理内存的能力。
Unsafe类,全限定名是sun.misc.Unsafe,从名字中我们可以看出来这个类对普通程序员来说是“危险”的,一般应用开发者不会用到这个类。
Unsafe类是"final"的,不允许继承。且构造函数是private的:
在上图倒数第三行中可以看到unsafe方法,
如上图可以看到它的get方法,首先会得到当前调用类的类加载器,如果是自己编写的java类,那么它的类加载器就是Application,应用程序类加载器;如果是外部java类,那么就是bootStrap,系统类加载器,在应用程序类它得到的是null,所以getUnsafe方法会区分当前调用的类是jdk中的类还是程序员自己编写的类,如果是程序员自己编写的,那么在获取unsafe对象的时候会抛出异常。因为unsafe方法的调用很容易导致一些问题,所以jdk不建议程序员调用unsafe方法,但是这个东东在jdk中用到的太多了,不说下不痛快!
那么如果想得到unsafe对象,怎么办?如下所示,通过反射来获取对象。
为什么用unsafe?
unsafe详解
我真的写吐了,每次写到一半,保存了第二天就没有了,我靠靠靠!!!!!!!!!!!!!!!!!!!!!!
真的是厕所里跳远------过分
ConcurrentHashMap的put方法
public V put(K key, V value) {
Segment<K,V> s;
if (value == null)
throw new NullPointerException();
int hash = hash(key);
int j = (hash >>> segmentShift) & segmentMask;
if ((s = (Segment<K,V>)UNSAFE.getObject // nonvolatile; recheck
(segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment
s = ensureSegment(j);
return s.put(key, hash, value, false);
}
put方法中Segement对象的生成及其位置
int j = (hash >>> segmentShift) & segmentMask;
其中segementShift的值在concurrentHashMap的构造方法可以看到
int sshift = 0;
int ssize = 1; // 1
while (ssize < concurrencyLevel) {
++sshift;
ssize <<= 1; // 2
}
this.segmentShift = 32 - sshift;
this.segmentMask = ssize - 1;
假设concurrencyLevel的大小为16,ssize大小为16时才会推出循环,此时sshift的大小为4,其实就是2^4=16中的4。segementShift的大小为32-4 = 28 。而segementMask的大小为15.
int j = (hash >>> segmentShift) & segmentMask;
hash >>> segmentShift 表示将得到的hash值右移28位,其实就是取最高的4位(int类型的hash值占32位)
例如:hash值为 1011 0100 0101 1111 1110 1100 0011 0101
右移之后得到的就是 0000 0000 0000 0000 0000 0000 0000 1011
和16与操作后 0000 0000 0000 0000 0000 0000 0000 1111
得到的值就是 0000 0000 0000 0000 0000 0000 0000 1011
即 j=11。
s = ensureSegment(j);如下
/**
* Returns the segment for the given index, creating it and
* recording in segment table (via CAS) if not already present.
*
* @param k the index
* @return the segment
*/
@SuppressWarnings("unchecked")
private Segment<K,V> ensureSegment(int k) {
final Segment<K,V>[] ss = this.segments;
long u = (k << SSHIFT) + SBASE; // raw offset
Segment<K,V> seg;
if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) {
Segment<K,V> proto = ss[0]; // use segment 0 as prototype
int cap = proto.table.length;
float lf = proto.loadFactor;
int threshold = (int)(cap * lf);
HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry[cap];
if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
== null) { // recheck
Segment<K,V> s = new Segment<K,V>(lf, threshold, tab);
while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
== null) {
if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))
break;
}
}
}
return seg;
}
可以看注释,它是返回一个segement数组下标为j的Segement对象。
Segement对象的同步
在多线程情况下,如何保证多个线程只会创建一个segement并且使用同一个segment。
k其实就是数组下标,(k << SSHIFT) + SBASE获取的值也是k,这样做的目的是保证在多线程同步,如果直接使用segement[j]来获取数组的第j个元素的话,这样做和单线程没啥区别,毕竟你不知道别的线程有没有创建这个segement对象。加锁能解决问题,但是这里concurrentHashMap采用了UnSafe方法(SBASE中的) ,也就是CAS的乐观锁方式来处理。这个比较难以理解,反正以后看到这种就当做k值来。
对于 CAS 系列方法的具体使用方法就不在这里赘述了,我们都知道该方法的作用是原子操作比较并交换两个值,运用的时底层硬件所提供的 CAS 支持,在 Java API 中我们可以看到该方法具有四个参数。
obj :包含要修改的字段对象;
offset :字段在对象内的偏移量;
expect : 字段的期望值;
update :如果该字段的值等于字段的期望值,用于更新字段的新值;
compareAndSwapObject 方法其实比较的就是两个 Java Object 的地址,如果相等则将新的地址(Java Object)赋给该字段。
保证segement对象的同步主要是使用CAS,在这里不在深究。
详细介绍如下:
源码解析 Java 的 compareAndSwapObject 到底比较的是什么?
回到上面,就比较容易理解了,就算线程1先执行但是没有创建一个segement对象,线程2后执行创建了对象,由于CAS的存在,线程1是会使用线程2的对象的。