2020-12-28

返回主页
Java旅途
微信搜索『Java旅途』每天推送优质文章
博客园 首页 新随笔 联系 订阅 管理 随笔 - 96 文章 - 0 评论 - 51
为什么线程安全的List推荐使用CopyOnWriteArrayList,而不是Vector
注:本系列文章中用到的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,扩容容量为0的Vector集合。

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的数组,并拷贝原来的元素给新数组
追加元素到新数组末尾
指向新数组
释放锁
这个过程是线程安全的,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()方法只要返回数组长度就可以了。

三、总结

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

点关注、不迷路

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

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

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

分类: Java基础
好文要顶 关注我 收藏该文
Java旅途
关注 - 3
粉丝 - 55
+加关注
0
« 上一篇: 从面试角度分析LinkedList源码
posted @ 2020-12-28 11:24 Java旅途 阅读(96) 评论(1) 编辑 收藏
评论列表
#1楼 2020-12-28 13:06 pingko
Dramazon: Your article really helps me in growing my business. Thank you
刷新评论刷新页面返回顶部
登录后才能发表评论,立即 登录 或 注册, 访问 网站首页
写给园友们的一封求助信
公告

关注公众号
技术干货推送
免费资料领取
定时福利发放
昵称: Java旅途
园龄: 1年6个月
粉丝: 55
关注: 3
+加关注
我的标签
java(10)
rabbitmq(4)
设计模式(4)
消息队列(4)
设计原则(3)
静态成员变量(3)
静态方法(3)
加载顺序(3)
内部类(3)
cookie(3)
更多
随笔分类
Java基础(12)
Spring Boot(3)
Spring Cloud(6)
spring-boot-route(24)
weixin开发(1)
技术杂谈(10)
教程系列(1)
开发工具(1)
数据结构(4)
消息队列(3)
指尖人生(3)
随笔档案
2020年12月(3)
2020年11月(3)
2020年10月(27)
2020年9月(5)
2020年8月(7)
2020年7月(8)
2020年6月(4)
2020年5月(6)
2020年4月(2)
2020年3月(1)
2020年1月(5)
2019年12月(5)
2019年11月(9)
2019年10月(10)
2019年9月(1)
阅读排行榜

  1. 如何优雅的使用springboot项目内置tomcat(6939) 2. 《RabbitMQ》如何保证消息不被重复消费(4938) 3. Spring Cloud Alibaba系列(二)nacos作为服务配置中心 (4156) 4. java类在何时被加载(3732) 5. 一个汉字占几个字节你真的记住了吗?(3336)
    推荐排行榜
  2. Redis可以做哪些事?(7)
  3. 如何设计一个牛逼的API接口(4)
  4. 1024|推荐一个开源免费的Spring Boot教程(3)
  5. spring-boot-route(二十一)quartz实现动态定时任务(3)
  6. 如何有效防止sql注入(3)
    Copyright © 2020 Java旅途
    Powered by .NET 5.0 on Kubernetes
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值