先不说这个关键字,先问你们一个问题,你们知道线程:
可见性
原子性
有序性
的这三个特行吗?
不理解,就继续看下文呗
Java 每一个线程在进行工作时,都有一个自己独有的工作内存,这个工作内存是线程独自访问的,其他线程访问不了,
每个线程进行读写数据时,都是先把主内存的数据copy一份数据副本到自己的工作内存,然后当这个线程需要读写修改这些数据时,会先在自己的工作内存读写,然后再把数据刷新到主内存去。但什么时候刷新到主内存呢?nobody can tell you......
因为涉及到的因素太多了
==============【可见性】==============
这个时候就有一个现象,比如:
主内存有一个属性isExit,
线程1读取了copy到自己工作内存,
线程2也copy变量isExit的数据副本到自己工作内存并改变了变量isExit的值。
比如下面代码:
private boolean isExit = false;
public void testVolati(){
new Thread(new Runnable() {
@Override
public void run() {
while (!isExit) {
//do something
}
}
}, "线程1").start();
new Thread(new Runnable() {
@Override
public void run() {
isExit = true;
//do something
}
}, "线程2").start();
}
如果这个 isExit 不需要实时性响应这个倒是没问题。如果需要一修改他的值,其他线程必须立马得知并必须刷新最新的值,那就有问题。
what???? 为什么呢?
因为线程2 虽然是修改了 isExit的值,但是线程2何时刷新到主内存,是不确定的。
如果线程1 在线程2修改值之前, 就已经 isExit (即:isExit = false ) 复制到自己的工作内存里,线程1不会再去主内存
读取这个值。所以线程1里面的值一直都是 isExit = false。所以线程1的执行功能一直在跑。虽然线程2已经改了这个isExit的值了。
这就是线程的 不可见性 ,一个线程修改了值,其他线程不一定立马得知
为了解决这个问题,Java提供了 volatile 这个一个关键字,只要被 volatile 修饰的属性,一旦被修改,所有线程都立马得知并会到主内存重新读取,进而刷新到自己的工作内存里去。比如修改一下上面的代码,对 isExit 用 volatile
private volatile boolean isExit = false;
public void testVolati(){
new Thread(new Runnable() {
@Override
public void run() {
while (!isExit) {
//do something
}
}
}, "线程1").start();
new Thread(new Runnable() {
@Override
public void run() {
isExit = true;
//do something
}
}, "线程2").start();
}
这个通过 volatile 修饰了isExit了,一旦线程2改了 isExit的值,线程1会立马得知并从主内存重新读取并刷新到自己工作内存的isExit。所以,通过 volatile 修饰的变量,具备 线程的可见性
==============【原子性】==============
嗯,我们现在再看看原子性这个是啥东东,我们先暂且不说这个,上个代码看看先
private volatile int num = 0;//累加的变量
private int count = 0;//完成线程累加
private int THREAD_COUNT = 20;//线程数目
public void textVo() {
//开启20个线程
for (int x = 0; x < THREAD_COUNT; x++) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
num++;
}
threadOK();
}
});
thread.start();
}
}
private void threadOK() {
count++;
//等待所有线程都执行完累加
if (count == THREAD_COUNT) {
Log.d(TAG, "num : " + num);
}
}
看代码,很简单,开启20个线程,每个线程对 变量num进行累加 10000,按道理20*10000,那结果应该是:200000
好,给你看下输出结果:
大概率的小于 我们期待的数目,what????? you kidding me ????
为什么会这样子的呢?来,我给你分析:
其实问题出现在 num++ 这一行代码里。我们都知道,Java语言最终还是编译成汇编语言,然后再通过汇编指令操作的。
num++ 这一行代码也不例外,最终还是会编译成N条汇编指令去执行,大概反编译出来的汇编指令:
Code:
Stack=2,Locals=0,Args_size=0,
0: getstatic #98;//
3: iconsts_1
4: iadd
5: putstatic #98
8: return
整个过程在汇编指令里大概就是:
1:先把数据读取出来,把这个值放在操作栈的顶部 (第0行)
2:对数据累加 (第4行)
3:把累加的数据返回去 (第8行)
那么这个时候就会出问题了,
比如:
1:我线程1 会把 num等于100的值读取出来,然后再把这个值放在操作栈的栈顶,这个时候CPU执行权被其他线程抢走
2:线程2 这个时候抢到CPU的执行权,然后 会把 num等于100的值读取出来,进行累加后把101返回去给num,此时num = 101
3:然后这个时候CPU执行权被线程1抢回去,注意,线程1操作栈栈顶的值还是100,然后对值类加后得到101,把值返回num
4:然后问题就很清晰,线程1和线程2都累加了1次,最终值只是加了1。
but,,,,你们可能会看到,num 使用了 volatile 修饰得,线2修改了之后,为什么线程1不会立马得知,再次去主内存读取新的数据呢?不是说具备 线程可见性吗?
你们有这个疑问是好的,但是很残忍的告诉你,汇编指令把值读取放在了操作栈的栈顶,你在其他线程对num这个变量怎么操作法,都和它没关系了。后面的累加操作指令都是原来栈顶的数据。
所以,这就是线程 的 原子性
原子性,就是说虚拟机在执行一个Java指令时,是否能保证 一个线程 必须执行完 一个Java 指令 后才能被其他线程 抢走CPU执行权。注意,是Java指令 ,
比如简单的 num++ 这么一行 java 代码,
很明显,volatile 关键字修饰的变量是不具备的,也就是说num变量不具备线程的原子性。
因为 它连一个 num++,这么一个简单自增的指令都不保证执行完才被其他线程强周CPU执行权。
其实,换一个角度说,可以认为,线程的运算功能,是无法保证原子性的。
==============【有序性】==============
好了,最后说一下有序性。
说下Java虚拟机的执行指令的行为,上个Demo说一下
int a = 0;//第1行
int b = 1;//第2行
int c = 2;//第3行
private void test() {
a++; //第4行
b++; //第5行
c = a + b;//第6行
}
就这么一个简单的几行代码。虚拟机是不保证
第4行 肯定会在 第5行 前面执行。虚拟机在编译后,会对Java重新优化排序。
只保证:执行到第6六行代码时,第4行和第5行必须已经执行完了。
也就是说,只保证最终的结果输出是正确的。不保证执行顺序和你写的是一致的。
好了,我看下不加 volatile 修饰的DoubleLock 单例模式会出啥问题
来,上个 不加 volatile 的单例模式,标准 DoubleLock 单例模式,
public class ThreadDemo {
private static ThreadDemo threadDemo;
public ThreadDemo getInstance() {
if (threadDemo == null) {
synchronized (ThreadDemo.this) {
if (threadDemo == null) {
threadDemo = new ThreadDemo();//第9行代码
}
}
}
return threadDemo;
}
}
如果不加 volatile 会出现线程安全问题,为什么不加 会出现线程安全问题呢
其实线程安全会出现在 第9行代码那里,其实第9行代码默认是拆分成三步去执行的,
1:在堆内存分配一个内存空间给 ThreadDemo的实例,(就是 new ThreadDemo() )
2:初始化对内存的实例 (调用 ThreadDemo 的构造方法 初始化实例 )
3: 在盏内存创建 threadDemo 变量,并指向 步骤1创建的地址(只要执行了这一步,threadDemo 就不是空)
很明显,单纯 threadDemo = new ThreadDemo() 一行代码,都分了3步。
更重要,就刚才所说的,就算是默认顺序,编译器也可能会优化代码执行顺序,
优化代码后的顺序可能是:1-2-3 也有可能是:1-3-2
1-2-3到时没啥问题。
我们看下 编译器把执行顺序改成了 1-3-2会出现啥问题,
我构造一个场景:
线程1 先进来,先执行 第9行代码,在执行完了 1-3,此时 threadDemo已经非空了。此时CPU执行权被线程2夺走
此时线程2获得CPU执行权, 进来获取单例,判断threadDemo 不等于空,直接取走拿去用,
这就出现问题。因为此时:threadDemo 只是单纯指向了一个内存地址,但这个地址存的数据还没初始化。用这个实例会出现很多不可预见的问题,比如空指针等等..........
所以为了解决这个问题,Java 给出关键字volatile ,用此关键字修饰的变量,告诉编译器:
在此变量前的代码,编译器必须保证先执行完。
在此变量后的代码,编译器必须保证后执行完。
拗口? 难理解吗?
我们写几行代码:
volatile int a = 1;
int b = 1;
int c = 1;
int b = 1;
private void test(){
b++;//第7行
c++;//第8行
a++;//第9行
b = c+b;//第11行
c = b+;10//第12行
}
变量a 用了 volatile 修饰,编译器保证
第7行和第8行代码必须在 第9行前面执行
第11行和第12行代码必须在 第9行后面执行
但不保证 第7行和第8行哪一行先执行
当然,也不保证 第11行和第12行哪一行先执行
volatile 修饰的变量,相当于一个分割线,前面的代码必须先于自己执行,后面必须后于自己执行
好了,我们说回刚才那个单例子模式,我们如果用 volatile 修饰了 threadDemo变量,就可以保证 步骤1和 步骤2必须先于步骤3执行,所以编译后的顺序必须是: 1-2-3,所以 在threadDemo指向内存地址后的 ,该堆内存地址的数据,肯定已经初始化好的了。所以不会出现线程安全问题。
所以正确的DoubleLock单例模式应该是这样的:
public class ThreadDemo {
private volatile static ThreadDemo threadDemo;
public ThreadDemo getInstance() {
if (threadDemo == null) {
synchronized (ThreadDemo.this) {
if (threadDemo == null) {
threadDemo = new ThreadDemo();//第9行代码
}
}
}
return threadDemo;
}
}
所以我们总结下:
用 volatile 修饰了的变量,
不具备线程原子性
具备线程可见性
具备线程有序性
以上代码亲测无问题,有问题请留言指正。。。谢谢