本次博客从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多线程的知识还很多。。。。希望各位带佬能多多点个赞啥的,感谢!!!!