文章目录
线程安全问题出现的原因
程序最开始是静态的、原子的、顺序执行的,虽然CPU利用率不高(CPU总是能有时间偷懒),但是程序员需要操心的问题不多。为了提升效率,陆续引入进程和线程的概念,并且实现了相应的数据结构。程序变成了动态的、并发的、异步的、执行也不再是原子的了。CPU利用率上去了,程序员要操心的事情多了…
线程安全问题的根源可以被归纳为三点:原子性问题、可见性问题和有序性问题。
原子性
由于存在线程和进程切换,我们无法做到一气呵成的执行某段程序,因为总是存在无法避免的中断产生。这倒没什么,因为如果它们的数据如果是私有的,这种切换不会造成什么问题,肯定不会存在“一会儿聊QQ一会儿聊微信,然后QQ发出的消息跑到微信上了”,真正导致线程安全问题的是:多个线程/进程操作的不是它们私有的数据,而是共享数据!!!如果一个线程修改共享数据不是原子的,那么其他线程就会读到“改到一半”的数据——不是原子性,就意味着访问共享变量的操作不能被称之为一个事务,可能会产生读写冲突(脏读)、写写冲突(修改丢失)等
可见性
CPU访问内存需要的时钟周期,和CPU执行一条指令的时钟周期相比实在是太慢了,引入高速缓存就是为了弥补这种速度差,要求CPU减少直接访存的次数。
这虽然提升了CPU读取数据的效率,但是引入了可见性问题,每个CPU都有自己的缓存(L1/L2 ),因为多核处理器时代中,每个核心具有独立的缓存,这导致核心之间缓存同一共享变量的值可能是不一致的。
有序性
编译器和处理器都会对不同层次的指令进行重排序,来加速指令执行效率(如一次性执行所有的读操作后,再统一执行一次写操作),重排序优化的原则只能保证不影响单线程的执行结果,但是可能使多线程程序执行结果出现问题。
Java内存模型
因为不同的平台具有不同的CPU、缓存、操作系统等,JVM为了实现跨平台性,制定了统一规范,这其中就包含内存模型JMM。
JVM本质上是一组规范,是一堆接口,而不同的平台去实现这组规范,最终达成不同平台但功能一致的特点
JMM定义了线程如何访问共享变量的规则:
所有的共享变量存储在主内存中,而每个线程都具有独立的工作内存,各线程只能自己的工作内存操作变量,而且线程之间不能直接访问对方的工作内存,工作内存存放的主内存的副本,线程之间如果需要传递变量值,必须依靠主内存。
(不是操作共享变量,那就不存在线程安全问题了,没有讨论必要)
JMM就是对CPU-高速缓存-主存关系的抽象。JMM不管你的CPU和内存条是AMD还是Intel,它提供一个统一的视图,你用java写程序,就不要考虑操作系统和CPU那一套东西了,就看我给你提供内存模型JMM就可以了。
总结:JMM屏蔽了底层内存模型的差异与细节,使得java程序在各个不同底层软件和硬件平台上,都可以达到一致的内存访问效果。
JMM中的原子/可见/有序问题
既然JMM让我们“眼里只有它”,那么我们就来分析一下包上一层外衣之后,java程序存在哪些线程安全问题,依然是之前的三条:
【1】线程切换,访问共享变量的一组操作无法被看