Java集合之同步容器详解

 更新时间:2022年08月10日 11:01:06   作者:王廷云的博客  
这篇文章主要为大家详细介绍了Java集合之同步容器,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

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

最常见的同步容器就是Vector和Hashtable了,那么,同步容器的所有操作都是线程安全的吗?下面我们来一一分析这个问题。

同步容器

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

  • Vector、Stack、HashTable
  • 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,其中包括删除操作,那么就同样有可能发生线程安全问题。所以,在使用同步容器的时候,如果涉及到多个线程同时执行删除操作,就要考虑下是否需要加锁。

同步容器的问题

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

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

并发容器

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

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

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

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

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

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

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

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

相关文章

  • Mybatis-Plus使用p6spy对SQL性能进行监控的方法

    Mybatis-Plus使用p6spy对SQL性能进行监控的方法

    这篇文章主要介绍了Mybatis-Plus使用p6spy对SQL性能进行监控的方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-12-12
  • SocketIo+SpringMvc实现文件的上传下载功能

    SocketIo+SpringMvc实现文件的上传下载功能

    这篇文章主要介绍了SocketIo+SpringMvc实现文件的上传下载功能,socketIo不仅可以用来做聊天工具,也可以实现局域网。文中给出了实现代码,需要的朋友可以参考下
    2018-08-08
  • 解决redisTemplate中leftPushAll隐性bug的问题

    解决redisTemplate中leftPushAll隐性bug的问题

    这篇文章主要介绍了解决redisTemplate中leftPushAll隐性bug的问题,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2021-02-02
  • SpringBoot实现API接口的完整代码

    SpringBoot实现API接口的完整代码

    这篇文章主要给大家介绍了关于SpringBoot实现API接口的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-10-10
  • 如何在Maven项目中运行JUnit5测试用例实现

    如何在Maven项目中运行JUnit5测试用例实现

    这篇文章主要介绍了如何在Maven项目中运行JUnit5测试用例实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-04-04
  • SpringBoot+随机盐值+双重MD5实现加密登录

    SpringBoot+随机盐值+双重MD5实现加密登录

    数据加密在很多项目上都可以用到,大部分都会采用MD5进行加密,本文主要介绍了SpringBoot+随机盐值+双重MD5实现加密登录,具有一定的参考价值,感兴趣的可以了解一下
    2024-02-02
  • 详解springboot整合ehcache实现缓存机制

    详解springboot整合ehcache实现缓存机制

    这篇文章主要介绍了详解springboot整合ehcache实现缓存机制,ehcache提供了多种缓存策略,主要分为内存和磁盘两级,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2018-01-01
  • 解决idea中maven项目无端显示404错误的方法

    解决idea中maven项目无端显示404错误的方法

    这篇文章主要介绍了解决idea中maven项目无端显示404错误的方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-12-12
  • SSH框架网上商城项目第4战之EasyUI菜单的实现

    SSH框架网上商城项目第4战之EasyUI菜单的实现

    SSH框架网上商城项目第4战之EasyUI菜单的实现,本文主要使用EasyUI技术简单实现后台菜单,感兴趣的小伙伴们可以参考一下
    2016-05-05
  • Java开发HashMap key必须实现hashCode equals方法原理

    Java开发HashMap key必须实现hashCode equals方法原理

    这篇文章主要为大家介绍了Java开发HashMap key必须实现hashCode equals方法原理详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-03-03

最新评论