Java的同步容器和并发容器
前言:
之前在介绍Java集合的时候说到,java提供的实现类很少是线程安全的。只有几个比较古老的类,比如Vector、Hashtable等是线程安全的,尤其是Hashtable,古老到连命名规范都没统一了……
同步容器:
1)Vector和Hashtable
来简单比较下:
Hashtable和Vector实现同步的方式也很类似,都是使用synchronized关键字,利用java的内置锁来实现。那些新增的线程不安全的,如果实现线程安全呢?
public class Hashtable<K,V>
extends Dictionary<K,V>
implements Map<K,V>, Cloneable, java.io.Serializable {
public synchronized int size() {
...
}
public synchronized V get(Object key) {
...
}
2)synchronized.XXX
介绍一个工具类Collections,其中对各种类型的集合进行了封装,实现了线程安全。
以其中的SynchronizedList为例,可以看到也是用的synchronized关键字来实现的。
注意:SynchronizedList并不是对某个实现类的封装,而面对的是List接口。
static class SynchronizedList<E>
extends SynchronizedCollection<E>
implements List<E> {
private static final long serialVersionUID = -7754090372962971524L;
final List<E> list;
SynchronizedList(List<E> list) {
super(list);
this.list = list;
}
SynchronizedList(List<E> list, Object mutex) {
super(list, mutex);
this.list = list;
}
public boolean equals(Object o) {
if (this == o)
return true;
synchronized (mutex) {return list.equals(o);}
}
public int hashCode() {
synchronized (mutex) {return list.hashCode();}
}
public E get(int index) {
synchronized (mutex) {return list.get(index);}
}
public E set(int index, E element) {
synchronized (mutex) {return list.set(index, element);}
}
public void add(int index, E element) {
synchronized (mutex) {list.add(index, element);}
}
public E remove(int index) {
synchronized (mutex) {return list.remove(index);}
}
}
无论是哪一种同步容器,在加锁限制时,都将同步代码块限制在了一个业务合理的最小粒度上。比如:SynchronizedList的get、set、remove等。
首先,需要承认的是这样做是相当合理的。但选择就意味着有利有弊,他不好的地方就是,如果程序需要稍微复杂点的操作,获取最后一个元素或者删除最后一个元素等等。这种情况下,我们就无法再指望人家能给你保证原子性了,就需要你客户端加锁了。
并发容器:
由于同步粒度控制的很小,这就使得容器中的方法都变成串行执行,虽然缓解了高并发,但也使得执行效率降低。高性能、高效率一直是我们追求的功能,为了解决这一点,引出了“并发容器”。
常用的有:ConcurrentHashMap、ConcurrentSkipListMap
并发容器替换同步容器,用很小的风险换得了可扩展性的提高。
在介绍ConcurrentHashMap实现之前,先来了解一下分离锁。
锁分段
如果一个锁中竞争很激烈,我们可以把他拆成两个锁。这样的话,可以通过两个线程并发执行,效果会好一点,但并不明显。
如果把拆分后的所扩展一下,分成可大可小的加锁块的集合,并且他们归属相互独立的对象,这就是我们说的锁分段。
ConcurrentHashMap
ConcurrentHashMap中主要实体类就是三个:ConcurrentHashMap(整个Hash表),Segment(桶),Node(节点)。(之前的版本中Node叫做 Hash Entry,一个意思。)
每个segment可能包含若干个table数组,每个table也都包含了若干的Node组成的链表。
如图:
ConcurrentHashMap使用了一个包含16个锁的Array,每个锁都守护Hash Bucket的十六分之一。更有利于并发访问,减少了锁的请求。
因为不是主要介绍源码,这里就简单的看几个重点的地方:
1、一些重要的变量常量
英文注释说的挺清楚的,就不翻译了,省得翻译还错了。
/**
* The default initial table capacity. Must be a power of 2
* (i.e., at least 1) and at most MAXIMUM_CAPACITY.
*/
private static final int DEFAULT_CAPACITY = 16;
/**
* The array of bins. Lazily initialized upon first insertion.
* Size is always a power of two. Accessed directly by iterators.
*/
transient volatile Node<K,V>[] table;
/**
* The next table to use; non-null only while resizing.
*/
private transient volatile Node<K,V>[] nextTable;
/**
* Base counter value, used mainly when there is no contention,
* but also as a fallback during table initialization
* races. Updated via CAS.
*/
private transient volatile long baseCount;
2、Segment
继承ReentrantLock 类,说明Segment是可以实现加锁的。
static class Segment<K,V> extends ReentrantLock implements Serializable {
private static final long serialVersionUID = 2249069246763182397L;
final float loadFactor;
Segment(float lf) { this.loadFactor = lf; }
}
3、Node类
实现了Map.entry,重写了一个equals和find方法。
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
volatile V val;
volatile Node<K,V> next;
Node(int hash, K key, V val, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.val = val;
this.next = next;
}
public final boolean equals(Object o) {
Object k, v, u; Map.Entry<?,?> e;
return ((o instanceof Map.Entry) &&
(k = (e = (Map.Entry<?,?>)o).getKey()) != null &&
(v = e.getValue()) != null &&
(k == key || k.equals(key)) &&
(v == (u = val) || v.equals(u)));
}
/**
* Virtualized support for map.get(); overridden in subclasses.
*/
Node<K,V> find(int h, Object k) {
Node<K,V> e = this;
if (k != null) {
do {
K ek;
if (e.hash == h &&
((ek = e.key) == k || (ek != null && k.equals(ek))))
return e;
} while ((e = e.next) != null);
}
return null;
}
}
注意:这里使用了volatile关键字,这也是实现线程同步的一种方式,一般而言,可以起到加锁的作用。
4、put()
介绍了这么多,说说他加锁的地方吧。在首次插入的时候,使用cas来保持同步,其他insert、delete、replace等需要使用锁来同步。
CAS : Compare And Swap,一种常用的复合操作。
以put为例:
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<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
//延迟初始化
if (tab == null || (n = tab.length) == 0)
tab = initTable();
//首次put添加,不使用锁,而是CAS操作
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
//不是首次put,需要加锁,注意Synchronized锁住的对象时f,即Node
V oldVal = null;
synchronized (f) {
if (tabAt(tab, i) == f) {
if (fh >= 0) {
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
//如果已经存在key值,替换掉value
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K,V> pred = e;
//如果不存在key值,创建一个Node
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key,
value, null);
break;
}
}
}
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
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;
}
不足之处:
正因为锁细化的太好了,如果你要想独占互斥访问的话,那就很难的,付出的成本是很高的。
为了稍稍改变一下这方面被动的局势,ConcurrentHashMap对一些常用的复合操作进行了封装,比如:“相等便替换”,“没有就添加”等等,如果使用同步集合,需要程序员自己加锁控制,这里就直接给写好了。
感受:
随着jdk不断更新,java一直在探索中,不断完善和改进。比如java2对集合的变动,java5对线程的扩展等等。在改进的过程中,也在不断参考C、C++更重语言的精华思想。当然,在借鉴的同时,他要保持java这门语言的设计原则和初衷。如果二者出现冲突,那么他肯定是不会借鉴的,或者借鉴一个皮毛的东西。
行了,不扯了。其实就是想说一点,如果之前的设计很不错,那么后期就不会进行特别大的完善,都是修修补补,类似改个bug什么的。同理,如果后面进行了很大的补充或者修改,那么之前的缺点肯定是多到不行了。新的设计也许有缺点,但相比之前,一定是利大于弊。
因此,如果想知道自己对一个知识点掌握的如何时,不妨试试能不能把他的演变历史讲出来,如果又蒙又猜,自己能说服自己的话,就很差不多了,剩下的就是去看书查阅资料验证自己的猜想吧!
Java的同步容器和并发容器的更多相关文章
- Java并发——同步容器与并发容器
同步容器类 早期版本的JDK提供的同步容器类为Vector和Hashtable,JDK1.2 提供了Collections.synchronizedXxx等工程方法,将普通的容器继续包装.对每个共有方 ...
- Java并发—同步容器和并发容器
简述同步容器与并发容器 在Java并发编程中,经常听到同步容器.并发容器之说,那什么是同步容器与并发容器呢?同步容器可以简单地理解为通过synchronized来实现同步的容器,比如Vector.Ha ...
- java多线程总结-同步容器与并发容器的对比与介绍
1 容器集简单介绍 java.util包下面的容器集主要有两种,一种是Collection接口下面的List和Set,一种是Map, 大致结构如下: Collection List LinkedLis ...
- JAVA同步容器和并发容器
同步容器类 同步容器类的创建 在早期的JDK中,有两种现成的实现,Vector和Hashtable,可以直接new对象获取: 在JDK1.2中,引入了同步封装类,可以由Collections.sync ...
- 【Java并发编程二】同步容器和并发容器
一.同步容器 在Java中,同步容器包括两个部分,一个是vector和HashTable,查看vector.HashTable的实现代码,可以看到这些容器实现线程安全的方式就是将它们的状态封装起来,并 ...
- Java并发编程原理与实战三十三:同步容器与并发容器
1.什么叫容器? ----->数组,对象,集合等等都是容器. 2.什么叫同步容器? ----->Vector,ArrayList,HashMap等等. 3.在多线程环境下,为什么不 ...
- java 普通容器,同步容器,并发容器,同步工具
同步容器,如HashTable,提供独占访问. 并发容器,ConcurrentHashMap,有着更好的并发性能,但是不能独占访问. --putIfAbsent 同步工具: 闭锁:CountDownL ...
- Java并发(9)- 从同步容器到并发容器
引言 容器是Java基础类库中使用频率最高的一部分,Java集合包中提供了大量的容器类来帮组我们简化开发,我前面的文章中对Java集合包中的关键容器进行过一个系列的分析,但这些集合类都是非线程安全的, ...
- Java线程同步类容器和并发容器(四)
同步类容器都是线程安全的,在某些场景下,需要枷锁保护符合操作,最经典ConcurrentModifiicationException,原因是当容器迭代的过程中,被并发的修改了内容. for (Iter ...
随机推荐
- 开发的服务集群部署方案,以etcd为基础(java)
当前有很多服务集群部署,但是对于我们自己开发的服务系统怎么样能够解决部署问题,对大家很麻烦和笨重. 首先,我想说对于我们国内,小公司小系统比较多.大型系统毕竟少数,向阿里云看齐的不多.其实所谓的需要集 ...
- linux 执行程序时,提示not found问题分析
sh: ./test: not found 通常可以通过readelf查看该进程文件所以依赖的运行环境,检查相关路径是否存在对应的文件. 比如如下: 1. 检查/lib目录,发现ld-X.XX.so为 ...
- C++中vector,set,map自定义排序
一.vector排序 vector支持cmp,就类似数组,可以直接sort. #include <iostream> #include <algorithm> #include ...
- idea中注解配置一对多,多对一,双向多对一映射(不详细)
一对多 package cn.pojo; import javax.persistence.*; import java.io.Serializable; import java.util.Set; ...
- jquery点击li 获取当前父节点所在类的索引
jquery点击li 获取当前父节点所在类的索引 $('.jbcz').find('.content li').click(function(){ //alert($('.jbcz').find('. ...
- IDEA开发vue.js卡死问题
在执行cnpm install后会在node_modules这个文件下面生成vue的相关依赖文件, 这个时候当执行cnpm run dev命令时,会导致IDEA出现卡死的问题,解决方法如下:
- django的查询集
查询集表示从数据库中获取的对象集合,在管理器上调用某些过滤器方法会返回查询集,查询集可以含有零个.一个或多个过滤器.过滤器基于所给的参数限制查询的结果,从Sql的角度,查询集和select语句等价,过 ...
- Python学习 :深浅拷贝
深浅拷贝 一.浅拷贝 只拷贝第一层数据(不可变的数据类型),并创建新的内存空间进行储蓄,例如:字符串.整型.布尔 除了字符串以及整型,复杂的数据类型都使用一个共享的内存空间,例如:列表 列表使用的是同 ...
- Matplotlib 基本图表的绘制
图表类别:线形图.柱状图.密度图,以横纵坐标两个维度为主 同时可延展出多种其他图表样式 plt.plot(kind='line', ax=None, figsize=None, use_index=T ...
- .Net 面试题 汇总(二)
51..net中读写XML的类都归属于哪些命名空间? 答:System.Xml 52.解释一下UDDI.WSDL的意义及其作用. 答:UDDI即统一描述.发现和集成协议.作用: 用来说明一个Web服务 ...