一:现代计算机的高速缓存
在计算机组成原理中讲到,现代计算机为了匹配 计算机存储设备的读写速度 与 处理器运算速度,在CPU和内存设备之间加入了一个名为Cache的高速缓存设备来作为缓冲:将运算需要用到的数据从内存复制到cache中,CPU可以在运算期间对cache进行高速的读写操作,运算结束后在从cache把数据同步回内存。
Cache引出了一个新问题:缓存一致性。每个处理器有自己的cache,而他们又共享一个主内存。当多个处理器的运算任务都设计同一内存区域时,将会导致各自的缓存数据不一致。那么,在各自运算结束后,以谁的缓存数据为准同步回主存呢?此时,就需要 缓存一致性协议 来制定多个处理器对统一内存区域的读写操作了。
除了Cache,为了更好地利用处理器资源,处理器还会对代码进行 乱序执行 ,在最后才把结果重组使得结果与顺序执行一致。
二:Java内存模型的类比
类比与现代计算机的主存与cache,JVM中规定了 所有变量都存储在主内存中(类比计算机的主存),然后每条线程有自己的工作内存(类比每个处理器的cache)。线程的工作内存中保存了该线程需要用到的变量的拷贝值,线程在CPU上运行时都是对自己工作线程中的数据进行读写操作,运行结束之后才把数据同步化主内存中。那么类比于计算机使用 缓存一致性协议 解决缓存一致性问题,JVM中就需要线程同步机制来达到多线程对同一内存区域的读写控制了。
此外,Java编译器为了提高性能,采取了 指令重排序(类比计算机的 乱序执行),若多个线程都有语句对同一内存区域进行操作的话,有可能因为指令重排序而导致结果不符预料。因此,也需要线程同步机制来达到多线程对同一内存区域的读写控制。
三:主内存与工作内存的数据交互
JVM定义了8种操作来完成主内存与线程工作内存的数据交互:
1:lock:把主内存变量标识为一条线程独占,此时不允许其他线程对此变量进行读写。
2:unlock:解锁一个主内存变量。
3:read:把一个主内存变量值读入到线程的工作内存,强调的是读入这个过程。
4:load:把read到变量值保存到线程工作内存中作为变量副本,强调的是读入的值的保存过程。
5:use:线程执行期间,把工作内存中的变量值传给字节码执行引擎。
6:assign(赋值):字节码执行引擎把运算结果传回工作内存,赋值给工作内存中的结果变量。
7:store:把工作内存中的变量值传送到主内存,强调传送的过程。
8:write:把store传送进来的变量值写入主内存的变量中,强调保存的过程。
JVM要求以上8个操作都具有原子性,即对数据的读写操作具有原子性。但也有例外,即:long、double的非原子性协定:这两个64位类型的数据的读、写操作各需两次进行,一次读/写 32 位,这两次读/两次写 是不保证原子性的。
四:原子性、可见性、有序性
原子性:基本数据类型的读写操作是原子性的;更大范围的(代码块)的原子性可以用lock、unlock操作来实现(上锁后就只有一个线程来执行了,所以不会被其他线程打断原子操作),表现到代码层面就是使用syncrhoized同步块。
可见性:当一个线程修改了被多线程共享的一个主内存变量值时,其他线程能立刻知道这个修改。
我们在上面可以知道,JVM是通过工作内存中的变量值变化后,把新值同步会主内存,然后其他线程从主内存读取这个新值来实现可见性的。这里有个区别:普通变量的值变化后,不一定会立刻同步会主内存,而是会等线程执行完或者一段时间后才同步会,而且同步回主内存后,其他线程的工作内存也不一定会立刻读取新值。而被volatile关键字修饰的变量,一旦在工作内存中被修改,则立刻同步回主内存,并且其他使用了这个变量的线程的工作内存会立刻从主内存读取新值。而syncrhoized关键字修饰的变量由于一次只能有一个线程能使用,故一次也只能有一个工作线程读写它,所以也能“纵向”地实现可见性。
有序性:多线程之间对共享数据的操作的有序性,可以通过volatile和syncrhoized关键字来保证。volatile关键字禁止了指令重排序,而syncrhoized关键字规定了多个线程每次只能有一个线程对共享数据进行操作。
五:volatile关键字
一个volatile变量具有两种特性:可见性、禁止指令重排序。但是,volatile不具备原子性!原因是volatile变量的值可以被多线程交替修改,而修改包括了read、load、use、store、write等过程,这些过程不能保证是原子执行的。
可见性:被volatile关键字修饰的变量,一旦在工作内存中被修改,则立刻同步回主内存,并且其他使用了这个变量的线程的工作内存会立刻从主内存读取新值。
禁止指令重排序:volatile变量在赋值后会创建一个内存屏障:指令重排序时,位于后面的指令不能排到内存屏障之前。