如何在Java中使用同步容器

今天就跟大家聊聊有关如何在Java中使用同步容器,可能很多人都不太了解,为了让大家更加了解,小编给大家总结了以下内容,希望大家根据这篇文章可以有所收获。

Java可以用来干什么

Java主要应用于:1. web开发;2. Android开发;3. 客户端开发;4. 网页开发;5. 企业级应用开发;6. Java大数据开发;7.游戏开发等。

一、什么是同步容器

定义:就是把容器类同步化,这样我们在并发中使用容器时,就不用手动同步,因为内部已经自动同步了

例子:比如Vector就是一个同步容器类,它的同步化就是把内部的所有方法都上锁(有的重载方法没上锁,但是最终调用的方法还是有锁的)

源码:Vector.add

//通过synchronized为add方法上锁
publicsynchronizedbooleanadd(Ee){
modCount++;
ensureCapacityHelper(elementCount+1);
elementData[elementCount++]=e;
returntrue;
}

同步容器主要分两类:

1.普通类:Vector、Stack、HashTable

2.内部类:Collections创建的内部类,比如Collections.SynchronizedList、 Collections.SynchronizedSet等

那这两种有没有区别呢?

当然是有的,刚开始的时候(Java1.0)只有第一种同步容器(Vector等)

但是因为Vector这种类太局气了,它就想着把所有的东西都弄过来自己搞(Vector通过toArray转为己有,HashTable通过putAll转为己有);

源码:Vector构造函数

publicVector(Collection<?extendsE>c){
	//这里通过toArray将传来的集合转为己有
elementData=c.toArray();
elementCount=elementData.length;
//c.toArraymight(incorrectly)notreturnObject[](see6260652)
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构造函数

finalCollection<E>c;//BackingCollection
//这个就是锁的对象
finalObjectmutex;//Objectonwhichtosynchronize

SynchronizedCollection(Collection<E>c){
this.c=Objects.requireNonNull(c);
//初始化为this
mutex=this;
}

SynchronizedCollection(Collection<E>c,Objectmutex){
this.c=Objects.requireNonNull(c);
this.mutex=Objects.requireNonNull(mutex);
}

这里要注意一点就是,内部类的迭代器没有同步(Vector的迭代器有同步),需要手动加锁来同步

源码:Vector.Itr.next 迭代方法(有上锁)

publicEnext(){
synchronized(Vector.this){
checkForComodification();
inti=cursor;
if(i>=elementCount)
thrownewNoSuchElementException();
cursor=i+1;
returnelementData(lastRet=i);
}
}

源码:Collections.SynchronizedCollection.iterator 迭代器(没上锁)

publicIterator<E>iterator(){
//这里会直接实现类的迭代器(比如ArrayList,它里面的迭代器肯定是没上锁的)
returnc.iterator();//Mustbemanuallysynchedbyuser!
}

二、为什么要有同步容器

因为普通的容器类(比如ArrayList)是线程不安全的,如果是在并发中使用,我们就需要手动对其加锁才会安全,这样的话就很麻烦;

所以就有了同步容器,它来帮我们自动加锁

下面我们用代码来对比下

线程不安全的类:ArrayList

publicclassSyncCollectionDemo{

privateList<Integer>listNoSync;

publicSyncCollectionDemo(){
this.listNoSync=newArrayList<>();
}

publicvoidaddNoSync(inttemp){
listNoSync.add(temp);
}

publicstaticvoidmain(String[]args)throwsInterruptedException{
SyncCollectionDemodemo=newSyncCollectionDemo();
				//创建10个线程
for(inti=0;i<10;i++){
					//每个线程执行100次添加操作
newThread(()->{
for(intj=0;j<1000;j++){
demo.addNoSync(j);
}
}).start();
}
}
}

上面的代码看似没问题,感觉就算有问题也应该是插入的顺序比较乱(多线程交替插入)

但实际上运行会发现,可能会报错数组越界,如下所示:

如何在Java中使用同步容器

原因有二:

因为ArrayList.add操作没有加锁,导致多个线程可以同时执行add操作add操作时,如果发现list的容量不足,会进行扩容,但是由于多个线程同时扩容,就会出现扩容不足的问题

源码:ArrayList.grow扩容

//扩容方法
privatevoidgrow(intminCapacity){
//overflow-consciouscode
intoldCapacity=elementData.length;
				//这里可以看到,每次扩容增加一半的容量
			intnewCapacity=oldCapacity+(oldCapacity>>1);
if(newCapacity-minCapacity<0)
newCapacity=minCapacity;
if(newCapacity-MAX_ARRAY_SIZE>0)
newCapacity=hugeCapacity(minCapacity);
//minCapacityisusuallyclosetosize,sothisisawin:
elementData=Arrays.copyOf(elementData,newCapacity);
}

可以看到,扩容是基于之前的容量进行的,因此如果多个线程同时扩容,那扩容基数就不准确了,结果就会有问题

线程安全的类:Collections.SynchronizedList

/**
*<p>
*同步容器类:为什么要有它
*</p>
*
*@author:JavaLover
*@time:2021/5/3
*/
publicclassSyncCollectionDemo{

privateList<Integer>listSync;

publicSyncCollectionDemo(){
	//这里包装一个空的ArrayList
this.listSync=Collections.synchronizedList(newArrayList<>());
}

publicvoidaddSync(intj){
	//内部是同步操作:synchronized(mutex){returnc.add(e);}
listSync.add(j);
}

publicstaticvoidmain(String[]args)throwsInterruptedException{
SyncCollectionDemodemo=newSyncCollectionDemo();

for(inti=0;i<10;i++){
newThread(()->{
for(intj=0;j<100;j++){
demo.addSync(j);
}
}).start();
}

TimeUnit.SECONDS.sleep(1);
	//输出1000
System.out.println(demo.listSync.size());
}
}

输出正确,因为现在ArrayList被Collections包装成了一个线程安全的类

这就是为啥会有同步容器的原因:因为同步容器使得并发编程时,线程更加安全

三、同步容器的优缺点

一般来说,都是先说优点,再说缺点

但是我们这次先说优点

优点:

  • 并发编程中,独立操作是线程安全的,比如单独的add操作

缺点(是的,优点已经说完了):

  • 性能差,基本上所有方法都上锁,完美的诠释了“宁可错杀一千,不可放过一个”

  • 复合操作,还是不安全,比如putIfAbsent操作(如果没有则添加)

  • 快速失败机制,这种机制会报错提示ConcurrentModificationException,一般出现在当某个线程在遍历容器时,其他线程恰好修改了这个容器的长度

为啥第三点是缺点呢?

因为它只能作为一个建议,告诉我们有并发修改异常,但是不能保证每个并发修改都会爆出这个异常

爆出这个异常的前提如下:

源码:Vector.Itr.checkForComodification 检查容器修改次数

finalvoidcheckForComodification(){
//modCount:容器的长度变化次数,expectedModCount:期望的容器的长度变化次数
if(modCount!=expectedModCount)
thrownewConcurrentModificationException();
}

那什么情况下并发修改不会爆出异常呢?有两种:

1.遍历没加锁的情况:对于第二种同步容器(Collections内部类)来说,假设线程A修改了modCount的值,但是没有同步到线程B,那么线程B遍历就不会发生异常(但实际上问题已经存在了,只是暂时没有出现)

2.依赖线程执行顺序的情况:对于所有的同步容器来说,假设线程B已经遍历完了容器,此时线程A才开始遍历修改,那么也不会发生异常

代码就不贴了,大家感兴趣的可以直接写几个线程遍历试试,多运行几次,应该就可以看到效果(不过第一种情况也是基于理论分析,实际代码我这边也没跑出来)

根据阿里巴巴的开发规范:不要在 foreach 循环里进行元素的 remove/add 操作。remove 元素请使用 Iterator方式,如果并发操作,需要对 Iterator 对象加锁。

这里解释下,关于List.remove和Iterator.remove的区别

  • Iterator.remove:会同步修改expectedModCount=modCount

  • list.remove:只会修改modCount,因为expectedModCount属于iterator对象的属性,不属于list的属性(但是也可以间接访问)

源码:ArrayList.remove移除元素操作

publicEremove(intindex){
rangeCheck(index);
				//1.这里修改了modCount
modCount++;
EoldValue=elementData(index);

intnumMoved=size-index-1;
if(numMoved>0)
System.arraycopy(elementData,index+1,elementData,index,
numMoved);
elementData[--size]=null;//cleartoletGCdoitswork

returnoldValue;
}

源码:ArrayList.Itr.remove迭代器移除元素操作

publicvoidremove(){
if(lastRet<0)
thrownewIllegalStateException();
checkForComodification();

try{
	//1.这里调用上面介绍的list.romove,修改modCount
ArrayList.this.remove(lastRet);
cursor=lastRet;
lastRet=-1;
	//2.这里再同步更新expectedModCount
expectedModCount=modCount;
}catch(IndexOutOfBoundsExceptionex){
thrownewConcurrentModificationException();
}
}

由于同步容器的这些缺点,于是就有了并发容器(下期来介绍)

四、同步容器的使用场景

多用在并发编程,但是并发量又不是很大的场景,比如一些简单的个人博客系统(具体多少并发量算大,这个也是分很多情况而论的,并不是说每秒处理超过多少个请求,就说是高并发,还要结合吞吐量、系统响应时间等多个因素一起考虑)

具体点来说的话,有以下几个场景:

  • 写多读少,这个时候同步容器和并发容器的性能差别不大(并发容器可以并发读)

  • 自定义的复合操作,比如getLast等操作(putIfAbsent就算了,因为并发容器有默认提供这个复合操作)

  • 等等

总结

什么是同步容器:就是把容器类同步化,这样我们在并发中使用容器时,就不用手动同步,因为内部已经自动同步了

为什么要有同步容器:因为普通的容器类(比如ArrayList)是线程不安全的,如果是在并发中使用,我们就需要手动对其加锁才会安全,这样的话就很太麻烦;所以就有了同步容器,它来帮我们自动加锁

同步容器的优缺点:

优点独立操作,线程安全

缺点 复合操作,还是不安全,性能差快速失败机制,只适合bug调试

同步容器的使用场景

多用在并发量不是很大的场景,比如个人博客、后台系统等

具体点来说,有以下几个场景:

  • 写多读少:这个时候同步容器和并发容器差别不是很大

  • 自定义复合操作:比如getLast等复合操作,因为同步容器都是单个操作进行上锁的,所以可以很方便地去拼接复合操作(记得外部加锁)

看完上述内容,你们对如何在Java中使用同步容器有进一步的了解吗?如果还想了解更多知识或者相关内容,请关注恰卡编程网行业资讯频道,感谢大家的支持。

发布于 2021-05-10 20:34:21
收藏
分享
海报
0 条评论
166
上一篇:java中对象占用内存情况 下一篇:python爬虫如何爬取微博粉丝数据
目录

    0 条评论

    本站已关闭游客评论,请登录或者注册后再评论吧~

    忘记密码?

    图形验证码