注:本系列文章中用到的jdk版本均为java8

相比很多同学在刚接触Java集合的时候,线程安全的List用的一定是Vector。但是现在用到的线程安全的List一般都会用CopyOnWriteArrayList,很少有人再去用Vector了,至于为什么,文章中会具体说到。接下来,我们先来简单分析以下Vector的源码。

一、Vector集合源码简析

由于本文的重点不是Vector集合,因此只是简单的分析一下Vector的初始化方法和添加元素的方法。

Vector的底层实现和ArrayList一样,都是由数组实现的。

Vector的主要变量如下:

/**
* 存放元素的数组
*/
protected Object[] elementData;
/**
* 元素个数
*/
protected int elementCount;
/**
* 扩容自增容量大小
*/
protected int capacityIncrement;

1.1 Vector初始化

Vector的初始化提供了三个方法,除了可以指定初始容量的大小,还可以指定扩容容量的大小。构造器分别如下:

无参构造器

public Vector() {
this(10);
}

指定初始化容量的构造器

public Vector(int initialCapacity) {
this(initialCapacity, 0);
}

指定初始化容量和扩容容量大小的构造器

public Vector(int initialCapacity, int capacityIncrement) {
super();
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+initialCapacity);
this.elementData = new Object[initialCapacity];
this.capacityIncrement = capacityIncrement;
}

从上面的构造器中可以看出,如果调用无参构造器,则会创建一个初始化容量为10,扩容容量为0Vector集合。

1.2 如何扩容

Vector的扩容机制和ArrayList的很像,如果不清楚ArrayList的扩容机制,可以看看这篇文章。这里我们直接看Vector的扩容方法grow

private void grow(int minCapacity) {
// overflow-conscious code
// 初始化数组的长度,默认为10
int oldCapacity = elementData.length;
// 是否指定扩容容量,不指定扩容为原来的2倍
int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
capacityIncrement : oldCapacity);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
elementData = Arrays.copyOf(elementData, newCapacity);
}

通过上面的方法,我们可以看出,如果指定了扩容容量的大小则扩容的新数组大小为原来的数组加上扩容容量的大小,如果不指定扩容容量的大小则扩容的新数组大小为原来数组大小的2倍。这样扩容为原来的2倍是很消耗空间的,这也是Vector被弃用的原因之一。

除此之外,看过源码的同学可能发现了,Vector集合的所有操作元素的方法都加了synchronized关键字,这就导致了操作Vector的效率会非常低,在开发中,往往读操作的使用频率会远高于其他操作,而CopyOnWriteArrayList 就是这样一种读操作效率远高于写操作效率的List,一起来看看。

二、CopyOnWriteArrayList源码简析

CopyOnWriteArrayList 类图:

2.1 CopyOnWrite思想

CopyOnWrite简称COW,根据名字来看就是写入时复制。意思就是大家共同去访问一个资源,如果有人想要去修改这个资源的时候,就需要复制一个副本,去修改这个副本,而对于其他人来说访问得资源还是原来的,不会发生变化。

2.2 初始化CopyOnWriteArrayList

CopyOnWriteArrayList 底层是也是有数组实现的。 本文我们只解读添加元素和读取元素的区别,删除修改元素原理和添加元素差不多,操作时都需要进行加锁,而读操作不会加锁。

CopyOnWriteArrayList 主要有以下两个变量:

// 独占锁
final transient ReentrantLock lock = new ReentrantLock(); // 存放元素的数组
private transient volatile Object[] array;

我们仔细来分析一下上面两个属性,这两个思想是 CopyOnWriteArrayList 的核心 。

  • lock:ReentrantLock,独占锁,多线程运行的情况下,只有一个线程会获得这个锁,只有释放锁后其他线程才能获得。
  • array:存放数据的数组,关键是被volatile修饰了,被volatile修饰,就保证了可见性,也就是一个线程修改后,其他线程立即可见。

最常用的初始化方式如下:

/**
* Creates an empty list.
*/
public CopyOnWriteArrayList() {
setArray(new Object[0]);
} /**
* Sets the array.
*/
final void setArray(Object[] a) {
array = a;
}

初始化只是创建了一个空的数组,并将array指向它。

2.3 添加元素

public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
// 获取原来的数组
Object[] elements = getArray();
// 原来数组的长度
int len = elements.length;
// 创建一个长度+1的新数组,并将原来数组的元素复制给新数组
Object[] newElements = Arrays.copyOf(elements, len + 1);
// 元素放在新数组末尾
newElements[len] = e;
// array指向新数组
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}

添加数组的步骤如下:

  1. 获得独占锁,将添加功能加锁
  2. 获取原来的数组,并得到其长度
  3. 创建一个长度为原来数组长度+1的数组,并拷贝原来的元素给新数组
  4. 追加元素到新数组末尾
  5. 指向新数组
  6. 释放锁

这个过程是线程安全的,COW的核心思想就是每次修改的时候拷贝一个新的资源去修改,add()方法再拷贝新资源的时候将数组容量+1,这样虽然每次添加元素都会浪费一定的空间,但是数组的长度正好是元素的长度,也在一定程度上节省了扩容的开销。

2.4 获取元素

public E get(int index) {
return get(getArray(), index);
} final Object[] getArray() {
return array;
} private E get(Object[] a, int index) {
return (E) a[index];
}

读操作是天然安全的操作,而且数组本身会进行检查越界问题,因此获取元素的方法很简单,只是根据索引获取该元素。

public int size() {
return getArray().length;
}

由于CopyOnWriteArrayList的底层数组长度,本身就是元素大小,因此size()方法只要返回数组长度就可以了。

三、总结

VectorCopyOnWriteArrayList都是线程安全的List,底层都是数组实现的,Vector的每个方法都进行了加锁,而CopyOnWriteArrayList的读操作是不加锁的,因此CopyOnWriteArrayList的读性能远高于VectorVector每次扩容的大小都是原来数组大小的2倍,而CopyOnWriteArrayList不需要扩容,通过COW思想就能使数组容量满足要求。两个集合都是先了RandomAccess接口,支持随机读取,因此更加推荐使用for循环进行遍历。在开发中,读操作会远远多于其他操作,因此使用CopyOnWriteArrayList集合效率更高。

点关注、不迷路

如果觉得文章不错,欢迎关注、点赞、收藏,你们的支持是我创作的动力,感谢大家。

如果文章写的有问题,请不要吝惜文笔,欢迎留言指出,我会及时核查修改。

如果你还想看到更多别的东西,可以微信搜索「Java旅途」进行关注。「Java旅途」目前已经整理各种中间件的使用教程及各类Java相关的面试题。扫描下方二维码进行关注就可以得到这些资料。

为什么线程安全的List推荐使用CopyOnWriteArrayList,而不是Vector的更多相关文章

  1. ArrayList线程不安全怎么办?(CopyOnWriteArrayList详解)

    ArrayList线程不安全怎么办? 有三种解决方法: 使用对应的 Vector 类,这个类中的所有方法都加上了 synchronized 关键字 就和 HashMap 和 HashTable 的关系 ...

  2. 为什么在Python里推荐使用多进程而不是多线程

    转载  http://bbs.51cto.com/thread-1349105-1.html 最近在看Python的多线程,经常我们会听到老手说:"Python下多线程是鸡肋,推荐使用多进程 ...

  3. 为什么在Python里推荐使用多进程而不是多线程?

    最近在看Python的多线程,经常我们会听到老手说:“Python下多线程是鸡肋,推荐使用多进程!”,但是为什么这么说呢?   要知其然,更要知其所以然.所以有了下面的深入研究: 首先强调背景: 1. ...

  4. 为什么在Python里推荐使用多进程而不是多线程?(为什么python多线程无法增加CPU使用率?)

    最近在看Python的多线程,经常我们会听到老手说:“Python下多线程是鸡肋,推荐使用多进程!”,但是为什么这么说呢? 要知其然,更要知其所以然.所以有了下面的深入研究: 首先强调背景:     ...

  5. 为什么在python中推荐使用多进程而不是多线程(转载)

    最近在看Python的多线程,经常我们会听到老手说:"Python下多线程是鸡肋,推荐使用多进程!",但是为什么这么说呢? 要知其然,更要知其所以然.所以有了下面的深入研究: GI ...

  6. python(34):为什么在Python里推荐使用多进程而不是多线程?

    最近在看Python的多线程,经常我们会听到老手说:“Python下多线程是鸡肋,推荐使用多进程!”,但是为什么这么说呢?                  要知其然,更要知其所以然.所以有了下面的深 ...

  7. <转载> 为什么在Python里推荐使用多进程而不是多线程?

    经常我们会听到老手说:“Python下多线程是鸡肋,推荐使用多进程!”,但是为什么这么说呢?                要知其然,更要知其所以然.所以有了下面的深入研究: 首先强调背景:     ...

  8. android推荐使用dialogFrament而不是alertDialog

    DialogFragment在android 3.0时被引入.是一种特殊的Fragment,用于在Activity的内容之上展示一个模态的对话框.典型的用于:展示警告框,输入框,确认框等等. 在Dia ...

  9. Vector 是线程安全的,是不是在多线程下操作Vector就可以不用加Synchronized

    如标题一样,如果之前让我回答,我会说,是的,在多线程的环境下操作Vector,不需要加Synchronized. 但是我今天无意间看到一篇文章,我才发现我之前的想法是错误的,这篇文章的地址: http ...

随机推荐

  1. 企业安全06-Fastjson-CNVD-2017-02833

    Fastjson-CNVD-2017-02833 一.漏洞概述 fastjson在解析json的过程中,支持使用@type字段来指定反序列化的类型,并调用该类的set/get方法来访问属性,当组件开启 ...

  2. python -- 对list去重并找出列表list中的重复元素

    一.一个列表中可能含有重复元素,使用set()可以实现列表的去重处理,但是无法知道哪些元素是重复的,下面的函数用于找出哪些元素重复了,以及重复的次数. 代码: from collections imp ...

  3. wirshark找不到本地接口

    解决方法: 1.以管理员权限运行wireshark 2.以管理员身份运行cmd,输入net start npf,打开网络抓包服务,运行wireshark legacy,选择要抓包的网卡

  4. 音视频入门-18-手动生成一张GIF图片

    * 音视频入门文章目录 * GIF 编码知识 GIF 包含的数据块: 文件头(Header) 逻辑屏幕标识符(Logical Screen Descriptor) 全局颜色表(Global Color ...

  5. Java面试专题-多线程篇(2)- 锁和线程池

  6. 第4.2节 神秘而强大的Python生成器精讲

    一. 生成器(generator)概念 生成器是一个特殊的迭代器,它保存的是算法,每次调用next()或send()就计算出下一个元素的值,直到计算出最后一个元素,没有更多的元素时,抛出StopIte ...

  7. 第7.28节 《Python类、类型、协议》章节总结

    本章详细介绍了Python协议.多态与"鸭子类型".类.类实例变量.类变量.实例方法.类方法.静态方法.类继承.抽象类.property函数和@property装饰器定义属性访问方 ...

  8. 关于open函数文件打开模式的有意思的一个现象

    老猿前阵子学习了文件IO,最近正在回顾及进行各种验证和总结,老猿在对文件进行打开后的返回值检查属性时,发现文件打开返回的文件对象的读写模式与打开文件的模式并不完全相同,如下案例: fp1 = open ...

  9. 第13.4 使用pip安装和卸载扩展模块

    一.pip指令介绍 Python 使用pip来管理扩展模块,包括安装和卸载,具体指令包括: pip install xx: 安装xx模块 pip list: 列出已安装的模块 pip install ...

  10. PyQt(Python+Qt)学习随笔:Qt Designer中QAbstractButton派生按钮部件autoExclusive属性

    autoExclusive 属性保留是否启用按钮的自动排它特性,如果启用了,则属于同一父部件的可选中按钮任何时候只能选中一个按钮:选中另一个按钮将自动取消选中先前选中的按钮,这个功能与排他性按钮组的功 ...