既然,hashMap是线程不安全的,我们怎么解决它的并发安全问题呢?
这里我们可以看一下HashTable的解决思路
hashTable是在整个方法上加了一个锁,锁的是当前对象。
那像hashTable这种做法,对当前map整个实例对象去加一把锁的话,并发安全问题肯定是解决了,但是,效率就非常非常低了。
假如,现在有两个线程,使用同一个hashtable对象,向里面put元素,如果一个元素放在1位置,而另一个元素放在3位置。
但是,这两个线程,总归是有一个先获取到锁的,而另一个线程,虽然想将元素放在这个数组的不同位置,但是,由于第一个线程还没有释放锁,它就必须等待着锁的释放,才能进行put操作。
所以,就造成效率非常非常低。为了解决这个问题,这时候concurrentHashMap就出现了。
二、ConcurrentHashMap 2.1 HashMap基本结构对于,hashTable,讲道理,我put元素时,放的位置都不一样,很明细就不冲突嘛,你为什么就不允许我放进去呢?这效率就非常非常低下
所以,segment里面hashEntry数组的长度是算出来的,总的hashEntry[].length / segement[].length = 每一个segement里面的hashEntry[].length\
对于concurrentHashMap的put操作,也是要通过key算出segment数组的下标。
对于算下标其实也是通过key.hashCode & segment[].length 的方式来计算的,
既然需要通过这个方式计算,那么它肯定也是需要segment数组的长度为2的幂次方,那它是怎么做的呢?我们继续来看它的构造方法
构造方法里面会生成一个segment[]数组,以及一个segment对象s0,放在数组的第一个位置,其他位置都是null
2.4 并发安全问题concurrentHashMap是怎么保证线程安全的呢,我们先举一个线程不安全的例子
我们上面之所以会重复,就是因为每个线程都是用自己工作内存里面的数据造成的,假如,我们不让他用自己工作内存里的数据,直接让他们用主物理内存的数据,是不是就不会重复了
2.4.1 UNSAFE类concurrentHashMap里面运用到CAS的思想,来保证并发安全问题
public class Person {
private int i = 0;
private static sun.misc.Unsafe UNSAFE; //UNSAFE类
private static long I_OFFSET; //偏移量
//Person类的类加载器是ApplicationContextClassLoader 不为null,ConcurrentHashMap的类加载器是BootstrapClassLoader null
static {
try {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
UNSAFE = (Unsafe) field.get(null);
//获取Person类,i字段的偏移量,也即是获取i的内存地址
I_OFFSET = UNSAFE.objectFieldOffset(Person.class.getDeclaredField("i"));
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
}
//两个线程对同一个person.i去加加,重复了肯定是线程不安全的,因为正常情况下,一个线程进行加1操作,肯定是逐渐增大的,不会发生重复的现象
public static void main(String[] args) {
final Person person = new Person();
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
//person.i ++;
/**
* person: 当前对象
* I_OFFSET: 内存偏移量,即内存地址
* person.i: 用peron和I_OFFSET找出的主内存的真实值
* person.i + 1: 要更新的新值
* 当前该对象内存的值,与主物理内存的值是否一样,如果一样,把值更新为person.i+1,返回true。
* 这个过程是一个原子过程
*/
boolean b = UNSAFE.compareAndSwapInt(person, I_OFFSET, person.i, person.i + 1);
//读也要读当前主物理内存最新的值
if (b) {
System.out.println(UNSAFE.getIntVolatile(person, I_OFFSET));
}
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}, "t1").start();
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
//person.i ++;
boolean b = UNSAFE.compareAndSwapInt(person, I_OFFSET, person.i, person.i + 1);
//System.out.println(person.i);
if (b) {
System.out.println(UNSAFE.getIntVolatile(person, I_OFFSET));
}
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}, "t2").start();
}
}
ConcurrentHashMap通过CAS,保证多个线程并发操作,只有一个线程能够修改成功,其他线程都会修改失败,不需要锁,就能保证并发安全。
怎么通过UNSAFE方法,修改数组里面的属性呢?
1.基本思路
put的是segment对象
2.concurrentHashMap,key不能为null
3.segmentMask
4.SSHFT
5.segmentShift
6.ensureSegment
设计模式之prototype原型模式。
put的是HashEntry[]
1.reentrantLock
采用ReentrantLock保证线程安全
tryLock()和lock的区别 tryLock:不阻塞,尝试获取锁,如果获取到锁,立马返回true,如果不能获取到立马返回false,不会阻塞。
lock:会阻塞等待,如果能获取到锁,就执行业务逻辑。如果不能获取到锁,就会阻塞等待,直到能获取到锁为止。
2. entryAt
3.setEntryAt
4.scanAndLockForPut(key,hash,value)
5.rehash扩容
视频教程