JAVA volatile关键字以及CAS算法详解(代码加图解)

本次博客从JVM内存的角度出发,着重解释volatile可见性操作原子性以及CAS算法,对遇到的问题,逐步解决。制作不易,如果有什么问题,可以在该博客下提问,大家一起探讨一下。

一、volatile可见性

我们知道,在JVM中,内存的逻辑分配,分为线程共享区与线程独占区(放一张JVM内存结构图)

注:右边较深的紫色就是线程独占区,而左边(方法区,堆)则属于线程共享区

代码:

package com.liu.classLoader;
/*
 * 代码功能十分简单:
 * 1.开启两个线程,一个是main线程,一个是自定义线程
 * 2.同时访问同一个变量flag
 * 3.自定义线程等待200ms,再修改flag的值
 * 4.main线程一直循环,直到flag的值为true就打印并退出循环
 */
public class Add {
	public static void main(String[] args) {
		ThreadDemo td = new ThreadDemo();
		new Thread(td).start();
		while(true) {
			if (td.isFlag()) {
				System.out.println("flag="+td.isFlag());
				break;
			}
		}
	}
}
class ThreadDemo implements Runnable{
	private boolean flag = false;
	@Override
	public void run() {
		try {
			Thread.sleep(200);
		} catch (InterruptedException e) {
		}
		flag =true;
		System.out.println("flag=" + isFlag());
	}
	public boolean isFlag() {
		return flag;
	}
}

结果如下:

正常情况下,应该自定义线程设置flag为true之后,main线程打印falg=true之后,程序就可以退出了。

代码解释:

在第四步中,main线程在自己缓存当中存储了flag之后,由于while(true)执行的非常快,导致flag并不能更新为主存当中的值,这样就导致了主存当中的flag不能被main线程察觉,因此flag对于main来说不可见

注:5,6步是自定义线程的执行过程,可随时发生,但一定发生在main读取flag之后,因为自定义线程sleep了200ms

解决方案:

采用volatile对flag进行标记,解决了可见性的问题

运行结果就变了

可见性解释:

对于这个问题较为简单的方法就是,如果我们所有线程对变量的操作,都是在线程共享区当中进行的,那么,我们就能避免这个问题,而添加volatile就是这么做的:通过添加volatile对变量进行修饰,使得多个线程在对该变量操作时,不再创建线程对其的缓存,而是直接在线程共享区进行操作。

反思:

1.既然在存在如上的问题,为什么JVM不直接采用可见性,这样就省得我们再多去写代码,为什么还会有缓存机制呢?

问题解释:如果我们所有的线程都是去读取数据,如果不用缓存的话,线程每次需要数据都得去线程共享区读取数据,效率比使用缓存机制肯定是要低的。

2.采用volatile除了效率问题,就没有其他方面的问题了吗?

问题解释:阅读接下来的文章。

二、i++问题以及操作原子性

在学习C语言的时候,相信很多人都会遇到这样的问题        i++与++i的区别在哪里?

区别在于,i++是先赋值后++;而++i是先++再赋值。

转成相应的代码,我们可以这么写:

//i = i++
int temp = i
i += i
i = temp
//i = ++i
int temp += i
i = temp

基于此,我们写出如下代码

代码:

package com.liu.classLoader;
/*
 * 代码功能:
 * 创建2000个线程,对i类型进行读写读的操作
 */
public class Add {
	public static void main(String[] args) {
		ThreadDemo td = new ThreadDemo();
		for (int i = 0; i < 2000; i++) {
			new Thread(td).start();
		}
	}
}
class ThreadDemo implements Runnable{
	private volatile int i = 0;
	@Override
	public void run() {
		try {
			Thread.sleep(20);
		} catch (Exception e) {
		}
		i++;
		System.out.println(Thread.currentThread().getName() + ":"+ i);
	}
}

结果如下:

我们发现,多个不同线程,操作结果都是一样的,很明显的发生了线程不安全的情况,我们是使用volatile对变量进行了修饰,但并不管用。

因此,我们可以说,volatile保证了变量可见性,但却不能保证操作的原子性

线程不安全产生原因:

解决方案:

1.既然这个问题是由于同步的问题产生的,那么我们对i变量加锁就行了吧!

可以是可以,但是,这样一想,我们如果对i变量加锁,那不就相当于是在单线程执行了吗?所有的线程都等待着某一个线程结束,然后再去抢占i资源,效率比单线程还低。(当然,如果我们是用for语句来做这道题的话,当然是效率非常高的,但是,本次博客是希望找出多线程的解决方案)

2.采用CAS算法以及对应的工具,就能轻松解决了。

首先,什么是CAS算法?请看下一章节。

其次,使用相关工具类(java.util.concurrent.atomic.*)。代码及运行结果如下:

代码:

运行结果:

这样就达到我们想要的结果了。

三、CAS算法

算法过程:

当我们想要同步修改某一值的时候,我们不去监控并通知别的程序(加锁)不能修改这一个值,我们只需判断,在我们修改值的期间,该变量是否被其他线程修改过。

1.在共享区获取值的时候,将这一时刻变量值记录下来,称之为oldValue

2.进行我们的操作

3.将oldValue与线程共享区的值进行比较,相等,就说明,在我们操作这段期间,别人没有修改过这个值;反之,不等,就证明别人修改过这个值,我们的计算已经是无效的了,就重新进行第一步,直到成功。

算法好处:

这样做,与加锁,有什么区别呢?

1.如果我们这么做的话,那么,所有线程对变量的操作是一直进行的,而加锁,其他的线程对变量的操作是禁止的。

2.并且,我们无需线程调度算法去实现我们的线程的控制,每一项操作,都可以在当前线程中独立的完成。

 

如果有想详细了解CAS算法的,请看https://www.jianshu.com/p/21be831e851e(转),讲的很好。

 

JAVA多线程的知识还很多。。。。希望各位带佬能多多点个赞啥的,感谢!!!!

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值