JMM
Java的内存模型分为栈(stack)和堆(heap), 栈空间是每个线程独有的,每个线程的栈空间中都会有多个方法栈桢,看下面的代码,
public static void main(String[] args) {
Object a = new Object();
}
这份代码运行起来后,jvm会启动一个名叫main的线程,同时为该线程分配栈空间,并运行一个名为main的方法栈桢,
而这个main方法运行后,栈桢中将会有一个类型为object,名为a的局部私有变量,
new Object() 这个对象将被创建到heap堆上去,在堆中的对象也可以被其他的地方的使用,但是栈桢中的局部私有变量,只有自己可以访问到,
例如下方的代码,test1和test2方法中,都有一个局部变量叫做i, 并且他们的状态值都是相互隔离的,但是他们可以访问同一个对象a,
Object a = new Object();
public void test1() {
int i = 0;
System.out.println(a);
}
public void test2() {
int i = 1;
System.out.println(a);
}
Java内存模型中,除了方法中的局部变量是线程私有的之外,除此之外的其他东西都是可以共享的,由于数据状态共享,就会引发一系列的问题,例如安全性,正确性等等,
Java内存模型的定义规范为: JSR-133: Java Memory Model and Thread Specification, 可以搜索一下该资料查看。
副本
虽然公有变量是可以共享的,但是为了发挥多核CPU线程的威力,公有变量在每个线程当中也是会存有副本的,
因为线程和内存之间同步数据的开销很大,CPU和自己的高速缓存取数据,比去内存当中取数据要快很多,每个线程在自己的工作内存当中会存有一个副本,定期和主内存(也就是真正存有这个数据的内存,就是堆)进行数据状态的同步,
由于每个线程都在自己的内存中持有数据副本,所以当多个线程对自己本地的副本做修改时,最终再同步到主内存中,就会出现由于副本数据不准确,导致的最终回写回去的数据也是错误的情况,
看下方代码:
static boolean cancel = false;
public static void main(String[] args) {
new Thread(() -> {
while (!cancel) {
}
System.out.println("我结束了");
}).start();
cancel = true;
}
笔者运行结果如下:
不停的在死循环,但是这种情况不是每次运行都会发生的,笔者这个死循环是运行了多次该程序出现的,
也就是说有部分几率会正常结束,有部分几率会死循环,读者可以自己测试一下,多运行几次,
这个是为什么呢? 这就是刚刚提到的副本引起的,由于死循环是在新的线程中,并且验证一个公有变量cancel,如果是false,就取消死循环,
我们在main线程汇总,设置了cancel = false,意味着让另外一个线程取消死循环,但这个设置并不是每次都生效,
有时候不会发生死循环,有时候发生死循环,正是因为main线程和newThread,都对这个cancel持有副本,
有的时候main线程修改了自己本地副本为false,但是这个副本的值没有被及时写会主内存,或者newThread没有及时从主内存中同步这个值,回到自己的副本,
这就导致了两把的cancel的值不同,程序的运行状态也无法达到预期的效果,如何解决这个问题?
volatile
刚刚的副本问题,我们可以使用valatile关键字进行修饰变量,可以保证变量副本引起的问题,
这个关键字的特性如下:
-
volatile修饰的变量,对其进行写入时,会直接写入主内存
-
volatile修饰的变量,对其进行读取时,会直接读取主内存
所以采用了这个关键字后,两个线程对该变量的读取和写入都同时使用了主内存的数据,而不是本地的副本,
所以很快的就可以看到对方所做的修改,并做出正确的反应,修改如下:
static volatile boolean cancel = false;
public static void main(String[] args) {
new Thread(() -> {
while (!cancel) {
}
System.out.println("我结束了");
}).start();
cancel = true;
}
但是需要注意的是,volatile保证的仅仅是可见性,也就是数据被改后的及时反应,不保证原子性,也就是多个线程对同一个变量进行修改状态时,不保证最终的结果一定是正确的,这个需要锁去做处理,
指令重排
指令重排, 编译器与CPU会在代码编译或运行的时候,对一些看起来毫不相关的代码,做顺序调整,以此达到更好的执行性能,
也就是你的代码可能在真正执行的时候,和你书写的顺序是不一样的,如果是这样的话,在某些特殊的情况下,可能会出现问题,看如下代码:
static boolean initFinished = false;
public static void main(String[] args) {
init();
initFinished = true;
// 开启一个线程监听初始化结束后,做一些操作
new Thread(() -> {
while (true) {
if (initFinished) {
// 执行结束, 做一些执行结束后的清理工作,并结束死循环
break;
}
}
}).start();
}
public static void init () {
// 一些初始化操作
}
上面的代码中,我们先进行初始化方法,初始化方法执行完毕以后,我们将initFinished设置为true,
随后另外一个线程监听到initFinished为true以后,做一些资源清理的处理,
但是,由于指令重排,我们上面的两行代码很有可能会被处理为:
由
init();
initFinished = true;
重排为
initFinished = true;
init();
也就是说CPU或者编译器认为我们的两行代码,毫无关联,因为优化执行效率,将两行互换了,
这时候会产生,initFinished变成true以后,初始化方法还没有执行,这时候另一个线程监听到initFinished为true,以为init方法已经执行完毕了,
进行一些事后清理操作,但此时init方法其实还没有执行结束,这就是指令重排带来的不便之处,
解决这个问题使用 volatile 关键字修饰 initFinished 变量就好,也就是说volatile可以禁止指令重排,
static volatile boolean initFinished = false;
.......
init();
// 在读之前产生内存屏障
initFinished = true;
// 在写之后产生内存屏障
.......
volatile 修饰的变量,在该变量从内存中读取前,或者写入后,产生一个内存屏障, 其作用是用来防止指令重排的 ,
所以当使用了volatile关键字后,给我们带来的好处就是,变量的及时可见性,以及防止指令重排把程序的顺序调整,带来的不便。
具有同步手段的时候无需volatile
当我们使用synchronized、Lock、AtomicInteger 等同步手段对一个变量进行处理的时候,就不在需要volatile了,
因为同步手段更为严格,除了保证可见性和重排的问题外,还可以保证原子性。