Java并发(四)Java并发安全容器和框架
ConcurrentHashMap和Hashtable的区别?
ConcurrentHashMap和Hashtable的区别主要体现在实现线程安全的方式上不同。 底层数据结构:
- JDK1.7的ConcurrentHashMap底层采用分段的数组+链表实现,JDK1.8采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树。
- Hashtable和JDK1.8之前的HashMap的底层数据结构类似都是采用数组+链表的形式,数组是HashMap的主体,链表侧是主要为了解决哈希冲突而存在的;
实现线程安全的方式(重要):
- 在JDK1.7的时候,ConcurrentHashMap对整个桶数组进行了分割分段(Segment,分段锁),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。
- 到了JDK1.8的时候,ConcurrentHashMap已经摒弃了Segment的概念,而是直接用Node数组+链表+红黑树的数据结构来实现,并发控制使用synchronized和CAS来操作。(JDK1.6以后synchronized锁做了很多优化)整个看起来就像是优化过且线程安全的HashMap,虽然在JDK1.8中还能看到Segment的数据结构,但是已经简化了属性,只是为了兼容旧版本;
- Hashtable(同一把锁):使用synchronized来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用put添加元素,另一个线程不能使用put添加元素,也不能使用get,竞争会越来越激烈效率越低。
ConcurrentHashMap JDK1.7实现的原理是什么?
首先将数据分为一段一段(这个"段”就是Segment)的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。
ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成。
- Segment继承了ReentrantLock,所以Segment是一种可重入锁,扮演锁的角色。
- HashEntry用于存储键值对数据。一个ConcurrentHashMap里包含一个Segment数组,Segment的个数一旦初始化就不能改
- Segment数组的大小默认是16,也就是说默认可以同时支持16个线程并发写。
- Segment的结构和HashMap类似,是一种数组和链表结构,一个Segment包含一个HashEntry数组,每个HashEntry是一个链表结构的元素,每个Segment守护着一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得对应的Segment的锁。也就是说,对同一Segment的并发写入会被阻塞,不同Segment的写入是可以并发执行的。
ConcurrentHashMap JDK1.8实现的原理是什么?
JDK1.8 ConcurrentHashMap取消了Segment分段锁,采用Node+CAS+synchronized来保证并发安全(CAS失败说明有并发就用synchronized)。
数据结构跟HashMap1.8的结构类似,数组+链表/红黑二叉树。
Java8在链表长度超过一定阈值(8)时将链表(寻址时间复杂度为O(N))转换为红黑树(寻址时间复杂度为O(Iog(N))。
Java8中,锁粒度更细,synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲突,就不会产生并发,就不会影响其他Node的读写,效率大幅提升。
ConcurrentHashMap JDK1.7的实现和1.8的实现有什么区别
线程安全实现方式:
- JDK1.7采用Segment分段锁来保证安全,Segment是继承自ReentrantLock
- JDK1.8放弃了Segment分段锁的设计,采用Node+CAS+synchronized保证线程安全,锁粒度更细,synchronized只锁定当前链表或红黑二叉树的首节点
Hāsh碰撞解决方法:
- JDK1.7采用拉链法
- JDK1.8采用拉链法结合红黑树(链表长度超过一定阈值时,将涟表转换为红黑树)
并发度:
- JDK1.7最大并发度是Segment的个数,默认是16
- JDK1.8最大并发度是Node数组的大小,并发度更大
JDK1.8中,ConcurrentHashmap的put过程是怎样的?
整体流程跟HashMap比较类似,大致是以下几步:
- 如果桶数组未初始化,则初始化:
- 如果待插入的元素所在的桶为空,则尝试把此元素直接插入到桶的第一个位置;
- 如果正在扩容,则当前线程一起加入到扩容的过程中;
- 如果待插入的元素所在的桶不为空且没在迁移元素,则锁住这个桶;
- 如果当前桶中元素以链表方式存储,则在链表中寻找该元素或者插入元素;
- 如果当前桶中元素以红黑树方式存储,则在红黑树中寻找该元素或者插入元素;
- 如果元素存在,则覆盖旧值;
- 如果元素不存在,整个Map的元素个数加1,并检查是否需要扩容。
ConcurrentHashMap的get方法是否要加锁,为什么?
不需要。get方法不涉及对变量的修改,所有会导致并发下可能出问题的原因就是读共享变量的可见性问题。
而ConcurrentHashMap中,对get方法中用到的共享变量都使用volatile关键字修饰,所以整个get方法不加锁也不会有问题。
ConcurrentHashMap默认初始容量是多少?
初始容量为16
ConCurrentHashmap的key,value是否可以为null?
不行。
如果key或者value为null会抛出空指针异常。(原因是因为没办法解决get返回值为null时的二义性问题,即没办法确定是存储的值本身为null,还是说值不存在); 注意:HashMap允许使用null作为值和键。(因为HashMap只能单线程下使用,所以hashmap可以用containsKey来二次判断,排除二义性问题)
存储在ConcurrentHashmap中每个节点是什么样的,有哪些变量?
它是实现Map.Entry<K,V>接口。里面存放了hash,key,value,以及next节点。
它的value和next节点是用volatile进行修饰,可以保证多线程之间的可见性。
BlockingQueue阻塞队列
什么是BlockingQueue?
阻塞队列(BlockingQueue)是一个支持阻塞的插入和移除操作的队列。
- 支持阻塞的插入方法:意思是当队列满时,队列会阻塞插入元素的线程,直到队列不满
- 支持阻塞的移除方法:意思是在队列为空时,获取元素的线程会等待队列变为非空
Java中的阻塞队列有哪些
- ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列
- LinkedBlockingQueue:一个由链表结构组成的阻塞队列。此队列创建时可以不指定容量大小,默认是Integer.MAX_VALUE,也就是无界的。但也可以指定队列大小,从而成为有界的。
- PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列。默认情况下元素采取自然顺序升序排列。也可以自定义类实现compareTo()方法来指定元素排序规则。
- DelayQueue:一个使用优先级队列实现的无界阻塞队列。队列使用PriorityQueue:来实现。队列中的元素必须实现Delayed接口,在创建元素时可以指定多久才能从队列中获取当前元素。只有在延迟期满时才能从队列中提取元素。(常用在缓存有效期,定时任务调度等场景)
- SynchronousQueue:一个不存储元素的阻塞队列。每一个put操作必须等待一个take操作,否则不能继续添加元素。队列本身并不存储任何元素,非常适合传递性场景。
- LinkedTransferQueue:一个由链表结构组成的单向无界阻塞队列。它设计了一种直接在生产者和消费者之间传输元素的机制,称为transfer'"。当生产者调用transfer(e)方法时,它会阻塞直到有一个消费者接收该元素。适用于需要高效地在生产者和消费者之间直接传输数据的场景,尤其是当生产者和消费者之间的速度大致匹配时
- LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。所谓双向队列指的是可以从队列的两端插入和移出元素。相比其他的阻塞队列,LinkedBlockingDeque多了addFirst、addLast、offer-First、offerLast、peekFirst和peekLasts等方法。双向阻塞队列可以运用在"工作窃取"模式中
ArrayBlockingQueue和LinkedBlockingQueue有什么区别?
ArrayBlockingQueue和LinkedBlockingQueue是Java并发包中常用的两种阻塞队列实现,它们都是线程安全的。不过,不过它们之间也存在下面这些区别:
- 底层实现:ArrayBlockingQueue基于数组实现,而LinkedBlockingQueue基于链表实现。
- 是否有界:ArrayBlockingQueue是有界队列,必须在创建时指定容量大小。LinkedBlockingQueue创建时可以不指定容量大小,默认是Integer.MAX_VALUE,也就是无界的。但也可以指定队列大小,从而成为有界的。
- 锁是否分离:ArrayBlockingQueuet中的锁是没有分离的,即生产和消费用的是同一个锁;LinkedBlockingQueue中的锁是分离的,即生产用的是putLock,消费是takeLock,这样可以防止生产者和消费者线程之间的锁争夺。
- 内存占用:ArrayBlockingQueue需要提前分配数组内存,而LinkedBlockingQueue则是动态分配链表节点内存。这意味着,ArrayBlockingQueue在创建时就会占用一定的内存空间,且往往申请的内存比实际所用的内存更大,而LinkedBlockingQueue则是根据元素的增加而逐渐占用内存空间。
如果队列是空的,消费者会一直等待,当生产者添加元素时,消费者是如何知道当前队列有元素的呢?
使用通知模式实现。
所谓通知模式:
- 当消费者从空的队列获取元素时会阻塞住消费者,此时如果生产者放了个元素进入队列,则需要通知阻塞住消费者当前有元素可取
- 同理当生产者往满的队列里添加元素时会阻塞住生产者,当消费者消费了一个队列中的元素后,会通知生产者当前队列可用
- 通过查看JDK源码发现部分阻塞队列使用了Condition来实现