0、引言
在juc多线程并发编程中,常常需要关注线程的“可见性”与“有序性”。本文将详细介绍这两部分内容,以及volatile关键字的使用。
阅读本文前需要一些jvm运行时内存、进程与线程、共享内存、锁等相关知识。
1、可见性
1.1 定义
定义:
当一个对象在多个内存中都存在副本时,如果一个内存修改了共享变量,其它线程也应该能够看到被修改后的值。
1.2 解释
解释这句话,首先我们要知道,进程是操作系统分配资源的最小单位,在每个进程里,都包含有数据共享的区域(例如其中有成员变量、堆、静态常量等,详情查阅jvm运行时内存),并可能包含多个线程。
在jvm底层,每个线程想要操作某个共享变量时,不能够直接操作共享区域的“主存”,而是要先把数据从主存读到线程自己的高速缓存中,再进行操作,再赋值回去。
“具体来说,当一个线程要读取某个变量的值时,它首先检查自己的工作缓存中是否存在该变量的副本。
如果存在,则直接读取副本的值;
如果不存在,则从主内存中获取该变量的最新值,并将其复制到自己的工作缓存中。
类似地,当一个线程要修改某个变量的值时,它首先会在自己的工作缓存中修改副本,再根据一定的策略将变化同步回主内存。”
那么可见性的概念相应而来:多个线程都存了同一个对象副本,如果此时有一个内存修改了共享变量,其他的线程应该能够“看到”,而不是傻b一样仍旧拿着自己缓存里的值操作了。
1.3 实例说明
例如下面情况,如果main 线程对 run 变量的修改对于 t 线程不可见,导致了 t 线程无法停止:
其原因是t线程频繁从主内存中读取 run 的值,JIT 编译器会将 run 的值缓存至自己工作内存中的高速缓存中, 减少对主存中 run 的访问。
那么,当主线程(也就是其他线程)把run变为false,由于子线程t(当前线程)仍然从高速缓存里读的run,会认为run并没有改变,从而逻辑出现问题。
1.4 解决方法:volatile(易变关键字)
作用:volatile可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值。(关键点)
然而 volatile 并不能保证原子性,例如不加锁的多线程执行函数,对成员变量i进行i++ i--,最后得到的结果i不一定是不变的。
因为volatile 关键字,只能让该线程在使用数据前强制从主存读取,但读取后是否发生改变是看不见的。如果有别的线程在该线程读取后成功改变了共享内存的值,还是会发生指令交错。
2、有序性
在多线程环境下,由于编译器和处理器对指令进行优化和重排序,可能会导致操作的执行顺序发生改变,从而违反了代码编写的原始顺序。
为了保证有序性,可以使用以下方法:
使用volatile关键字:对关键的共享变量使用volatile关键字进行声明,以确保对该变量的写操作立即对其他线程可见。
使用synchronized关键字或Lock机制:通过使用同步块或锁来保护共享资源的读写操作,确保线程之间的有序访问。
使用原子类:Java提供了一些原子类(如AtomicInteger、AtomicLong等)来进行原子操作,这些原子类的方法能够保证操作的原子性、可见性和有序性。
简单来说volatile避免了指令重排,也就避免了多线程中可能产生的问题。