很多人将Java内存结构
与Java内存模型
傻傻分不清,Java内存模型
是Java memory model(JMM)
的意思。简单地说,JMM
定义了一套在多线程的环境下读写共享数据(比如成员变量、数组
)时,对数据的可见性
、有序性
和原子性
的规则和保障。所以他跟Java内存结构
是没有什么关系。
原子性
问题分析
两个线程对初始值为0的静态变量一个做自增,一个做自检,各做50000次,结果是0吗?答案是:结果不一定是0。
public class Test {
static int i = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(
() -> {
for (int j = 0; j < 50000; j ++) {
i ++;
}
}
);
Thread t2 = new Thread(
() -> {
for (int j = 0; j < 50000; j ++) {
i --;
}
}
);
t1.start();
t2.start();
t1.join();// join()方法的作用就是让主线程等待子线程执行结束之后再运行主线程。
t2.join();
System.out.println(i);
}
}
运行后就出现各种结果,有时出现负数,有时出现正数,当然有时也会输出为0。这是因为Java中对静态变量的自增、自减并不是原子操作,即多线程时他们会被CPU交错执行。而所谓原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何context switch(切换到另一个线程
)。
例如对于i++
而言(i为静态变量
),实际会产生如下的JVM字节码指令:
getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
iadd // 加法(局部变量的i++调用的是iinc,直接在局部变量槽上执行,而静态变量是在操作数栈上执行。)
putstatic i // 将修改后的值存入静态变量i(在操作数栈加完后再put回静态变量)
而对应i--
也是类似:
getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
isub // 减法
putstatic i // 将修改后的值存入静态变量i
而Java的内存模型如下图(图片取自网络黑马,以下同):
内存模型由两部分组成,一部分叫主内存
,一部分叫工作内存
。但需要注意的是这里的主内存
、工作内存
不能和堆栈
混淆起来,像堆、栈
这样的是在Java内存结构上的说法,而这里的主内存
、工作内存
是指JMM里的说法。虽然名称有点相似,但是不要混淆。
像i
这样的静态变量(换句话说共享的变量信息
)他们是放在主内存
中的,而线程是在工作内存
中的。所以假如要完成上面的四行字节码,他的执行需要在主内存
和工作内存
中需要数据的交换。即getstatic
是把i
的值从主内存
中读到工作内存
的线程中,然后在工作内存
中完成了加法后,他又得把结果写会主存
中去。
如果是在单线程下,执行以上8行代码是顺序执行(不会交错
)就没有问题:
getstatic i // 线程1-获取静态变量i的值,线程内i=0
iconst_1 // 线程1-准备常量1
iadd // 线程1-自增 线程内i=1
putstatic i // 线程1-将修改后的值存入静态变量i 静态变量i=1
getstatic i // 线程1-获取静态变量i的值 线程内i=1
iconst_1 // 线程1-准备常量1
isub // 线程1-自减,线程内i=0
putstatic i // 线程1-将修改后的值存入静态变量i 静态变量i=0
但在多线程下,这8行代码可能交错执行。出现交错的原因是Java的线程模型(乃至整个操作系统的线程模型
)是一种抢先式多任务系统,就是线程呢会轮流拿到cpu的使用权,cpu会以时间片为单位,比如在时间片1把使用权交给线程1使用,在时间片2再把时间分给线程2执行,也就是多个线程轮流使用cpu。
比如出现负数的情况(假设i初始值为0,同下
):
getstatic i // 线程1-获取静态变量i的值,线程内i=0
getstatic i // 线程2-获取静态变量i的值 线程内i=0
iconst_1 // 线程1-准备常量1
iadd // 线程1-自增 线程内i=1
putstatic i // 线程1-将修改后的值存入静态变量i 静态变量i=1
iconst_1 // 线程2-准备常量1
isub // 线程2-自减,线程内i=-1
putstatic i // 线程2-将修改后的值存入静态变量i 静态变量i=-1
比如线程1获取到了i
的值为0
(getstatic
),但是他恰巧在这个时刻他的时间片用完了,cpu就把他踢出去了,踢出去以后cpu开始执行线程2的代码,线程2的代码执行的还是getstatic
,他也获取了静态变量i
的值,也是0
,因为线程1还没来得及修改。假设之后CPU又切换回了线程1,线程1准备了常量并执行了加法(iconst_1 iadd
),然后将相加后的结果写回静态变量(putstatic
),所以静态变量变成了1
。这时cpu又把时间片分给了线程2,线程2也准备常量1(iconst_1
),然后做了减法(isub
),但线程2读到的i
是0
,所以减的结果是-1
,然后写回静态变量(putstatic
)。所以虽然两个线程各进行了加一和减一,但结果却是-1
,因为线程2的结果覆盖了线程1加完后的结果。
也可能会出现正数,比如:
getstatic i // 线程1-获取静态变量i的值,线程内i=0
getstatic i // 线程2-获取静态变量i的值 线程内i=0
iconst_1 // 线程1-准备常量1
iadd // 线程1-自增 线程内i=1
iconst_1 // 线程2-准备常量1
isub // 线程2-自减,线程内i=-1
putstatic i // 线程2-将修改后的值存入静态变量i 静态变量i=-1
putstatic i // 线程1-将修改后的值存入静态变量i 静态变量i=1
虽然这八个字节码本意是执行一次加法和一次减法,但却出现了正数结果。
以上是在多线程的情况下,由于指令交错而产生的问题的分析。
解决方法
在Java内存模型中,通过synchronized
关键字来保证原子性。语法如下:
synchronized(对象) {
要作为原子操作代码
}
这样写的话,比如线程1来了,他就会被“对象”加锁,加锁以后,他可以安全的去执行同步代码块儿内的代码。这时如果有线程2过来想执行同步代码块儿内的代码的话,他就执行不了了,他就会等待线程1释放“对象”所加的锁,也就是说线程1把同步块儿内的代码都运行完毕,线程1就会把这个锁释放开,那其他的线程才会有机会去争抢“对象”的锁。即同一时刻,只有一个线程能进入同步代码块儿,这样就保证了同步代码块儿内的这些代码的原子性。
public class Test {
static int i = 0;
// 定义一个静态Object对象
static Object obj = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(
() -> {
for (int j = 0; j < 50000; j ++) {
synchronized(obj) {
i ++;
}
}
}