多线程、高并发笔记(二):线程安全问题

线程安全问题

非线程安全主要是指多个线程对同一个对象的实例进行操作时,会出现被更改,值不同的情况。

线程安全问题表现为三个方面:

原子性、可见性、有序性

  • 原子性:

    不可分割的意思。原子操作的不可分割有两种含义:

a、访问(读写)某个共享变量的操作从其他线程来看,要么执行完毕,要么尚未发生,即其他线程看不到当前操作的中间结果。
b、访问同一组共享变量的原子操作时不能交错的

java有两种方式实现原子性: 锁、CAS指令

锁具有排他性,可以保证共享变量在某一时刻只能被一个线程访问
CAS指令直接在硬件层次(处理器和内存)上实现,看作是硬件锁

  • 可见性:

在多线程环境中,一个线程对某个共享变量进行更新之后,后续其他线程可能无法立即读到这个更新的结果。
如果一个线程对共享变量更新后,后续访问该变量的其他线程可以读到更新的结果,称这个变量的更新对其他线程可见。

  • 有序性:

是指在什么情况下一个处理器上运行的一个线程执行的内存访问操作在另一个处理器上运行的其他线程看来是乱序的。
乱序是指内存访问操作的顺序看起来发生了变化。

在多核处理器的环境下,编写的顺序结构,这种操作执行的顺序可能是没有保障的:
编译器可能会改变两个操作的先后顺序
处理器也可能不会按照目标代码的顺序执行

一个处理器上执行的多个操作,在其他处理器来看它的顺序与目标代码的顺序可能不一样,这种现象称为重排序。

重排序是对内存访问有序操作的一种优化,可以在不影响单线程程序正确的情况下提升程序的性能,但是可能对多线程陈虚谷的正确性产生影响,即可能导致线程安全问题。

不是必然出现的。

与内存操作顺序有关的几个概念:

源码顺序,就是源代码中指定的内存访问顺序
程序顺序,处理器上运行的代码所指定的内存访问顺序
执行顺序,内存访问操作在处理器上的实际执行顺序
感知顺序,给定处理器所感知到的该处理器及其他处理器的内存访问操作顺序

可以把重排序分为指令重排序与存储子系统重排序两种

指令重排序,主要是由JIT编译器,处理器引起的,指程序顺序与执行顺序不一样
存储子系统重排序是由高速缓存,写缓冲器引起的,感知顺序与执行顺寻不一致

  • 指令重排序

在源码顺序与程序顺序不一致,或者程序顺序与执行顺序不一致的情况下,我们就说发生了指令重排序(Instruction
Reorder).指令重排是一种动作,确实对指令的顺序做了调整, 重排序的对象指令.javac 编译器一般不会执行指令重排序, 而 JIT
编译器可能执行指令重排序.处理器也可能执行指令重排序, 使得执行顺序与程序顺序不一致.

  • 存储子系统重排序

存储子系统是指写缓冲器与高速缓存.高速缓存(Cache)是 CPU 中为了匹配与主内存处理速度不匹配而设计的一个高速缓存写缓冲器(Store
buffer, Write buffer)用来提高写高速缓存操作的效率

从处理器角度来看, 读内存就是从指定的 RAM 地址中加载数据到寄存器,称为 Load 操作; 写内存就是把数据存储到指定的地址表示的 RAM 存储单元中,称为 Store 操作.内存重排序有以下四种可能:

LoadLoad 重排序,一个处理器先后执行两个读操作 L1 和 L2,其他处理器对两个内存操作的感知顺序可能是 L2->L1

StoreStore重排序,一个处理器先后执行两个写操作W1和W2,其他处理器对两个内存操作的感知顺序可能是 W2->W1

LoadStore 重排序,一个处理器先执行读内存操作 L1 再执行写内存操作 W1, 其他处理器对两个内存操作的感知顺序可能是
W1->L1

StoreLoad重排序,一个处理器先执行写内存操作W1再执行读内存操作 L1, 其他处理器对两个内存操作的感知顺序可能是 L1->W1

貌似串行语义

JIT 编译器,处理器,存储子系统是按照一定的规则对指令,内存操作的结果进行重排序,
给单线程程序造成一种假象----指令是按照源码的顺序执行的.这种假象称为貌似串行语义. 并不能保证多线程环境程序的正确性。

数据依赖关系 --如果不存在数据依赖关系则可能重排序
如果不存在数据依赖关系则可能重排序

保证内存访问的顺序性

可以使用 volatile 关键字, synchronized 关键字实现有序性

Java 内存模型

每个线程都有独立的栈空间。

每个线程都可以访问堆内存 。

计算机的cpu不直接从主内存中读取数据,cpu读取数据,先把主内存的数据读到cache缓存中,把cache中的数据读到register寄存器中。

jvm中的共享数据可能会分配到寄存器中,每个cpu都有自己的寄存器,一个cpu不能读取其他cpu上寄存器中的内容,如果两个线程运行在不同的cpu上,而这个共享的数据被分配到寄存器上,会产生可见性问题。

即使jvm中的共享数据分配到主内存中,也不能保证数据的可见性,cpu不能直接堆主内存访问,而是通过cache高速缓存进行的。

一个处理器的cache不能直接读取另一个处理器的cache,但是一个处理器可以通过缓存一致性协议来读取其他处理器缓存中的数据,并将读取的数据更新到该处理器的cache中。

缓存同步

缓存同步使的一个处理器上运行的线程可以读取到另一个处理器上运行的线程对共享数据所作的更新,可以保障了可见性。为了保障可见性,必须使一个处理器对共享数据的更新最终被写入该处理器的cache中,这个过程称为冲刷处理器缓存。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值