在多线程开发中,我们经常要考虑线程并发的问题,那么如何来避免线程并发代码的数据读写问题呢?
我们常见的HashMap、TreeMap、LinkedList、ArrayList都是线程不安全的,而Java也提供了一些线程安全的容器类:
如:
各种并发容器:CocurrentHashMap、CopyOnWriteArrayLis等;
各种线程安全队列(Queue/Deque):ArrayBlockingQueque、SynchronousQueue等;
各种有序容器的线程安全版本等。
下面我们就来说一说CocurrentHashMap和CopyOnWriteArrayList是如何实现高效线程安全的?
记得当初在刚学习java时,遇到可能存在并发情况时,如数据库的读写时,只要简单的加个synchronized关键字即可,那么并发的问题就解决了。但是这是最低效的并发方式的处理,也就是不管三七二十一,set和get方法都给加个synchronized就完事了。那么怎样才能实现高效并发呢?下面来看下CocurrentHashMap源码关于高效并发问题的解决方案,不讨论所有源码,仅涉及线程安全的相关源码。
不了解volatile关键字的可以看我收藏的这篇:https://blog.csdn.net/fwt336/article/details/80986409,对volatile 说的很详细。
而在线程并发中,我们就需要用到volatile 的可见特性,来保证并发操作变量的可见性,而对于volatile 的非原子操作,我们可以看CocurrentHashMap是怎么做的。
下面来看看CocurrentHashMap源码中关于volatile的应用:
我们都知道,在源码中是通过这个table数组来保存我们存入Map中的key和value值的:
transient volatile Node[] table;
而Node是才是真正对我们的key和value值的封装:
static class Node implements Map.Entry {
final int hash;
final K key;
volatile V val;
volatile Node next;
Node(int hash, K key, V val, Node next) {
this.hash = hash;
this.key = key;
this.val = val;
this.next = next;
}
public final K getKey() { return key; }
public final V getValue() { return val; }
public final int hashCode() { return key.hashCode() ^ val.hashCode(); }
public final String toString() {
return Helpers.mapEntryToString(key, val);
}
public final V setValue(V value) {
throw new UnsupportedOperationException();
}
...
}
而在Node中有一个next,如果你想问为啥会有一个next节点在这里呢?那就说明你对Map的存储细节不够熟悉,这里与HashMap是类似的。简单总结下就是,Map虽然是通过数组table来存储数据,但是在table数组中的Node节点,确是通过链表来实现的,因为在存储的时候会发生hash碰撞,但是不同key可能通过hash换算后所对应table数组的index是一样的,所以在数组中同一个index的值会有多个key和value存在,那么我们通过链表就可以接近这个问题,这也就是为什么HashMap不是有序存储的了。而由于数据量太大时,链表的查找性能问题就会很明显了,这时候会对链表进行树化,来优化性能,而树化用的是红黑树。
扯远了,回来=======================================================================
我们看到源码中的val和next都用volatile修饰了,而且在Node的源码中我们也没有看到synchronized这个同步关键字。我们看到get方法也是不需要synchronized关键字的,因为有了volatile的可见性来保证数据的可见性操作。那么它的原子操作又是在哪里实现的呢?
现在我们知道通过使用volatile修饰val和next之后,get方法是不需要synchronized来修饰的,这样性能就得到了一定的提升。
那么涉及到修改,肯定是在set方法了:
public V put(K key, V value) {
return putVal(key, value, false);
}
/** Implementation for put and putIfAbsent */
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[] tab = table;;) {
Node f; int n, i, fh;
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(hash, key, value, null)))
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 e = f;; ++binCount) {
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 pred = e;
if ((e = e.next) == null) {
pred.next = new Node(hash, key,
value, null);
break;
}
}
}
else if (f instanceof TreeBin) {
Node p;
binCount = 2;
if ((p = ((TreeBin)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
else if (f instanceof ReservationNode)
throw new IllegalStateException("Recursive update");
}
}
...
}
}
addCount(1L, binCount);
return null;
}
我们看到synchronize同步了变量f这个节点,也就是我们需要操作的value。比起我们直接同步一个方法,性能也会大大提高。也就是我们在设置节点,替换节点,清除节点等对value操作时,只需要同步我们操作的这个节点即可。
同样的,CopyOnWriteArrayList也使用了volatile来保证可见性,synchronized进行同步,看数组定义:
private transient volatile Object[] elements;
不同的是:
final transient Object lock = new Object();
还有一个这玩意,lock,没错:
public E set(int index, E element) {
synchronized (lock) {
Object[] elements = getArray();
...
return oldValue;
}
}
public boolean add(E e) {
synchronized (lock) {
...
return true;
}
}
private boolean remove(Object o, Object[] snapshot, int index) {
synchronized (lock) {
...
return true;
}
}
...
在CocurrentHashMap中,synchronized同步的是当前需要操作的Node节点,而这里使用的是一个Object类实例来作为锁的对象,所有涉及到对elements数组的操作都需要先获取这把锁。这样也就达到了线程同步的作用。
所以,从Java提供的线程安全类的源码来看,实现高效并发的方式有:
1.对可变字段使用volatile修饰
2.getXX方法不需要加synchronized关键字
3.涉及到变量的所有修改操作,对需要操作的变量使用synchronized关键字进行同步,或定义一个Object实例充当锁,进行同步