Java并发-同步容器篇
作者:汤圆
个人博客:javalover.cc
前言
官人们好啊,我是汤圆,今天给大家带来的是《Java并发-同步容器篇》,希望有所帮助,谢谢
文章如果有问题,欢迎大家批评指正,在此谢过啦
简介
同步容器主要分两类,一种是Vector这样的普通类,一种是通过Collections的工厂方法创建的内部类
虽然很多人都对同步容器的性能低有偏见,但它也不是一无是处,在这里我们插播一条阿里巴巴的开发手册规范:
高并发时,同步调用应该去考量锁的性能损耗。能用无锁数据结构,就不要用锁;能锁区块,就不要锁整个方法体;能用对象锁,就不要用类锁。
可以看到,只有在高并发才会考虑到锁的性能问题,所以在一些小而全的系统中,同步容器还是有用武之地的(当然也可以考虑并发容器,后面章节再讨论)
附言:这不是洗白贴
目录
我们这里分三步来分析:
- 什么是同步容器
- 为什么要有同步容器
- 同步容器的优缺点
- 同步容器的使用场景
正文
1. 什么是同步容器
定义:就是把容器类同步化,这样我们在并发中使用容器时,就不用手动同步,因为内部已经自动同步了
例子:比如Vector就是一个同步容器类,它的同步化就是把内部的所有方法都上锁(有的重载方法没上锁,但是最终调用的方法还是有锁的)
源码:Vector.add
// 通过synchronized为add方法上锁
public synchronized boolean add(E e) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}
同步容器主要分两类:
- 普通类:Vector、Stack、HashTable
- 内部类:Collections创建的内部类,比如Collections.SynchronizedList、 Collections.SynchronizedSet等
那这两种有没有区别呢?
当然是有的,刚开始的时候(Java1.0)只有第一种同步容器(Vector等)
但是因为Vector这种类太局气了,它就想着把所有的东西都弄过来自己搞(Vector通过toArray转为己有,HashTable通过putAll转为己有);
源码:Vector构造函数
public Vector(Collection<? extends E> c) {
// 这里通过toArray将传来的集合 转为己有
elementData = c.toArray();
elementCount = elementData.length;
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, elementCount, Object[].class);
}
所以就有了第二种同步容器类(通过工厂方法创建的内部容器类),它就比较聪明了,它只是把原有的容器进行包装(通过this.list = list直接指向需要同步的容器),然后局部加锁,这样一来,即生成了线程安全的类,又不用太费力;
源码:Collections.SynchronizedList构造函数
SynchronizedList(List<E> list) {
super(list);
// 这里只是指向传来的list,不转为己有,后面的相关操作还是基于原有的list集合
this.list = list;
}
他们之间的区别如下:
两种同步容器的区别 | 普通类 | 内部类 |
---|---|---|
锁的对象 | 不可指定,只能this | 可指定,默认this |
锁的范围 | 方法体(包括迭代) | 代码块(不包括迭代) |
适用范围 | 窄-个别容器 | 广-所有容器 |
这里我们重点说下锁的对象:
- 普通类锁的是当前对象this(锁在方法上,默认this对象);
- 内部类锁的是mutex属性,这个属性默认是this,但是可以通过构造函数(或工厂方法)来指定锁的对象
源码:Collections.SynchronizedCollection构造函数
final Collection<E> c; // Backing Collection
// 这个就是锁的对象
final Object mutex; // Object on which to synchronize
SynchronizedCollection(Collection<E> c) {
this.c = Objects.requireNonNull(c);
// 初始化为 this
mutex = this;
}
SynchronizedCollection(Collection<E> c, Object mutex) {
this.c = Objects.requireNonNull(c);
this.mutex = Objects.requireNonNull(mutex);
}
这里要注意一点就是,内部类的迭代器没有同步(Vector的迭代器有同步),需要手动加锁来同步
源码:Vector.Itr.next 迭代方法(有上锁)
public E next() {
synchronized (Vector.this) {
checkForComodification();
int i = cursor;
if (i >= elementCount)
throw new NoSuchElementException();
cursor = i + 1;
return elementData(lastRet = i);
}
}
源码:Collections.SynchronizedCollection.iterator 迭代器(没上锁)
public Iterator<E> iterator() {
// 这里会直接实现类的迭代器(比如ArrayList,它里面的迭代器肯定是没上锁的)
return c.iterator(); // Must be manually synched by user!
}
2. 为什么要有同步容器
因为普通的容器类(比如ArrayList)是线程不安全的,如果是在并发中使用,我们就需要手动对其加锁才会安全,这样的话就很麻烦;
所以就有了同步容器,它来帮我们自动加锁
下面我们用代码来对比下
线程不安全的类:ArrayList
public class SyncCollectionDemo {
private List<Integer> listNoSync;
public SyncCollectionDemo() {
this.listNoSync = new ArrayList<>();
}
public void addNoSync(int temp){
listNoSync.add(temp);
}
public static void main(String[] args) throws InterruptedException {
SyncCollectionDemo demo = new SyncCollectionDemo();
// 创建10个线程
for (int i = 0; i < 10; i++) {
// 每个线程执行100次添加操作
new Thread(()->{
for (int j = 0; j < 1000; j++) {
demo.addNoSync(j);
}
}).start();
}
}
}
上面的代码看似没问题,感觉就算有问题也应该是插入的顺序比较乱(多线程交替插入)
但实际上运行会发现,可能会报错数组越界,如下所示:
原因有二:
- 因为ArrayList.add操作没有加锁,导致多个线程可以同时执行add操作
- add操作时,如果发现list的容量不足,会进行扩容,但是由于多个线程同时扩容,就会出现扩容不足的问题
源码:ArrayList.grow扩容
// 扩容方法
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
// 这里可以看到,每次扩容增加一半的容量
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
可以看到,扩容是基于之前的容量进行的,因此如果多个线程同时扩容,那扩容基数就不准确了,结果就会有问题
线程安全的类:Collections.SynchronizedList
/**
* <p>
* 同步容器类:为什么要有它
* </p>
*
* @author: JavaLover
* @time: 2021/5/3
*/
public class SyncCollectionDemo {
private List<Integer> listSync;
public SyncCollectionDemo() {
// 这里包装一个空的ArrayList
this.listSync = Collections.synchronizedList(new ArrayList<>());
}
public void addSync(int j){
// 内部是同步操作: synchronized (mutex) {return c.add(e);}
listSync.add(j);
}
public static void main(String[] args) throws InterruptedException {
SyncCollectionDemo demo = new SyncCollectionDemo();
for (int i = 0; i < 10; i++) {
new Thread(()->{
for (int j = 0; j < 100; j++) {
demo.addSync(j);
}
}).start();
}
TimeUnit.SECONDS.sleep(1);
// 输出1000
System.out.println(demo.listSync.size());
}
}
输出正确,因为现在ArrayList被Collections包装成了一个线程安全的类
这就是为啥会有同步容器的原因:因为同步容器使得并发编程时,线程更加安全
3. 同步容器的优缺点
一般来说,都是先说优点,再说缺点
但是我们这次先说优点
优点:
- 并发编程中,独立操作是线程安全的,比如单独的add操作
缺点(是的,优点已经说完了):
性能差,基本上所有方法都上锁,完美的诠释了“宁可错杀一千,不可放过一个”
复合操作,还是不安全,比如putIfAbsent操作(如果没有则添加)
快速失败机制,这种机制会报错提示
ConcurrentModificationException
,一般出现在当某个线程在遍历容器时,其他线程恰好修改了这个容器的长度
为啥第三点是缺点呢?
因为它只能作为一个建议,告诉我们有并发修改异常,但是不能保证每个并发修改都会爆出这个异常
爆出这个异常的前提如下:
源码:Vector.Itr.checkForComodification 检查容器修改次数
final void checkForComodification() {
// modCount:容器的长度变化次数, expectedModCount:期望的容器的长度变化次数
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
那什么情况下并发修改不会爆出异常呢?有两种:
遍历没加锁的情况:对于第二种同步容器(Collections内部类)来说,假设线程A修改了modCount的值,但是没有同步到线程B,那么线程B遍历就不会发生异常(但实际上问题已经存在了,只是暂时没有出现)
依赖线程执行顺序的情况:对于所有的同步容器来说,假设线程B已经遍历完了容器,此时线程A才开始遍历修改,那么也不会发生异常
代码就不贴了,大家感兴趣的可以直接写几个线程遍历试试,多运行几次,应该就可以看到效果(不过第一种情况也是基于理论分析,实际代码我这边也没跑出来)
根据阿里巴巴的开发规范:不要在 foreach 循环里进行元素的 remove/add 操作。remove 元素请使用 Iterator方式,如果并发操作,需要对 Iterator 对象加锁。
这里解释下,关于List.remove和Iterator.remove的区别
Iterator.remove:会同步修改expectedModCount=modCount
list.remove:只会修改modCount,因为expectedModCount属于iterator对象的属性,不属于list的属性(但是也可以间接访问)
源码:ArrayList.remove移除元素操作
public E remove(int index) {
rangeCheck(index);
// 1. 这里修改了 modCount
modCount++;
E oldValue = elementData(index);
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
源码:ArrayList.Itr.remove迭代器移除元素操作
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
// 1. 这里调用上面介绍的list.romove,修改modCount
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
// 2. 这里再同步更新 expectedModCount
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
由于同步容器的这些缺点,于是就有了并发容器(下期来介绍)
4. 同步容器的使用场景
多用在并发编程,但是并发量又不是很大的场景,比如一些简单的个人博客系统(具体多少并发量算大,这个也是分很多情况而论的,并不是说每秒处理超过多少个请求,就说是高并发,还要结合吞吐量、系统响应时间等多个因素一起考虑)
具体点来说的话,有以下几个场景:
- 写多读少,这个时候同步容器和并发容器的性能差别不大(并发容器可以并发读)
- 自定义的复合操作,比如getLast等操作(putIfAbsent就算了,因为并发容器有默认提供这个复合操作)
- 等等
总结
- 什么是同步容器:就是把容器类同步化,这样我们在并发中使用容器时,就不用手动同步,因为内部已经自动同步了
- 为什么要有同步容器:因为普通的容器类(比如ArrayList)是线程不安全的,如果是在并发中使用,我们就需要手动对其加锁才会安全,这样的话就很太麻烦;所以就有了同步容器,它来帮我们自动加锁
- 同步容器的优缺点:
优点 | 缺点 | |
---|---|---|
同步容器 | 独立操作,线程安全 | 复合操作,还是不安全 |
性能差 | ||
快速失败机制,只适合bug调试 |
- 同步容器的使用场景
多用在并发量不是很大的场景,比如个人博客、后台系统等
具体点来说,有以下几个场景:
写多读少:这个时候同步容器和并发容器差别不是很大
自定义复合操作:比如getLast等复合操作,因为同步容器都是单个操作进行上锁的,所以可以很方便地去拼接复合操作(记得外部加锁)
等等
参考内容:
- 《Java并发编程实战》
- 《实战Java高并发》
后记
最后,感谢大家的观看,谢谢
原创不易,期待官人们的三连哟
Java并发-同步容器篇的更多相关文章
- Java并发——同步容器与并发容器
同步容器类 早期版本的JDK提供的同步容器类为Vector和Hashtable,JDK1.2 提供了Collections.synchronizedXxx等工程方法,将普通的容器继续包装.对每个共有方 ...
- Java并发--同步容器
为了方便编写出线程安全的程序,Java里面提供了一些线程安全类和并发工具,比如:同步容器.并发容器.阻塞队列.Synchronizer(比如CountDownLatch).今天我们就来讨论下同步容器. ...
- Java并发—同步容器和并发容器
简述同步容器与并发容器 在Java并发编程中,经常听到同步容器.并发容器之说,那什么是同步容器与并发容器呢?同步容器可以简单地理解为通过synchronized来实现同步的容器,比如Vector.Ha ...
- 【JAVA并发第四篇】线程安全
1.线程安全 多个线程对同一个共享变量进行读写操作时可能产生不可预见的结果,这就是线程安全问题. 线程安全的核心点就是共享变量,只有在共享变量的情况下才会有线程安全问题.这里说的共享变量,是指多个线程 ...
- Java并发 - (无锁)篇6
, 摘录自葛一鸣与郭超的 [Java高并发程序设计]. 本文主要介绍了死锁的概念与一些相关的基础类, 摘录自葛一鸣与郭超的 [Java高并发程序设计]. 无锁是一种乐观的策略, 它假设对资源的访问是没 ...
- Java的同步容器和并发容器
前言: 之前在介绍Java集合的时候说到,java提供的实现类很少是线程安全的.只有几个比较古老的类,比如Vector.Hashtable等是线程安全的,尤其是Hashtable,古老到连命名规范都没 ...
- 转:java多线程--同步容器
java同步容器 在Java的集合容器框架中,主要有四大类别:List.Set.Queue.Map.List.Set.Queue接口分别继承了Collection接口,Map本身是一个接口.注意Col ...
- java并发-同步容器类
java平台类库包含了丰富的并发基础构建模块,如线程安全的容器类以及各种用于协调多个相互协作的线程控制流的同步工具类. 同步容器类 同步容器类包括Vector和Hashtable,是早期JDK的一部分 ...
- 【JAVA并发第三篇】线程间通信
线程间的通信 JVM在运行时会将自己管理的内存区域,划分为不同的数据区,称为运行时数据区.每个线程都有自己私有的内存空间,如下图示: Java线程按照自己虚拟机栈中的方法代码一步一步的执行下去,在这一 ...
随机推荐
- 微信支付 V3 的 Java 实现 Payment Spring Boot-1.0.7.RELEASE 发布
Payment Spring Boot 是微信支付V3的Java实现,仅仅依赖Spring内置的一些类库.配置简单方便,可以让开发者快速为Spring Boot应用接入微信支付. 功能特性 实现微信支 ...
- 后端程序员之路 22、RESTful API
理解RESTful架构 - 阮一峰的网络日志http://www.ruanyifeng.com/blog/2011/09/restful.html RESTful API 设计指南 - 阮一峰的网络日 ...
- LeetCode674. 最长连续递增序列
原题链接 1 class Solution: 2 def findLengthOfLCIS(self, nums: List[int]) -> int: 3 ans = begin = 0 4 ...
- Oracle VM VirtualBox的下载和安装
软件介绍 VirtualBox 是一款开源虚拟机软件,由德国 Innotek 公司开发,由Sun Microsystems公司出品的软件,使用Qt编写,在 Sun 被 Oracle 收购后正式更名成 ...
- 扫盲贴|如何评价一款App的稳定性和质量?
作者:友盟+移动开发专家 张文 「崩溃」与「卡顿」.「异常退出」等一样,是影响App稳定性常见的三种情况.相关数据显示,当iOS的崩溃率超过0.8%,Android的崩溃率超过0.4%的时候,活跃用户 ...
- 基于CefSharp开发浏览器(九)浏览器历史记录弹窗面板
一.前言 前两篇文章写的是关于浏览器收藏夹的内容,因为收藏夹的内容不会太多,故采用json格式的文本文件作为收藏夹的存储方式. 关于浏览器历史记录,我个人每天大概会打开百来次网页甚至更多,时间越长历史 ...
- PriorityQueue 是线性结构吗?90%的人都搞错了!
文章首发于「陈树义」公众号及个人博客 shuyi.tech 其实这个问题的完整描述是:Java 中的 PriorityQueue 实现,其数据的逻辑结构是线性结构吗?其数据的物理结构又是什么? 估计很 ...
- Harry And Magic Box HDU - 5155
题目链接:https://vjudge.net/problem/HDU-5155#author=0 题意:在一个n*m的方格中要满足每一行每一列至少有一个珠宝,问总共有多少种方案. 思路:利用递推的思 ...
- 巧用 SVG 滤镜还能制作表情包?
本文将介绍一些使用 SVG feTurbulence 滤镜实现的一些有趣.大胆的的动效. 系列另外两篇: 有意思!强大的 SVG 滤镜 有意思!不规则边框的生成方案 背景 今天在群里面聊天,看到有人发 ...
- 图像Resize方式对深度学习模型效果的影响
在基于卷积神经网络的应用过程中,图像Resize是必不可少的一个步骤.通常原始图像尺寸比较大,比如常见监控摄像机出来的是1080P高清或者720P准高清画面,而网络模型输入一般没有这么大,像Yolo系 ...