Java同步容器和并发容器详解

同步容器

在 Java 中,同步容器主要包括 2 类:

  • Vector、Stack、HashTableCollections 类中提供的静态工厂方法创建的类(由 Collections.synchronizedXxxx 等方法)
  • Collections类中提供的静态工厂方法创建的类

Vector 实现了 List 接口,Vector 实际上就是一个数组,和 ArrayList 类似,但是Vector 中的方法都是 synchronized 方法,即进行了同步措施。

Stack 也是一个同步容器,它的方法也用 synchronized 进行了同步,它实际上是继承于 Vector 类。

HashTable 实现了 Map 接口,它和 HashMap 很相似,但是 HashTable 进行了同步处理,而 HashMap 没有。

同步容器的缺陷

同步容器的同步原理就是在方法上用 synchronized 修饰。那么,这些方法每次只允许一个线程调用执行。

性能问题

由于被 synchronized 修饰的方法,每次只允许一个线程执行,其他试图访问这个方法的线程只能等待。显然,这种方式比没有使用 synchronized 的容器性能要差。

安全问题

同步容器真的一定安全吗?

答案是:未必。同步容器未必真的安全。在做复合操作时,仍然需要加锁来保护。

常见复合操作如下:

  • 迭代:反复访问元素,直到遍历完全部元素;
  • 跳转:根据指定顺序寻找当前元素的下一个(下 n 个)元素;
  • 条件运算:例如若没有则添加等;

不安全的示例

<pre style="margin: 0px; padding: 0px; white-space: pre-wrap; overflow-wrap: break-word; font-family: "Courier New" !important; font-size: 12px !important;">public class Test { static Vector<Integer> vector = new Vector<Integer>();
public static void main(String[] args) throws InterruptedException {
while(true) {
for (int i=0;i<10;i++)
vector.add(i);
Thread thread1 = new Thread(){
public void run() {
for (int i=0;i<vector.size();i++)
vector.remove(i);
}
;
}
;
Thread thread2 = new Thread(){
public void run() {
for (int i=0;i<vector.size();i++)
vector.get(i);
}
;
}
;
thread1.start();
thread2.start();
while(Thread.activeCount()>10) {
}
}
}
}
</pre>

执行时可能会出现数组越界错误。

Vector 是线程安全的,为什么还会报这个错?很简单,对于 Vector,虽然能保证每一个时刻只能有一个线程访问它,但是不排除这种可能:
当某个线程在某个时刻执行这句时:

<pre style="margin: 0px; padding: 0px; white-space: pre-wrap; overflow-wrap: break-word; font-family: "Courier New" !important; font-size: 12px !important;">for (int i=0;i<vector.size();i++)
vector.get(i);
</pre>

假若此时 vector 的 size 方法返回的是 10,i 的值为 9

然后另外一个线程执行了这句:

<pre style="margin: 0px; padding: 0px; white-space: pre-wrap; overflow-wrap: break-word; font-family: "Courier New" !important; font-size: 12px !important;">for (int i=0;i<vector.size();i++)
vector.remove(i);
</pre>

将下标为 9 的元素删除了。

那么通过 get 方法访问下标为 9 的元素肯定就会出问题了。

安全示例

因此为了保证线程安全,必须在方法调用端做额外的同步措施,如下面所示:

public class Test {
static Vector<Integer> vector = new Vector<Integer>();
public static void main(String[] args) throws InterruptedException {
while(true) {
for (int i=0;i<10;i++)
vector.add(i);
Thread thread1 = new Thread(){
public void run() {
synchronized (Test.class) {
//进行额外的同步
for (int i=0;i<vector.size();i++)
vector.remove(i);
}
}
;
}
;
Thread thread2 = new Thread(){
public void run() {
synchronized (Test.class) {
for (int i=0;i<vector.size();i++)
vector.get(i);
}
}
;
}
;
thread1.start();
thread2.start();
while(Thread.activeCount()>10) {
}
}
}
}

ConcurrentModificationException 异常

在对 Vector 等容器并发地进行迭代修改时,会报 ConcurrentModificationException 异常,关于这个异常将会在后续文章中讲述。

但是在并发容器中不会出现这个问题。

并发容器

JDK 的 java.util.concurrent 包(即 juc)中提供了几个非常有用的并发容器。

  • CopyOnWriteArrayList - 线程安全的 ArrayList
  • CopyOnWriteArraySet - 线程安全的 Set,它内部包含了一个 CopyOnWriteArrayList,因此本质上是由 CopyOnWriteArrayList 实现的。
  • ConcurrentSkipListSet - 相当于线程安全的 TreeSet。它是有序的 Set。它由 ConcurrentSkipListMap 实现。
  • ConcurrentHashMap - 线程安全的 HashMap。采用分段锁实现高效并发。
  • ConcurrentSkipListMap - 线程安全的有序 Map。使用跳表实现高效并发。
  • ConcurrentLinkedQueue - 线程安全的无界队列。底层采用单链表。支持 FIFO。
  • ConcurrentLinkedDeque - 线程安全的无界双端队列。底层采用双向链表。支持 FIFO 和 FILO。
  • ArrayBlockingQueue - 数组实现的阻塞队列。
  • LinkedBlockingQueue - 链表实现的阻塞队列。
  • LinkedBlockingDeque - 双向链表实现的双端阻塞队列。

ConcurrentHashMap

要点

作用:ConcurrentHashMap 是线程安全的 HashMap。
原理:JDK6 与 JDK7 中,ConcurrentHashMap 采用了分段锁机制。JDK8 中,摒弃了锁分段机制,改为利用 CAS 算法。

源码

JDK7

ConcurrentHashMap 类在 jdk1.7 中的设计,其基本结构如图所示:

每一个 segment 都是一个 HashEntry<K,V>[] table, table 中的每一个元素本质上都是一个 HashEntry 的单向队列。比如 table[3]为首节点,table[3]->next 为节点 1,之后为节点 2,依次类推。

<pre style="margin: 0px; padding: 0px; white-space: pre-wrap; overflow-wrap: break-word; font-family: "Courier New" !important; font-size: 12px !important;">public class ConcurrentHashMap<K, V> extends AbstractMap<K, V>
implements ConcurrentMap<K, V>, Serializable { // 将整个hashmap分成几个小的map,每个segment都是一个锁;与hashtable相比,这么设计的目的是对于put, remove等操作,可以减少并发冲突,对 // 不属于同一个片段的节点可以并发操作,大大提高了性能
final Segment<K,V>[] segments;
// 本质上Segment类就是一个小的hashmap,里面table数组存储了各个节点的数据,继承了ReentrantLock, 可以作为互拆锁使用
static final class Segment<K,V> extends ReentrantLock implements Serializable {
transient volatile HashEntry<K,V>[] table;
transient int count;
}
// 基本节点,存储Key, Value值
static final class HashEntry<K,V> {
final int hash;
final K key;
volatile V value;
volatile HashEntry<K,V> next;
}
}
</pre>

JDK8

jdk8 中主要做了 2 方面的改进

  • 取消 segments 字段,直接采用 transient volatile HashEntry<K,V>[] table 保存数据,采用 table 数组元素作为锁,从而实现了对每一行数据进行加锁,进一步减少并发冲突的概率。
  • 将原先 table 数组+单向链表的数据结构,变更为 table 数组+单向链表+红黑树的结构。

对于 hash 表来说,最核心的能力在于将 key hash 之后能均匀的分布在数组中。如果 hash 之后散列的很均匀,那么 table 数组中的每个队列长度主要为 0 或者 1。

但实际情况并非总是如此理想,虽然 ConcurrentHashMap 类默认的加载因子为 0.75,但是在数据量过大或者运气不佳的情况下,还是会存在一些队列长度过长的情况,如果还是采用单向列表方式,那么查询某个节点的时间复杂度为 O(n);

因此,对于个数超过 8(默认值)的列表,jdk1.8 中采用了红黑树的结构,那么查询的时间复杂度可以降低到 O(logN),可以改进性能。

<pre style="margin: 0px; padding: 0px; white-space: pre-wrap; overflow-wrap: break-word; font-family: "Courier New" !important; font-size: 12px !important;">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;
// 如果table为空,初始化;否则,根据hash值计算得到数组索引i,如果tab[i]为空,直接新建节点Node即可。注:tab[i]实质为链表或者红黑树的首节点。
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<K,V>(hash, key, value, null))) break;
// no lock when adding to empty bin
}
// 如果tab[i]不为空并且hash值为MOVED,说明该链表正在进行transfer操作,返回扩容完成后的table。 else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f); else {
V oldVal = null;
// 针对首个节点进行加锁操作,而不是segment,进一步减少线程冲突
synchronized (f) {
if (tabAt(tab, i) == f) {
if (fh >= 0) {
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
// 如果在链表中找到值为key的节点e,直接设置e.val = value即可。
if (e.hash == hash && ((ek = e.key) == key || (ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
// 如果没有找到值为key的节点,直接新建Node并加入链表即可。
Node<K,V> pred = e;
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key,
value, null);
break;
}
}
}
// 如果首节点为TreeBin类型,说明为红黑树结构,执行putTreeVal操作。 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) {
// 如果节点数>=8,那么转换链表结构为红黑树结构。
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null) return oldVal;
break;
}
}
}
// 计数增加1,有可能触发transfer操作(扩容)。
addCount(1L, binCount);
return null;
}
</pre>

示例

<pre style="margin: 0px; padding: 0px; white-space: pre-wrap; overflow-wrap: break-word; font-family: "Courier New" !important; font-size: 12px !important;">public class ConcurrentHashMapDemo { public static void main(String[] args) throws InterruptedException { // HashMap 在并发迭代访问时会抛出 ConcurrentModificationException 异常 // Map<Integer, Character> map = new HashMap<>();
Map<Integer, Character> map = new ConcurrentHashMap<>();
Thread wthread = new Thread(() -> {
System.out.println("写操作线程开始执行"); for (int i = 0; i < 26; i++) {
map.put(i, (char) ('a' + i));
}
});
Thread rthread = new Thread(() -> {
System.out.println("读操作线程开始执行"); for (Integer key : map.keySet()) {
System.out.println(key + " - " + map.get(key));
}
});
wthread.start();
rthread.start();
Thread.sleep(1000);
}
}</pre>

CopyOnWriteArrayList

要点

作用:CopyOnWrite 字面意思为写入时复制。CopyOnWriteArrayList 是线程安全的 ArrayList。

原理:

  • 在 CopyOnWriteAarrayList 中,读操作不同步,因为它们在内部数组的快照上工作,所以多个迭代器可以同时遍历而不会相互阻塞(1,2,4)。
  • 所有的写操作都是同步的。他们在备份数组(3)的副本上工作。写操作完成后,后备阵列将被替换为复制的阵列,并释放锁定。支持数组变得易变,所以替换数组的调用是原子(5)。
  • 写操作后创建的迭代器将能够看到修改的结构(6,7)。
  • 写时复制集合返回的迭代器不会抛出 ConcurrentModificationException,因为它们在数组的快照上工作,并且无论后续的修改(2,4)如何,都会像迭代器创建时那样完全返回元素。

源码

重要属性

  • lock - 执行写时复制操作,需要使用可重入锁加锁
  • array - 对象数组,用于存放元素
<pre style="margin: 0px; padding: 0px; white-space: pre-wrap; overflow-wrap: break-word; font-family: "Courier New" !important; font-size: 12px !important;">
/** The lock protecting all mutators */
final transient ReentrantLock lock = new ReentrantLock();
/** The array, accessed only via getArray/setArray. */
private transient volatile Object[] array;
</pre>

重要方法

添加操作

添加的逻辑很简单,先将原容器 copy 一份,然后在新副本上执行写操作,之后再切换引用。当然此过程是要加锁的。

<pre style="margin: 0px; padding: 0px; white-space: pre-wrap; overflow-wrap: break-word; font-family: "Courier New" !important; font-size: 12px !important;">public Boolean add(E e) {
//ReentrantLock加锁,保证线程安全
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
//拷贝原容器,长度为原容器长度加一
Object[] newElements = Arrays.copyOf(elements, len + 1);
//在新副本上执行添加操作
newElements[len] = e;
//将原容器引用指向新副本
setArray(newElements);
return true;
}
finally {
//解锁
lock.unlock();
}
}
</pre>

删除操作

删除操作同理,将除要删除元素之外的其他元素拷贝到新副本中,然后切换引用,将原容器引用指向新副本。同属写操作,需要加锁。

<pre style="margin: 0px; padding: 0px; white-space: pre-wrap; overflow-wrap: break-word; font-family: "Courier New" !important; font-size: 12px !important;">public E remove(int index) {
//加锁
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
E oldValue = get(elements, index);
int numMoved = len - index - 1;
if (numMoved == 0) //如果要删除的是列表末端数据,拷贝前len-1个数据到新副本上,再切换引用
setArray(Arrays.copyOf(elements, len - 1)); else {
//否则,将除要删除元素之外的其他元素拷贝到新副本中,并切换引用
Object[] newElements = new Object[len - 1];
System.arraycopy(elements, 0, newElements, 0, index);
System.arraycopy(elements, index + 1, newElements, index,
numMoved);
setArray(newElements);
}
return oldValue;
}
finally {
//解锁
lock.unlock();
}
}
</pre>

读操作

CopyOnWriteArrayList 的读操作是不用加锁的,性能很高。

<pre style="margin: 0px; padding: 0px; white-space: pre-wrap; overflow-wrap: break-word; font-family: "Courier New" !important; font-size: 12px !important;">public E get(int index) {
return get(getArray(), index);
}
private E get(Object[] a, int index) {
return (E) a[index];
}
</pre>

示例

<pre style="margin: 0px; padding: 0px; white-space: pre-wrap; overflow-wrap: break-word; font-family: "Courier New" !important; font-size: 12px !important;">public class CopyOnWriteArrayListDemo { static class ReadTask implements Runnable {
List<String> list;
ReadTask(List<String> list) {
this.list = list;
}
public void run() {
for (String str : list) {
System.out.println(str);
}
}
}
static class WriteTask implements Runnable {
List<String> list;
int index;
WriteTask(List<String> list, int index) {
this.list = list;
this.index = index;
}
public void run() {
list.remove(index);
list.add(index, "write_" + index);
}
}
public void run() {
final int NUM = 10;
// ArrayList 在并发迭代访问时会抛出 ConcurrentModificationException 异常 // List<String> list = new ArrayList<>();
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
for (int i = 0; i < NUM; i++) {
list.add("main_" + i);
}
ExecutorService executorService = Executors.newFixedThreadPool(NUM);
for (int i = 0; i < NUM; i++) {
executorService.execute(new ReadTask(list));
executorService.execute(new WriteTask(list, i));
}
executorService.shutdown();
}
public static void main(String[] args) {
new CopyOnWriteArrayListDemo().run();
}
}
</pre>

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • Java中同步与并发用法分析

    本文较为详细的分析了Java中同步与并发的用法.分享给大家供大家参考.具体分析如下: 1.同步容器类包括两部分:vector和hashtable 另一类是同步包装类,由Collections.synchronizedXXX创建.同步容器对容器的所有状态进行串行访问,从而实现线程安全. 它们存在如下问题: a) 对于符合操作,需要额外的锁保护.比如迭代,缺少则添加等条件运算. b) toString,hashCode,equals都会间接的调用迭代,都需要注意并发.   2.java5.0中的并发

  • 浅谈Java并发 J.U.C之AQS:CLH同步队列

    CLH同步队列是一个FIFO双向队列,AQS依赖它来完成同步状态的管理,当前线程如果获取同步状态失败时,AQS则会将当前线程已经等待状态等信息构造成一个节点(Node)并将其加入到CLH同步队列,同时会阻塞当前线程,当同步状态释放时,会把首节点唤醒(公平锁),使其再次尝试获取同步状态. 在CLH同步队列中,一个节点表示一个线程,它保存着线程的引用(thread).状态(waitStatus).前驱节点(prev).后继节点(next),其定义如下: static final class Node

  • java并发编程之同步器代码示例

    同步器是一些使线程能够等待另一个线程的对象,允许它们协调动作.最常用的同步器是CountDownLatch和Semaphore,不常用的是Barrier和Exchanger 队列同步器AbstractQueuedSynchronizer是用来构建锁或者其他同步组件的基础框架,它内部使用了一个volatiole修饰的int类型的成员变量state来表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作. 同步器的主要使用方式是继承,子类通过继承同步器并实现它的抽象方法来管理同步状态,在抽

  • 详解Java利用同步块synchronized()保证并发安全

    本文实例为大家分享了Java利用同步块synchronized()保证并发安全的具体代码,供大家参考,具体内容如下 package day10; /** * 同步块 * 有效地缩小同步范围 * 可以在保证并发安全的同时尽可能提高并发效率 * * 实例:模拟两个人同时进店买衣服,为提高效率 * 只在试衣服阶段进行同步排队过程,其他阶段无需排队. * @author kaixu * */ public class SyncDemo2 { public static void main(String[

  • Java并发之传统线程同步通信技术代码详解

    本文研究的主要是Java并发之传统线程同步通信技术的相关代码示例,具体介绍如下. 先看一个问题: 有两个线程,子线程先执行10次,然后主线程执行5次,然后再切换到子线程执行10,再主线程执行5次--如此往返执行50次. 看完这个问题,很明显要用到线程间的通信了, 先分析一下思路:首先肯定要有两个线程,然后每个线程中肯定有个50次的循环,因为每个线程都要往返执行任务50次,主线程的任务是执行5次,子线程的任务是执行10次.线程间通信技术主要用到wait()方法和notify()方法.wait()方

  • Java从同步容器到并发容器的操作过程

    引言 容器是Java基础类库中使用频率最高的一部分,Java集合包中提供了大量的容器类来帮组我们简化开发,我前面的文章中对Java集合包中的关键容器进行过一个系列的分析,但这些集合类都是非线程安全的,即在多线程的环境下,都需要其他额外的手段来保证数据的正确性,最简单的就是通过synchronized关键字将所有使用到非线程安全的容器代码全部同步执行.这种方式虽然可以达到线程安全的目的,但存在几个明显的问题:首先编码上存在一定的复杂性,相关的代码段都需要添加锁.其次这种一刀切的做法在高并发情况下性

  • Java并发编程之同步容器与并发容器详解

    一.同步容器  1.Vector-->ArrayList vector 是线程(Thread)同步(Synchronized)的,所以它也是线程安全的: Arraylist是线程异步(ASynchronized)的,是不安全的: 2.Hashtable-->HashMap Hashtable是synchronized,这意味着Hashtable是线程安全的,多个线程可以共享一个Hashtable: HashMap是非synchronized,这意味着HashMap是非线程安全的; 3.Coll

  • Java图形化界面设计之容器(JFrame)详解

    Java图形化界面设计--容器(JFrame) 程序是为了方便用户使用的,因此实现图形化界面的程序编写是所有编程语言发展的必然趋势,在命令提示符下运行的程序可以让我们了解java程序的基本知识体系结构,现在就进入java图形化界面编程. 一.Java基本类(JFC) Java基本类("JavaFoundationClasses",JFC),由一些软件包组成.这些软件包主要包括下面一些应用程序接口(API): ·抽象窗口工具集(AWT)(1.1及以上版本). ·Swing构件. ·Jav

  • spring在IoC容器中装配Bean详解

    1.Spring配置概述 1.1.概述 Spring容器从xml配置.java注解.spring注解中读取bean配置信息,形成bean定义注册表: 根据bean定义注册表实例化bean: 将bean实例放入bean缓存池: 应用程序使用bean. 1.2.基于xml的配置 (1)xml文件概述 xmlns------默认命名空间 xmlns:xsi-------标准命名空间,用于指定自定义命名空间的schema文件 xmlns:xxx="aaaaa"-------自定义命名空间,xx

  • Java面试必备之JMM高并发编程详解

    目录 一.什么是JMM 二.JMM定义了什么 原子性 可见性 有序性 三.八种内存交互操作 四.volatile关键字 可见性 volatile一定能保证线程安全吗 禁止指令重排序 volatile禁止指令重排序的原理 五.总结 一.什么是JMM JMM就是Java内存模型(java memory model).因为在不同的硬件生产商和不同的操作系统下,内存的访问有一定的差异,所以会造成相同的代码运行在不同的系统上会出现各种问题.所以java内存模型(JMM)屏蔽掉各种硬件和操作系统的内存访问差

  • Spring容器刷新obtainFreshBeanFactory示例详解

    目录 Spring容器刷新—02—obtainFreshBeanFactory BeanFactory和ApplicationContext obtainFreshBeanFactory 1.GenericApplicationContext系列的实现 2.AbstractRefreshableApplicationContext系列的实现 该使用哪个BeanFactory? Servlet环境 SpringBoot环境 Spring容器刷新—02—obtainFreshBeanFactory

  • java 中同步方法和同步代码块的区别详解

    java 中同步方法和同步代码块的区别详解 在Java语言中,每一个对象有一把锁.线程可以使用synchronized关键字来获取对象上的锁.synchronized关键字可应用在方法级别(粗粒度锁)或者是代码块级别(细粒度锁). 问题的由来: 看到这样一个面试题: //下列两个方法有什么区别 public synchronized void method1(){} public void method2(){ synchronized (obj){} } synchronized用于解决同步问

  • C++Vector容器常用函数接口详解

    目录 一.基础框架 二.迭代器实现 三.size capacity resize reserve 四.insert,erase 五.pop_back,push_back 六.operator[] 七.构造函数 析构函数 赋值重载 一.基础框架 template<class T> class vector { public: typedef T* iterator; typedef const T* const_iterator; private: iterator _start;//指向第一个

  • Java进程内缓存框架EhCache详解

    目录 一:目录 二: 简介 2.1.基本介绍 2.2.主要的特性 2.3. 集成 2.4. ehcache 和 redis 比较 三:事例 3.1.在pom.xml中引入依赖 3.2.在src/main/resources/创建一个配置文件 ehcache.xml 3.3.测试类 3.4.缓存配置 一:xml配置方式: 二:编程方式配置 3.5.Ehcache API 四:Spring整合 4.1.pom.xml 引入spring和ehcache 4.2.在src/main/resources添

  • Java实现年兽大作战游戏详解

    目录 前言 一.玩法介绍 二.代码介绍 2.1 程序入口[Frame] 2.2 构造器[GamePanel] 2.3 游戏逻辑实现[GamePanel] 2.4 游戏的血液[InitProcessor] 2.5 实体类[FireworksDO][FlowersDO] 2.6 图片素材 三.总结 前言 春节要到了,看惯了前端各种小游戏,确实做得很好,很精致.但是我也要为后端程序员稍微做一点贡献,做一款java版本的[年兽大作战]. 这个游戏加上编写文章,上班摸鱼时间加上回家的空闲时间,大概花了三天

随机推荐