集合类不安全之并发修改异常
新建对象的过程中,底层新建了一个初始大小为10的obj数组。
size是数组里面的元素。
add方法的源码:
添加的时候先确认内部空间,让其加一。函数源码如下:
如果是默认的数组,没写大小,内部空间就是10,minCapacity是size,size是全局变量,jvm赋初始值0,加一之后就是1。default_capacity是全局静态变量,如下:
如果增加到了一定程度,需要通过ensureExplicitcapacity方法扩容。
扩充原来长度的一半。
arraylist线程不安全的例子,写一个。以add方法为例,演示线程不安全,因为add方法的源代码没有加锁。
结果2:
数量上不对了,内容上不对了,但是,没有报错!
线程改为30个之后,出现了经典的并发修改问题:
发生了JUC的并发修改异常,这是做并发项目经常遇到的。
作为一个程序员,解决问题的框架是4步:
发现故障现象,分析导致原因,提出解决方案,总结最佳实践(优化建议)。
故障现象就是那个异常了,导致原因就是arraylist不能支持并发,会有多线程的安全问题。
建立自己的故障笔记。只会撸代码走不远。天下武艺,没有高低之分,只有习武之人,有强弱之别。
arraylist没有加sync同步关键字,报了这个错,换成线程安全的vector,它的add方法加了sync关键字,结果就是:
没有出现异常。但是加锁,会让并发性急剧下降!
arraylist是jdk1.2版本出现的,vector是1.1版本。
我们可以提出方案是vector解决,就不会报异常了,但是不是最佳实践。
可以使用Collections包装类,来包装list为同步的,兼容并发。
也是可以解决的,这是方案二,但是底层还是加了sync关键字。性能下降是硬伤。
方案三:copyOnWriteArrayList,俗称写时复制。
先看运行效果,是可以解决问题的。
到此,不要只是会用,要明白底层原理!
笔试,过关说明你没有丧失学习能力,不一定能直接干活!
底层解析:
存储数据的数组是volatile修饰的,线程可见,禁止重排。在高并发场景里面,重要的核心变量都需要加volatile。
add方法解析:
源代码加了可重入锁(reentrantlock),先对原先的数组进行赋值,得到备份数组,然后对备份数组操作,此时其他线程可以并发读取,不影响,对备份数组操作完了之后,此处为添加一个数据,再把真实数组的引用指向备份数组,实现数据的更新,替换掉旧的数组。最后记得释放锁。
出现并发修改异常的原因解读:
类似签到,没有顺序,你争我抢,就可能在纸上划一长道。
阳哥笔记:
集合类不安全之set
hashset线程不安全代码展示
同样的配方,同样的味道。
解决方案
最直接的使用collections工具类,把hashset包装起来。
还有JUC写时复制set。
打开它的源码,你会发现,两套台子,一套班子,底层是用的写时复制arraylist。
hashset的底层数据结构就是hashmap,只保留key。
创建了一个初始大小是16,负载因子0.75的hashmap。
add方法hashset有1个参数,但是hashmap是有两个值的数据结构k-v。
hashset确实只用到了hashmap的k部分,v部分是一个obj常量。所以add方法只有一个参数即可。
集合类不安全值map
并发更新异常代码:
解决方案-并发map
用法跟hashmap没有区别。
同样的,用collections工具类也可以。
transferValue醒脑小练习
题目
运行结果:
需要弄明白这些变量再jvm里面的位置和变量作用域。
栈管运行,堆管存储。第一个age,是存在栈的方法里,值传递,相当于传递了复印件。基本类型传递复印件。
person的属性改变,改变堆里的的内容,传递的是引用,两个引用指向同一块地址,所以修改的是同一个对象。
string类型的变量,是因为string分两种,一种是new出来的,一种是直接等于字符串的。
因为string本身的特殊性,更改的时候,会去池子里找有没有这个xxx,没有就新建,这样方法里面的指针,和string原来的指针就对不上了。