JVM Java内存模型(JMM)

很多人将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的值为0getstatic),但是他恰巧在这个时刻他的时间片用完了,cpu就把他踢出去了,踢出去以后cpu开始执行线程2的代码,线程2的代码执行的还是getstatic,他也获取了静态变量i的值,也是0,因为线程1还没来得及修改。假设之后CPU又切换回了线程1,线程1准备了常量并执行了加法(iconst_1 iadd),然后将相加后的结果写回静态变量(putstatic),所以静态变量变成了1。这时cpu又把时间片分给了线程2,线程2也准备常量1(iconst_1),然后做了减法(isub),但线程2读到的i0,所以减的结果是-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 ++;
					}
				}
			}
		
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值