Java 并发编程问题
在并发编程中,我们通常会遇到以下三个问题:原子性问题,可见性问题,有序性问题。这些问题发生的原因是 Java 的内存模式决定的。我们先看看 Java 的内存模型,再来解释一下上面的三个问题。
Java 内存模型
Java内存模型规定所有的变量都是存在主存当中(堆内存),每个线程都有自己的工作内存(方法栈)。线程对变量的所有操作都必须在工作内存中进行,而不能直接对主存进行操作。并且每个线程不能访问其他线程的工作内存。并且在工作内存中进行的操作并不是实时写入到主内存中的。
原子性
原子性:即一个操作或者多个操作
要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。经典转账问题不是原子性操作(因为并不是一步操作)
x = 10; // 是原子性操作,不需要读取 x 的值,只进行赋值到内存,一步完成,所以说原子性操作
y = x; // 不是原子性操作,需要第一步读取 x 的值,第二步为 y 赋值到内存中,所以不是原子性操作
在Java中,对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行。只有简单的读取、赋值(而且必须是将数字赋值给某个变量,变量之间的相互赋值不是原子操作)才是原子操作。
Java内存模型只保证了基本读取和赋值是原子性操作,如果要实现更大范围操作的原子性,可以通过 synchronized 和 Lock 来实现。由于 synchronized 和 Lock 能够保证任一时刻只有一个线程执行该代码块,那么自然就不存在原子性问题了,从而保证了原子性。
可见性
可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,会马上写入到主内存,其他线程能够立即看得到修改的值。
解释:可见性是指,一个线程改变了这个变量的值,这个值会马上写入到主内存,其他线程访问时会得到已经改变了的这个值
对于可见性,Java 提供了 volatile 关键字来保证可见性。
当一个共享变量被 volatile 修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,会在内存中读取到最新值。
而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。
另外,通过 synchronized 和 Lock 也能够保证可见性,synchronized 和 Lock 能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。
当线程 1 修改了一个变量的值,会马上写入内存,当线程 2 读取时会读取到最新的
当线程 1 读取了一个变量的值,接着线程 1 阻塞,线程 2 接着读取并修改该变量的值,线程 1 再操作时不会收到提示,会操作旧的值 由于线程 1 中的操作可能不是原子操作,所以可能出现上述问题
有序性
有序性:即程序执行的顺序按照代码的先后顺序执行。当代码执行顺序不同时,多线程就会出现问题。
//线程1:
context = loadContext(); //语句1
inited = true; //语句2
//线程2:
while(!inited ){
sleep()
}
doSomethingwithconfig(context);
如果线程1修改 inited 为 true 但是没有执行语句1,线程2中使用 context 就会出问题。
在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
以上就是由于 Java 的内存模型引起的并发编程时遇到的三个问题,这三个问题的解决 Java 也提供了量中场景的机制,使用 volatile 关键字或者使用 synchronized 同步锁
一、volatile
一旦一个共享变量(类的成员变量、类的静态成员变量)被 volatile 修饰之后,那么就具备了两层语义:
-
保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
-
禁止进行指令重排序。
注意:volatile 不能保证原子性
volatile 可见性
这段代码是很典型的一段代码,很多人在中断线程时可能都会采用这种标记办法。但是事实上,这段代码会完全运行正确么?即一定会将线程中断么?不一定,也许在大多数时候,这个代码能够把线程中断,但是也有可能会导致无法中断线程(虽然这个可能性很小,但是只要一旦发生这种情况就会造成死循环了)。
//线程1
boolean stop = false;
while(!stop){
doSomething();
}
//线程2
stop = true;
下面解释一下这段代码为何有可能导致无法中断线程。在前面已经解释过,每个线程在运行过程中都有自己的工作内存,那么线程1在运行的时候,会将stop变量的值拷贝一份放在自己的工作内存当中。
那么当线程2更改了stop变量的值之后,但是还没来得及写入主存当中,线程2转去做其他事情了,那么线程1由于不知道线程2对stop变量的更改,因此还会一直循环下去。
但是用volatile修饰之后就变得不一样了:
第一:使用volatile关键字会强制将修改的值立即写入主存;
第二:使用volatile关键字的话,当线程2进行修改时,会导致线程1的工作内存中缓存变量stop的缓存行无效(反映到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存行无效);
第三:由于线程1的工作内存中缓存变量stop的缓存行无效,所以线程1再次读取变量stop的值时会去主存读取。那么线程1读取到的就是最新的正确的值。
自增并不是原子性操作,所以多个线程同时操作一个内存中的一个数字自增操作,使用 volatile 修饰该对象时会出现问题
volatile 有序性
-
当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;
-
Java 在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。
volatile 使用场景
- 状态标记量
- double check 单例模式时使用,使用 volatile 目的是为了保证可见性,当一个线程中对对象有修改之后其他线程可见。
二、使用 synchronized 同步锁
在需要同步的时候,第一选择应该是synchronized关键字,这是最安全的方式,尝试其他任何方式都是有风险的。使用同步锁可以实现解决原子性、可见性、有序性等问题
http://www.cnblogs.com/dolphin0520/p/3920373.html
http://blog.csdn.net/ns_code/article/details/17290021
网友评论