[Java 并发编程实战] 同步容器类潜在的问题(含实例代码)

路漫漫其修远兮,吾将上下而求索。———屈原《离骚》

PS: 如果觉得本文有用的话,请帮忙点赞,留言评论支持一下哦,您的支持是我最大的动力!谢谢啦~

本篇文章主要讲同步容器类存在的潜在问题以及解决办法。我们不禁想问,同步容器就一定是真正地完全线程安全吗?不一定。因为它可能会抛出下面这两种异常。

  1. ArrayIndexOutBoundsException 异常
  2. ConcurrentModificationException 异常

恩,这篇我们就来讨论这两个异常出现的原因以及解决办法。

同步策略

好,现在再来看上一篇文章中说到的同步容器类,先来了解它们的同步策略。它们主要有:

  • HashTable
  • Vector
  • Stack
  • 同步包装器 : [ Collections.synchronizedMap(), Collections.synchronizedList() ]

Vector 的部分源码如下:

 1  //... 2  public synchronized E get(int index) { 3        if (index >= elementCount) 4            throw new ArrayIndexOutOfBoundsException(index); 5 6        return elementData(index); 7    } 8 9    public synchronized E set(int index, E element) {10        if (index >= elementCount)11            throw new ArrayIndexOutOfBoundsException(index);1213        E oldValue = elementData(index);14        elementData[index] = element;15        return oldValue;16    }1718    public synchronized boolean add(E e) {19        modCount++;20        ensureCapacityHelper(elementCount + 1);21        elementData[elementCount++] = e;22        return true;23    }24    //...

通过同步容器的源码可以看出,它们将状态封装起来,对每个方法都采用 synchronized 进行同步,每次只有一个线程能访问容器。

ArrayIndexOutBoundsException 异常

同步容器类理论上都是线程安全的,但是在某些情况下,依然会出错。我们用 Vector 来举例,比方现在要删除 Vector 里面的最后一个元素。如果此时有多个线程并发执行这一删除操作,能正常执行吗?

 1import java.util.Vector; 2 3public class VectorTest { 4 5    //定义删除最后一个元素的方法 6    public static void deleteLast(Vector list) { 7        int lastindex = list.size() - 1; 8        list.remove(lastindex); 9    }1011    public static void main(String[] args) {1213        Vector v = new Vector(); //创建一个 Vector1415        //添加 10000 个元素到容器16        for(int i = 0; i < 10000; i++) {17            v.add(i);18        }1920        //启动 N 个线程执行删除操作21        for(int i = 0; i < 900; i++) {22            new Thread(new Runnable() {23                @Override24                public void run() {25                    deleteLast(v);//删除最后一个元素26                }27            }).start();28        }29        System.out.println("end");30    }31}

执行结果:

执行结果
执行结果是抛出异常[ArrayIndexOutBoundsException],数组下标越界。因为在这个程序中,Vector 的 size 是不断减小的。可能一个元素已经被删除的,另一个元素有再去删除它然而元素已经不存在了,所以抛出异常。

当然,这种情况我们只能通过客户端加锁来解决,好在 Vector 的同步策略就是用自己的内置锁。所以我们的代码修改如下,便可以运行:

 1synchronized (v) { 2    deleteLast(v);//删除最后一个元素 3} 4 5//迭代的时候也应当注意加锁 6synchronized (v) { 7    for(int i = 0; i < v.size(); i++) { 8        doSomeThing(v.get(i)); 9    }10}

ConcurrentModificationException 异常

再来看下面一段代码:

 1import java.util.Iterator; 2import java.util.Vector; 3 4public class VectorTest { 5 6    public static void main(String[] args) { 7 8        Vector v = new Vector(); //创建一个 Vector 910        //添加 100 个元素到容器11        for(int i = 0; i < 100; i++) {12            v.add(i);13        }1415        //利用迭代器遍历,在遍历的同时删除一个元素16        Iterator it = v.iterator();17        while(it.hasNext()) {18            Integer integer = (Integer) it.next();19            if(integer == 5) {20                v.remove(integer); //删除一个元素21            }22        }2324        System.out.println(v.toString());25    }26}

执行结果是:

执行结果
在 checkForComodification 中,it.next() 会检查这两个变量(modCount、expectedModCount) 是否相等,不等则抛出这个异常。直接调用 v.remove() ,它会更新 modCount 的值,却没有更新 expectedModCount 的值,所以抛出异常。这两个变量在容器类的位置如下所示:
modCount

要解决这个问题,把 v.remove(integer) 改为使用迭代器删除 it.remove() 即可,迭代器中的方法能够同时更新这两个值,确保相等。有兴趣的朋友可以查看 Itr 的源码看个究竟。

1if(integer == 5) {2    it.remove(); //用迭代器遍历,则用迭代器删除3}

上面说的是单线程的情况下发生异常。那么问题来了,按照上面改好后,多线程的情况下是不是否万事大吉了呢?

答案是否定的。因为多线程情况下,不同线程的操作可能导致这两个变量在不同线程读到的值会不相等,因为这两个变量不是 volatile 变量,所以在多线程之间可能存在不可见性。

就多线程的情况,我们再来看下面一个例子:

 1import java.util.Iterator; 2import java.util.Vector; 3 4public class VectorTest { 5 6    public static void main(String[] args) { 7 8        Vector v = new Vector(); //创建一个 Vector 910        //添加 100 个元素到容器11        for(int i = 0; i < 100; i++) {12            v.add(i);13        }14        //创建一个线程,遍历 Vector15        new Thread(new Runnable() {16            @Override17            public void run() {18                Iterator it = v.iterator();19                while(it.hasNext()) {20                    Integer integer = (Integer) it.next(); //调用next21                    try {22                        Thread.sleep(100);    //睡眠23                    } catch (InterruptedException e) {24                        // TODO Auto-generated catch block25                        e.printStackTrace();26                    }27                }28            }29        }).start();30        //创建一个线程,遍历 Vector31        new Thread(new Runnable() {32            @Override33            public void run() {34                //利用迭代器遍历,在遍历的同时删除一个元素35                Iterator it = v.iterator();36                while(it.hasNext()) {37                    Integer integer = (Integer) it.next();38                    if(integer == 5) {39                        it.remove(); //删除一个元素,更新变量 modCount,expectedModCount的值。40                    }41                }42            }43        }).start();44    }45}

执行结果如下:

这里写图片描述
在对 Vector 等同步容器进行并发迭代修改的时候,迭代器就可能报 ConcurrentModificationException 异常。

针对多线程的情况,为了避免出现异常,一般有2种解决办法:

  1)在使用iterator迭代的时候使用synchronized或者Lock进行同步;
  2)使用并发容器CopyOnWriteArrayList代替ArrayList和Vector。
  
现在通过客户端加锁来举例解决,每个线程在迭代期间持有一个容器锁

1//利用迭代器遍历,在遍历的同时删除一个元素2synchronized(v) {3Iterator it = v.iterator();4while(it.hasNext()) {5    Integer integer = (Integer) it.next();6    if(integer == 5) {7        it.remove(); //删除一个元素,更新变量 modCount,expectedModCount的值。8    }9}

隐藏的迭代器

尽管加锁可以避免迭代器出现 modCount 异常,但是必须记住在一个可能发生迭代的共享容器中,各处都要使用锁。因为有时候,迭代器是隐藏的。

比如标准容器中的 toString() 实现会迭代容器中的每个元素。类似的,容器的hashCode,equal,containsAll,removeAll和retainAIl等方法,都会对容器进行迭代,抛出ConcurrentModificationException。

这是我们应该注意的地方。

好了,今天就写到这,关于并发容器的内容将在下一篇文章中讲述。

本文完结,如对你有帮助,欢迎关注我,谢谢啦~

原文地址:http://blog.51cto.com/13760461/2124399

时间: 2024-08-04 14:16:55

[Java 并发编程实战] 同步容器类潜在的问题(含实例代码)的相关文章

【Java并发编程实战】—– AQS(四):CLH同步队列

在[Java并发编程实战]-–"J.U.C":CLH队列锁提过,AQS里面的CLH队列是CLH同步锁的一种变形. 其主要从双方面进行了改造:节点的结构与节点等待机制.在结构上引入了头结点和尾节点,他们分别指向队列的头和尾,尝试获取锁.入队列.释放锁等实现都与头尾节点相关.而且每一个节点都引入前驱节点和后兴许节点的引用:在等待机制上由原来的自旋改成堵塞唤醒. 其结构例如以下: 知道其结构了,我们再看看他的实现.在线程获取锁时会调用AQS的acquire()方法.该方法第一次尝试获取锁假设

《Java并发编程实战》第十五章 原子变量与非阻塞同步机制 读书笔记

一.锁的劣势 锁定后如果未释放,再次请求锁时会造成阻塞,多线程调度通常遇到阻塞会进行上下文切换,造成更多的开销. 在挂起与恢复线程等过程中存在着很大的开销,并且通常存在着较长时间的中断. 锁可能导致优先级反转,即使较高优先级的线程可以抢先执行,但仍然需要等待锁被释放,从而导致它的优先级会降至低优先级线程的级别. 二.硬件对并发的支持 处理器填写了一些特殊指令,例如:比较并交换.关联加载/条件存储. 1 比较并交换 CAS的含义是:"我认为V的值应该为A,如果是,那么将V的值更新为B,否则不需要修

Java并发编程:同步容器

为了方便编写出线程安全的程序,Java里面提供了一些线程安全类和并发工具,比如:同步容器.并发容器.阻塞队列.Synchronizer(比如CountDownLatch).今天我们就来讨论下同步容器. 以下是本文的目录大纲: 一.为什么会出现同步容器? 二.Java中的同步容器类 三.同步容器的缺陷 若有不正之处请多多谅解,并欢迎批评指正. 请尊重作者劳动成果,转载请标明原文链接: http://www.cnblogs.com/dolphin0520/p/3933404.html 一.为什么会出

《Java并发编程实战》要点笔记及java.util.concurrent 的结构介绍

买了<java并发编程实战>这本书,看了好几遍都不是很懂,这个还是要在实战中找取其中的要点的,后面看到一篇文章笔记做的很不错分享给大家!! 原文地址:http://blog.csdn.net/cdl2008sky/article/details/26377433 Subsections  1.线程安全(Thread safety) 2.锁(lock) 3.共享对象 4.对象组合 5.基础构建模块 6.任务执行 7.取消和关闭 8.线程池的使用 9.性能与可伸缩性 10.并发程序的测试 11.显

《Java并发编程实战》读书笔记

Subsections 线程安全(Thread safety) 锁(lock) 共享对象 对象组合 基础构建模块 任务执行 取消和关闭 线程池的使用 性能与可伸缩性 并发程序的测试 显示锁 原子变量和非阻塞同步机制 一.线程安全(Thread safety) 无论何时,只要多于一个线程访问给定的状态变量.而且其中某个线程会写入该变量,此时必须使用同步来协助线程对该变量的访问. 线程安全是指多个线程在访问一个类时,如果不需要额外的同步,这个类的行为仍然是正确的. 线程安全的实例: (1).一个无状

《Java并发编程实战》/童云兰译【PDF】下载

<Java并发编程实战>/童云兰译[PDF]下载链接: https://u253469.pipipan.com/fs/253469-230062521 内容简介 本书深入浅出地介绍了Java线程和并发,是一本完美的Java并发参考手册.书中从并发性和线程安全性的基本概念出发,介绍了如何使用类库提供的基本并发构建块,用于避免并发危险.构造线程安全的类及验证线程安全的规则,如何将小的线程安全类组合成更大的线程安全类,如何利用线程来提高并发应用程序的吞吐量,如何识别可并行执行的任务,如何提高单线程子

《java并发编程实战》笔记(一)

最近在看<java并发编程实战>,希望自己有毅力把它读完. 线程本身有很多优势,比如可以发挥多处理器的强大能力.建模更加简单.简化异步事件的处理.使用户界面的相应更加灵敏,但是更多的需要程序猿面对的是安全性问题.看下面例子: public class UnsafeSequence { private int value; /*返回一个唯一的数值*/ public int getNext(){ return value++; } } UnsafeSequence的问题在于,如果执行时机不对,那么

《Java并发编程实战》第十六章 Java内存模型 读书笔记

Java内存模型是保障多线程安全的根基,这里仅仅是认识型的理解总结并未深入研究. 一.什么是内存模型,为什么需要它 Java内存模型(Java Memory Model)并发相关的安全发布,同步策略的规范.一致性等都来自于JMM. 1 平台的内存模型 在架构定义的内存模型中将告诉应用程序可以从内存系统中获得怎样的保证,此外还定义了一些特殊的指令(称为内存栅栏或栅栏),当需要共享数据时,这些指令就能实现额外的存储协调保证. JVM通过在适当的位置上插入内存栅栏来屏蔽在JVM与底层平台内存模型之间的

《Java并发编程实战》第八章 线程池的使用 读书笔记

一.在任务与执行策略之间的隐性解耦 有些类型的任务需要明确地指定执行策略,包括: . 依赖性任务.依赖关系对执行策略造成约束,需要注意活跃性问题.要求线程池足够大,确保任务都能放入. . 使用线程封闭机制的任务.需要串行执行. . 对响应时间敏感的任务. . 使用ThreadLocal的任务. 1. 线程饥饿死锁 线程池中如果所有正在执行任务的线程都由于等待其他仍处于工作队列中的任务而阻塞,这种现象称为线程饥饿死锁. 2. 运行时间较长的任务 Java提供了限时版本与无限时版本.例如Thread