一、现代计算机中的高速缓存
在计算机组成原理中讲到,现在计算机为了匹配计算机存储设备的读写速度与处理器运算速度,在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要求以上八个操作都具有原子性,即对数据的读写操作具有原子性。但也有例外,即long,double的非原子性协定:
这个两个64位类型的数据的读、写操作各需两次进行,一次读/写32位,这两次读/两次写是不保证原子性的。
四、原子性、可见性、有序性
原子性:基本数据类型的读写操作都是原子性的;更大范围的(代码块)的原子性可以用lock、unlock操作来实现(上锁之
后就只有一个线程来执行了,所以不会被其他线程打断原子操作),表现到代码层面就是使用sychronized同步
块。
可见性:当一个线程修改了被多线程共享的一个主内存变量值时,其他线程能立刻知道这个修改。
我们在上面已经知道,jvm是通过工作内存中的变量值变化后,把新值同步回主内存,然后其他线程从主内存
读取这个新值来实现可见性的。这里有个区别:普通变量的值变化后不一定会立刻同步回主内存中,而是会等
线程执行完成或者等待一段时间之后才同步回,而且同步回主内存之后其他线程也不一定会立刻读取新值,而
被voliate关键字修饰的变量,一旦在工作内存中被修改则立刻同步回主内存,并且其他使用了该变量的线程的
工作内存也会立刻从主内存中读取新值。而syncrhoized关键字修饰的变量由于一次只能有一个线程能使用,故
一次也只能有一个工作线程读写它,所以也能“纵向”地实现可见性。
有序性:多线程之间对共享数据的操作的有序性,可以通过volatile和syncrhoized关键字来保证。volatile关键字禁止了指
令重排序,而syncrhoized关键字规定了多个线程每次只能有一个线程对共享数据进行操作。
五、volatile
其实道理上讲同一实例的同一属性本身只有一个副本。但是多线程是会缓存值的,本质上,volatile就是不去缓存,
直接取值。在线程安全的情况下加volatile会牺牲性能。
volatile变量具有两种特性:可见性、禁止指令重排序。但是,volatile不具备原子性,因为volatile变量的值可以被
多线程交替修改,而修改包括了read、load、use、store、write等过程,这些过程不能保证是原子执行的。
可见性:被volatile关键字修饰的变量,一旦在工作内存中被修改,则立刻同步回主内存,并且其他使用了这个变量
的线程的工作内存会立刻从主内存读取新值。
禁止指令重排序:volatile变量在赋值后会创建一个内存屏障:指令重排序时,位于后面的指令不能排到内存屏障之前。
使用条件:
① 对变量的写操作不依赖于当前值
② 该变量没有包含在具体其他变量的不变式中
这些条件表明,可以被写入 volatile 变量的这些有效值独立于任何程序的状态,包括变量的当前状态。
第一个条件的限制使volatile变量不能用作线程安全计算器。虽然x++是一个简单的操作,但是实际上它是
一个由读取-修改-写入操作序列组成的组合操作,volatie对于操作不具有原子性,所以volatile不能保证在
x的值在操作期间保持不变。
使用场景:volatile一个使用场景是状态位;还有只有一个线程写,其余线程读的场景
与sychronized的比较:
volatile是轻量级锁,sychronized是重量级锁,且volatile不保证原子性,而sychronized保证原子性。