java成员变量线程安全问题的疑问

qq_45962712 2020-09-26 11:31:35
为什么这段代码会有线程安全问题呢 这里为什么会有线程安全问题啊,按着这样的调用,无论那个线程remove()的时候,集合里面都会有一个元素,因为是先加后remove 怎么可能发生执行到remove的时候,集合没元素,就算一个线程没加上,另外一个线程也加上了啊
...全文
12941 12 打赏 收藏 转发到动态 举报
AI 作业
写回复
用AI写文章
12 条回复
切换为时间正序
请发表友善的回复…
发表回复
qq_45962712 2020-11-17
  • 打赏
  • 举报
回复 1
引用 6 楼 冰思雨的回复:
ArrayList不是线程安全的链表。 线程安全的意思就是多个线程访问相同数据时,无论怎么访问,数据的状态都要保持一致(和一个线程单独访问的结果是一样的)。 比如说,有两个线程,一个线程对同一个整数变量自增运算100次,另一个线程对同一个整数变量自减50次,如果算法是线程安全的,那么,两个线程同时运行之后,结果和单独一个线程先自增100次再自减50次是一样的。这就要求,每一次的自增(自减)操作,都是线程安全的才可以。 楼主可能不太理解,为啥两个线程同时访问同一个内存变量会出现差错。这是由于线程切换和多CPU的缓存造成的。一般都解释为线程切换造成的。 这是一个比较讲究细节的过程,一个线程运行的时候,同一时间另一个线程可能没有处于运行状态。操作系统中有两三千个线程需要运行,CPU核心数也就那么几个,所以要轮着来使用。线程切换的时候,需要先将要换出去的线程进行现场保护,将当时的线程状态保存起来,包括CPU寄存器中的数据,会被压入线程栈中,之后再将切换进来的线程进行现场还原,从线程栈中把CPU寄存器的数据还原回去。我们知道,CPU做的算术运算和逻辑运算都是在寄存器之间做的,做完之后再回写到内存中。这样的话,寄存器的数据相当于内存数据的一个镜像。多个线程访问同一个内存数据,就会有多个镜像,那么,在线程切换的时候,镜像没来得及回写到内存就被压栈了,其他线程更改镜像有的就可以及时回写,等刚才那个没来得及回写的线程被恢复运行时,第一件事就是回写他上一次执行的镜像,那么,其他线程的镜像被覆盖掉了,最终造成了内存数据的不一致。 多CPU时,由于每块CPU都自带缓存cache,也会造成这种现象。 我之所以写这些,就是因为,楼主在表述问题时,把程序正在操纵的数据与链表混为一谈,细节上,两者是截然不同的东西,虽然,你的代码操纵的就是链表,但是,实际上,操纵的是链表里面的东西(内容结构)。链表被覆盖指的是链表的引用被其他东西覆盖了,和链表内部,指向链表元素的引用被其他东西所覆盖,是两个概念。而且,不单单是引用,链表的大小也会产生线程安全的问题,所以,整个链表就不是线程安全的
学到了,谢谢前辈了,最近看了深入理解计算机操作系统,发现自己底子太薄了,
八爻老骥 2020-10-09
  • 打赏
  • 举报
回复
多线程么,又不安顺序执行的。很有可能两个Add完了,size还是1,remove掉一个,再remove就报错了。

从源代码可以看出,Add的时候,并没有立即更新size,如果两个线程同时进入add,那么size的值是一样的,那么其中一个结算结果就会被另一个覆盖了,而不是在其基础上进行叠加。

public boolean add(E e) {
modCount++;
add(e, elementData, size);
return true;
}
DXF2020 2020-10-09
  • 打赏
  • 举报
回复
如果你想每个线程的这个list不相互影响,你就使用threadlocal去定义
qq_45962712 2020-09-30
  • 打赏
  • 举报
回复
引用 8 楼 qq_39936465的回复:
[quote=引用 4 楼 qq_45962712 的回复:] 那个前辈,你说刷新主存,如果线程按照你上面的流程执行的话,是不是当线程a刷新主存的时候,会把前面线程b所做的工作给覆盖掉,比如当线程b在add的时候,就刷新主存,之后这时候,集合有一个元素,并且size等于1,之后线程a再刷新主存,这时候就把集合就被刷没了,并且size也等于0了
我觉得这个刷新主存关系不大,如果一个线程是安全的那么它的操作应该都是原子性的。 而list的add和remove都是非原子性的,因为 list的add起码要有2个环节 1是list的size加1 2是向list中添加新的值。 当线程1在执行add操作时先从内存读取list的值,并在该值的基础上进行 list的size加1的操作同时另一个线程2也开始执行add操作,这时候线程2读取list的值非最新的值(线程1的add操作还没完成),线程2读取的list值和线程1读取的值是一样的。因此当线程1完成add操作把新的值返回给内存后,线程2也完成add操作后把新值返回给内存后,其实前后2次操作最终会造成只添加了1个值,线程1的结果被线程2所取代。 同理,remove也是这样。所以会出现加少了,或者减少了这种不可控的情况。[/quote] 一样啊,都是从当前线程的工作内存读取数据,工作内存并没有更新到主存,
qq_39936465 2020-09-30
  • 打赏
  • 举报
回复
引用 4 楼 qq_45962712 的回复:
那个前辈,你说刷新主存,如果线程按照你上面的流程执行的话,是不是当线程a刷新主存的时候,会把前面线程b所做的工作给覆盖掉,比如当线程b在add的时候,就刷新主存,之后这时候,集合有一个元素,并且size等于1,之后线程a再刷新主存,这时候就把集合就被刷没了,并且size也等于0了
我觉得这个刷新主存关系不大,如果一个线程是安全的那么它的操作应该都是原子性的。 而list的add和remove都是非原子性的,因为 list的add起码要有2个环节 1是list的size加1 2是向list中添加新的值。 当线程1在执行add操作时先从内存读取list的值,并在该值的基础上进行 list的size加1的操作同时另一个线程2也开始执行add操作,这时候线程2读取list的值非最新的值(线程1的add操作还没完成),线程2读取的list值和线程1读取的值是一样的。因此当线程1完成add操作把新的值返回给内存后,线程2也完成add操作后把新值返回给内存后,其实前后2次操作最终会造成只添加了1个值,线程1的结果被线程2所取代。 同理,remove也是这样。所以会出现加少了,或者减少了这种不可控的情况。
weixin_43845112 2020-09-28
  • 打赏
  • 举报
回复
引用 4 楼 qq_45962712 的回复:
[quote=引用 3 楼 qq_45962712的回复:][quote=引用 1 楼 weixin_43845112的回复:]主要问题是在于list不是线程安全的,可以这样理解:线程A主存读取当前size 0,add 1 remove 0,但是并不刷新主存,线程B主存读取size 0 add 1 刷新主存,此时线程A也刷新主存,导致最终的size是0 线程B再执行remove操作的时候,有可能先从主存同步数据,拿到size 0。 线程从主存同步数据和修改后将数据同步回去的时机是随机的
感谢,前辈您这种刷新主存的思想对我启发很大,我在最开始也知道是size的问题,我当时是看了add的源码,之后突然了有了思路,当时想的是一个线程添加了元素,但size没有更新,之后2次add其实都是添加在同一个位置上[/quote] 那个前辈,你说刷新主存,如果线程按照你上面的流程执行的话,是不是当线程a刷新主存的时候,会把前面线程b所做的工作给覆盖掉,比如当线程b在add的时候,就刷新主存,之后这时候,集合有一个元素,并且size等于1,之后线程a再刷新主存,这时候就把集合就被刷没了,并且size也等于0了[/quote] 我是这样理解的,你可以看下线程可见性的文章,大意就是对于共享变量,线程执行时会从主存拷贝一个副本,操作完成后会有个更新主存的过程,但是这个更新不是立即更新的,很容易就出现覆盖的情况
冰思雨 2020-09-28
  • 打赏
  • 举报
回复
ArrayList不是线程安全的链表。 线程安全的意思就是多个线程访问相同数据时,无论怎么访问,数据的状态都要保持一致(和一个线程单独访问的结果是一样的)。 比如说,有两个线程,一个线程对同一个整数变量自增运算100次,另一个线程对同一个整数变量自减50次,如果算法是线程安全的,那么,两个线程同时运行之后,结果和单独一个线程先自增100次再自减50次是一样的。这就要求,每一次的自增(自减)操作,都是线程安全的才可以。 楼主可能不太理解,为啥两个线程同时访问同一个内存变量会出现差错。这是由于线程切换和多CPU的缓存造成的。一般都解释为线程切换造成的。 这是一个比较讲究细节的过程,一个线程运行的时候,同一时间另一个线程可能没有处于运行状态。操作系统中有两三千个线程需要运行,CPU核心数也就那么几个,所以要轮着来使用。线程切换的时候,需要先将要换出去的线程进行现场保护,将当时的线程状态保存起来,包括CPU寄存器中的数据,会被压入线程栈中,之后再将切换进来的线程进行现场还原,从线程栈中把CPU寄存器的数据还原回去。我们知道,CPU做的算术运算和逻辑运算都是在寄存器之间做的,做完之后再回写到内存中。这样的话,寄存器的数据相当于内存数据的一个镜像。多个线程访问同一个内存数据,就会有多个镜像,那么,在线程切换的时候,镜像没来得及回写到内存就被压栈了,其他线程更改镜像有的就可以及时回写,等刚才那个没来得及回写的线程被恢复运行时,第一件事就是回写他上一次执行的镜像,那么,其他线程的镜像被覆盖掉了,最终造成了内存数据的不一致。 多CPU时,由于每块CPU都自带缓存cache,也会造成这种现象。 我之所以写这些,就是因为,楼主在表述问题时,把程序正在操纵的数据与链表混为一谈,细节上,两者是截然不同的东西,虽然,你的代码操纵的就是链表,但是,实际上,操纵的是链表里面的东西(内容结构)。链表被覆盖指的是链表的引用被其他东西覆盖了,和链表内部,指向链表元素的引用被其他东西所覆盖,是两个概念。而且,不单单是引用,链表的大小也会产生线程安全的问题,所以,整个链表就不是线程安全的
qq_39936465 2020-09-28
  • 打赏
  • 举报
回复
引用 楼主 qq_45962712 的回复:
为什么这段代码会有线程安全问题呢 这里为什么会有线程安全问题啊,按着这样的调用,无论那个线程remove()的时候,集合里面都会有一个元素,因为是先加后remove 怎么可能发生执行到remove的时候,集合没元素,就算一个线程没加上,另外一个线程也加上了啊
arraylist的add方法非原子性,只在最后才会返回新的arraylist值,如果2个线程同时进入add方法话,会产生2个不同的arraylist,这时候前一个arraylist会被后一个arraylist覆盖,造成add值的丢失。
qq_45962712 2020-09-27
  • 打赏
  • 举报
回复
引用 3 楼 qq_45962712的回复:
[quote=引用 1 楼 weixin_43845112的回复:]主要问题是在于list不是线程安全的,可以这样理解:线程A主存读取当前size 0,add 1 remove 0,但是并不刷新主存,线程B主存读取size 0 add 1 刷新主存,此时线程A也刷新主存,导致最终的size是0 线程B再执行remove操作的时候,有可能先从主存同步数据,拿到size 0。 线程从主存同步数据和修改后将数据同步回去的时机是随机的
感谢,前辈您这种刷新主存的思想对我启发很大,我在最开始也知道是size的问题,我当时是看了add的源码,之后突然了有了思路,当时想的是一个线程添加了元素,但size没有更新,之后2次add其实都是添加在同一个位置上[/quote] 那个前辈,你说刷新主存,如果线程按照你上面的流程执行的话,是不是当线程a刷新主存的时候,会把前面线程b所做的工作给覆盖掉,比如当线程b在add的时候,就刷新主存,之后这时候,集合有一个元素,并且size等于1,之后线程a再刷新主存,这时候就把集合就被刷没了,并且size也等于0了
qq_45962712 2020-09-27
  • 打赏
  • 举报
回复
引用 1 楼 weixin_43845112的回复:
主要问题是在于list不是线程安全的,可以这样理解:线程A主存读取当前size 0,add 1 remove 0,但是并不刷新主存,线程B主存读取size 0 add 1 刷新主存,此时线程A也刷新主存,导致最终的size是0 线程B再执行remove操作的时候,有可能先从主存同步数据,拿到size 0。 线程从主存同步数据和修改后将数据同步回去的时机是随机的
感谢,前辈您这种刷新主存的思想对我启发很大,我在最开始也知道是size的问题,我当时是看了add的源码,之后突然了有了思路,当时想的是一个线程添加了元素,但size没有更新,之后2次add其实都是添加在同一个位置上
子非鱼uu 2020-09-27
  • 打赏
  • 举报
回复
指令重排序?
weixin_43845112 2020-09-27
  • 打赏
  • 举报
回复
主要问题是在于list不是线程安全的,可以这样理解:线程A主存读取当前size 0,add 1 remove 0,但是并不刷新主存,线程B主存读取size 0 add 1 刷新主存,此时线程A也刷新主存,导致最终的size是0 线程B再执行remove操作的时候,有可能先从主存同步数据,拿到size 0。 线程从主存同步数据和修改后将数据同步回去的时机是随机的

62,635

社区成员

发帖
与我相关
我的任务
社区描述
Java 2 Standard Edition
社区管理员
  • Java SE
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧