△Hollis, 一个对Coding有着独特追求的人△

这是Hollis的第 226篇原创分享

作者 l Hollis

来源 l Hollis(ID:hollischuang)

为了方便编写出线程安全的程序,Java里面提供了一些线程安全类和并发工具,比如:同步容器、并发容器、阻塞队列等。

最常见的同步容器就是Vector和Hashtable了,那么,同步容器的所有操作都是线程安全的吗?

这个问题不知道你有没有想过,本文就来深入分析一下这个问题,一个很容易被忽略的问题。

1

同步容器

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

  • 1、Vector、Stack、HashTable

  • 2、Collections类中提供的静态工厂方法创建的类

本文拿相对简单的Vecotr来举例,我们先来看下Vector中几个重要方法的源码:

public synchronized boolean add(E e) {
    modCount++;
    ensureCapacityHelper(elementCount + 1);
    elementData[elementCount++] = e;
    return true;
}

public synchronized E remove(int index) {
    modCount++;
    if (index >= elementCount)
        throw new ArrayIndexOutOfBoundsException(index);
    E oldValue = elementData(index);

int numMoved = elementCount - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
    elementData[--elementCount] = null; // Let gc do its work

return oldValue;
}

public synchronized E get(int index) {
    if (index >= elementCount)
        throw new ArrayIndexOutOfBoundsException(index);

return elementData(index);
}

可以看到,Vector这样的同步容器的所有公有方法全都是synchronized的,也就是说,我们可以在多线程场景中放心的使用单独这些方法,因为这些方法本身的确是线程安全的。

但是,请注意上面这句话中,有一个比较关键的词:单独

因为,虽然同步容器的所有方法都加了锁,但是对这些容器的复合操作无法保证其线程安全性。需要客户端通过主动加锁来保证。

简单举一个例子,我们定义如下删除Vector中最后一个元素方法:

public Object deleteLast(Vector v){
    int lastIndex  = v.size()-1;
    v.remove(lastIndex);
}

上面这个方法是一个复合方法,包括size()和remove(),乍一看上去好像并没有什么问题,无论是size()方法还是remove()方法都是线程安全的,那么整个deleteLast方法应该也是线程安全的。

但是时,如果多线程调用该方法的过程中,remove方法有可能抛出ArrayIndexOutOfBoundsException。

Exception in thread "Thread-1" java.lang.ArrayIndexOutOfBoundsException: Array index out of range: 879
    at java.util.Vector.remove(Vector.java:834)
    at com.hollis.Test.deleteLast(EncodeTest.java:40)
    at com.hollis.Test$2.run(EncodeTest.java:28)
    at java.lang.Thread.run(Thread.java:748)

我们上面贴了remove的源码,我们可以分析得出:当index >= elementCount时,会抛出ArrayIndexOutOfBoundsException ,也就是说,当当前索引值不再有效的时候,将会抛出这个异常。

因为removeLast方法,有可能被多个线程同时执行,当线程2通过index()获得索引值为10,在尝试通过remove()删除该索引位置的元素之前,线程1把该索引位置的值删除掉了,这时线程一在执行时便会抛出异常。


为了避免出现类似问题,可以尝试加锁:

public void deleteLast() {
    synchronized (v) {
        int index = v.size() - 1;
        v.remove(index);
    }
}

如上,我们在deleteLast中,对v进行加锁,即可保证同一时刻,不会有其他线程删除掉v中的元素。

另外,如果以下代码会被多线程执行时,也要特别注意:

for (int i = 0; i < v.size(); i++) {
    v.remove(i);
}

由于,不同线程在同一时间操作同一个Vector,其中包括删除操作,那么就同样有可能发生线程安全问题。所以,在使用同步容器的时候,如果涉及到多个线程同时执行删除操作,就要考虑下是否需要加锁。

2

同步容器的问题

前面说过了,同步容器直接保证单个操作的线程安全性,但是无法保证复合操作的线程安全,遇到这种情况时,必须要通过主动加锁的方式来实现。

而且,除此之外,同步容易由于对其所有方法都加了锁,这就导致多个线程访问同一个容器的时候,只能进行顺序访问,即使是不同的操作,也要排队,如get和add要排队执行。这就大大的降低了容器的并发能力。

3

并发容器

针对前文提到的同步容器存在的并发度低问题,从Java5开始,java.util.concurent包下,提供了大量支持高效并发的访问的集合类,我们称之为并发容器。


针对前文提到的同步容器的复合操作的问题,一般在Map中发生的比较多,所以在ConcurrentHashMap中增加了对常用复合操作的支持,比如putIfAbsent()、replace(),这2个操作都是原子操作,可以保证线程安全。

另外,并发包中的CopyOnWriteArrayList和CopyOnWriteArraySet是Copy-On-Write的两种实现。

Copy-On-Write容器即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。

CopyOnWriteArrayList中add/remove等写方法是需要加锁的,而读方法是没有加锁的。

这样做的好处是我们可以对CopyOnWrite容器进行并发的读,当然,这里读到的数据可能不是最新的。因为写时复制的思想是通过延时更新的策略来实现数据的最终一致性的,并非强一致性。

但是,作为代替Vector的CopyOnWriteArrayList并没有解决同步容器的复合操作的线程安全性问题。

4

总结

本文介绍了同步容器和并发容器。

同步容器是通过加锁实现线程安全的,并且只能保证单独的操作是线程安全的,无法保证复合操作的线程安全性。并且同步容器的读和写操作之间会互相阻塞。

并发容器是Java 5中提供的,主要用来代替同步容器。有更好的并发能力。而且其中的ConcurrentHashMap定义了线程安全的复合操作。

在多线程场景中,如果使用并发容器,一定要注意复合操作的线程安全问题。必要时候要主动加锁。

在并发场景中,建议直接使用java.util.concurent包中提供的容器类,如果需要复合操作时,建议使用有些容器自身提供的复合方法。

快薅,当当的羊毛,晚了就没了!

快薅,当当的羊毛,晚了就没了!

快薅,当当的羊毛,晚了就没了!

- MORE | 更多精彩文章 -

  • 华为发布会:  牛逼鸿蒙,吹水的大会

  • 我被失联2年后,终于从东南亚的技术“魔窟”逃出来了...

  • 如何参与一个顶级开源项目

  • 原创 | 既生synchronized,何生volatile?!

如果你喜欢本文,

请长按二维码,关注 Hollis.

转发至朋友圈,是对我最大的支持。

好文章,我在看❤️

原创 | 面试官问我同步容器(如Vector)的所有操作一定是线程安全的吗?我懵了!...相关推荐

  1. 往map里的vector添加_面试官问我同步容器(如Vector)的所有操作一定是线程安全的吗?我懵了!...

    为了方便编写出线程安全的程序,Java里面提供了一些线程安全类和并发工具,比如:同步容器.并发容器.阻塞队列等. 最常见的同步容器就是Vector和Hashtable了,那么,同步容器的所有操作都是线 ...

  2. eureka自我保护时间_阿里面试官问我:到底知不知道什么是Eureka,这次,我没沉默...

    文章首发:阿里面试官问我:到底知不知道什么是Eureka,这次,我没沉默 什么是服务注册? 首先我们来了解下,服务注册.服务发现和服务注册中心的之间的关系. 举个形象的例子,三者之间的关系就好像是供货 ...

  3. 【264期】面试官问:Spring Boot 启动时自动执行代码方式有哪几种?解释一二!...

    点击上方"Java精选",选择"设为星标" 别问别人为什么,多问自己凭什么! 下方有惊喜,留言必回,有问必答! 每一天进步一点点,是成功的开始... 前言 目前 ...

  4. 【高并发】关于乐观锁和悲观锁,蚂蚁金服面试官问了我这几个问题!!

    写在前面 最近,一名读者去蚂蚁金服面试,面试官问了他关于乐观锁和悲观锁的问题,幸亏他看了我的[高并发专题]文章,结果是替这名读者高兴!现就部分面试题目总结成文,供小伙伴们参考. 小伙伴们可以关注 冰河 ...

  5. 面试官问:Kafka 会不会丢消息?怎么处理的?

    点击上方蓝色"方志朋",选择"设为星标" 回复"666"获取独家整理的学习资料! Kafka存在丢消息的问题,消息丢失会发生在Broker, ...

  6. 后处理程序文件大小的变量_【每日一题】(17题)面试官问:JS中事件流,事件处理程序,事件对象的理解?...

    关注「松宝写代码」,精选好文,每日一题 作者:saucxs | songEagle 2020,实「鼠」不易 2021,「牛」转乾坤 风劲潮涌当扬帆,任重道远须奋蹄! 一.前言 2020.12.23 立 ...

  7. 大叔手记(10):别再让面试官问你单例

    大叔手记(10):别再让面试官问你单例(暨6种实现方式让你堵住面试官的嘴) ... 2012-2-19 09:03| 发布者: benben| 查看: 283| 评论: 0 摘要: 引子经常从Recr ...

  8. .jar中没有主清单属性_面试官问:为什么SpringBoot的 jar 可以直接运行?

    点击上方蓝色字体,选择"设为星标" 优质文章,及时送达 来源 | https://urlify.cn/uQvIna SpringBoot提供了一个插件spring-boot-mav ...

  9. 面试官问:能否模拟实现JS的bind方法(高频考点)

    可以点击上方的话题JS基础系列,查看往期文章 写于2018年11月21日,发布在掘金阅读量1.3w+ 前言 这是面试官问系列的第二篇,旨在帮助读者提升JS基础知识,包含new.call.apply.t ...

最新文章

  1. 设计模式读书笔记-单件模式
  2. 一起学nRF51xx 7 -  spi
  3. c++学习笔记之异常
  4. python科学计算笔记(十二)pandas的resample采样
  5. mysql text与blog的区别
  6. CASS10.1软件在windows10中细等线等字体显示不出来的解决方案
  7. 在分页后web报表的最后一页补足空行的方法
  8. 锁存器怎么使用c语言编程,读引脚、读锁存器与读-改-写指令
  9. cdr圆形渐变填充怎么设置_CDR渐变填充实例教程
  10. 苹果Mac电脑文件夹路径怎么看?“访达”也能显示文件路径
  11. “人工智能基础”课程笔记
  12. hd集成显卡 linux驱动,ati 集成显卡HD3200 驱动安装
  13. 鸭子心包积液发病比较多是因为什么该怎么治疗
  14. double类型大小比较的方法
  15. 图书馆 管理系统--可行性报告
  16. 2014可信软件系统工程(国际)暑期学校
  17. 关于pig是否可以匹配中文字符
  18. 网络推广中应如何发软文?
  19. 思科交换机2960G重灌IOS
  20. 基于中国剩余定理的(t, n)门限秘密共享方案

热门文章

  1. comparator 字符串比较大小_java – 如何使用Comparator比较空值?
  2. 单片机c语言三种经典程序结构,单片机C语言程序的结构和设计精选.docx
  3. concat效率 mysql_MySQL统计函数GROUP_CONCAT使用陷阱分析
  4. Redis Flushall 命令
  5. python 线程的使用
  6. 20165307《网络对抗技术》Exp1 PC平台逆向破解
  7. python第八题 查找敏感单词
  8. mysql创建全文索引
  9. torch学习笔记--tensor介绍2,对tensor的结构
  10. Raspberry Pi 学习笔记之一