多线程—— 内存可见性
Posted Tiger Expensive
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了多线程—— 内存可见性相关的知识,希望对你有一定的参考价值。
一、可见性
多个线程对同一个变量(称为:共享变量)进行操作,但是这多个线程有可能被分配到多个处理器中运行,那么编译器会对代码进行优化,当线程要处理该变量时,多个处理器会将变量从主存复制一份分别存储在自己的存储器中,等到进行完操作后,再赋值回主存。
这样做的好处是提高了运行的速度,同样优化带来的问题之一是变量可见性——如果线程t1与线程t2分别被安排在了不同的处理器上面,那么t1与t2对于变量A的修改时相互不可见,如果t1给A赋值,然后t2又赋新值,那么t2的操作就将t1的操作覆盖掉了,这样会产生不可预料的结果。因此,需要保证变量的可见性(一个线程对共享变量值的修改,能够及时地被其它线程看到)。
注意:共享数据的访问权限必须定义为private
多线程操作共享变量实现可见性过程JVM的内存模型如下:
所有的变量都存储在主内存中每个线程都有自己独立的工作内存,里面保存该线程使用到的变量的副本(主内存中该变量的一份拷贝)。
JVM模型两条规定:
1、线程对共享变量的所有操作必须在自己的内存中进行,不能直接从主内存中读写
2、不同线程之间无法直接访问其它线程工作内存中的变量,线程间变量值的传递需要通过主内存来完成
那么共享变量应该怎样实现可见性呢?
比如:线程1对共享变量的修改想要被线程2及时看到,必须经过如下2步操作:
把工作内存1中的更新过的共享变量刷新到主内存中,再将主内存中最新的共享变量的值更新到2的工作内存中。
初始值X=0,线程1将X=1,到其他线程X值的更改,如下图的更改过程。
因此,要实现共享变量的可见性,必须保证两点:
线程修改后的共享变量值能够及时从工作内存刷新到主内存中;
其他线程能够及时的把共享变量的最新值从主内存更新到自己的工作内存中。
在Java语言层面支持的可见性实现原理方式有synchronize和volatile。
备注:
导致共享变量在线程间不可见的原因:
线程的交叉执行;
重排序结合线程的交叉执行;
共享变量更新后的值没有在工作内存与主内存及时更新。
二、synchronize
能够实现代码的原子性(同步)和 内存的可见性。
原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
原子性就像数据库里面的事务一样,他们是一个团队,同生共死。其实理解原子性非常简单,我们看下面一个简单的例子即可:
i = 0;---1j = i ; ---2i++; ---3i = j + 1; ---4
上面四个操作,有哪个几个是原子操作,那几个不是?如果不是很理解,可能会认为都是原子性操作,其实只有1才是原子操作,其余均不是。
1—在Java中,对基本数据类型的变量和赋值操作都是原子性操作;
2—包含了两个操作:读取i,将i值赋值给j
3—包含了三个操作:读取i值、i + 1 、将+1结果赋值给i;
4—同三一样
在单线程环境下我们可以认为整个步骤都是原子性操作,但是在多线程环境下则不同,Java只保证了基本数据类型的变量和赋值操作才是原子性的(注:在32位的JDK环境下,对64位数据的读取不是原子性操作*,如long、double)。要想在多线程环境下保证原子性,则可以通过锁、synchronized来确保。
JVM对其的两条规定:
线程解锁前,必须把共享变量的最新值刷新到主内存中;
线程枷锁前,将清空工作内存中的共享变量的值,从而使用共享变量时需要从主内存中重新读取新的值。(枷锁与解锁需要是同一把锁)。
线程解锁前对共享线程变量的修改在下次枷锁时对其他线程可见。
线程 执行互斥代码的过程:
1、获得互斥锁;
2、清空工作内存;
3、从主内存拷贝变量的最新的值到工作内存;
4、执行代码;
5、将更改后的共享变量的值刷新到主内存;
6、释放互斥锁。
三、volatile
保证变量的可见性,不能保证变量的符合操作原子性。
实现内存可见:
深入的说:通过加入内存屏障和禁止重排序优化实现。对其变量执行写操作时,会在写操作后加入一条store屏障指令;对其进行读操作时,会在读操作前加入一条load屏障指令。
通俗的说:volatile变量在每次被线程访问时,都强迫从主线程中重读该变量的值,而当该变量发生变化时,又会强迫线程将最新的值刷新到主内存中,这样任何时刻,不同的线程总能看到该变量最新的值。
线程写volatile变量的过程:
1、改变线程工作内存中volatile变量副本的值;
2、将改变后的副本的值从工作内存刷新到主内存。
3、线程读volatile变量的过程:
4、从主内存中读取volatile变量的最新值到线程的工作内存中;
5、从工作内存中读取volatile变量的副本。
验证 volatile 可以保证原子性。代码如下:
/**
* 验证 volatile 是否保证原子性
* @author Administrator
*
*/
public class VolatileDemo {
private volatile int number = 0;
public int getNumber(){
return this.number;
}
public void increase(){
try {
//更好的输出效果
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
this.number++;
}
/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
final VolatileDemo volDemo = new VolatileDemo();
//实现500次自增
for(int i = 0 ; i < 500 ; i++){
new Thread(new Runnable() {
@Override
public void run() {
volDemo.increase();
}
}).start();
}
//如果还有子线程在运行,主线程就让出CPU资源,
//直到所有的子线程都运行完了,主线程再继续往下执行
while(Thread.activeCount() > 1){
Thread.yield();
}
System.out.println("number : " + volDemo.getNumber());
}
}
上述代码,理想情况或单线程时输出结果应该为:500,然而实际输出的结果是小于500的数字(多执行几遍)。这是因为 number++ 不是原子操作,会造成多个线程交叉执行。
方法一: synchronized
public void increase(){
try {
//更好的输出效果
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
synchronized(this){
this.number++;
}
}
当然,也可以 public synchronized int increase(){...} 但是这样造成程序性能更加低效。
方法二: java.util.concurrent.locks.ReentrantLock
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class VolatileDemo {
private Lock lock = new ReentrantLock();
private int number = 0;
public int getNumber(){
return this.number;
}
public void increase(){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
lock.lock();
try {
this.number++;
} finally {
lock.unlock();
}
}
/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
final VolatileDemo volDemo = new VolatileDemo();
for(int i = 0 ; i < 500 ; i++){
new Thread(new Runnable() {
@Override
public void run() {
volDemo.increase();
}
}).start();
}
//如果还有子线程在运行,主线程就让出CPU资源,
//直到所有的子线程都运行完了,主线程再继续往下执行
while(Thread.activeCount() > 1){
Thread.yield();
}
System.out.println("number : " + volDemo.getNumber());
}
}
方法三:java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicInteger;
public class VolatileDemo {
private static AtomicInteger a = new AtomicInteger();
public int getNumber(){
return a.get();
}
public void increase(){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
a.getAndIncrement();
}
/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
final VolatileDemo volDemo = new VolatileDemo();
for(int i = 0 ; i < 500 ; i++){
new Thread(new Runnable() {
@Override
public void run() {
volDemo.increase();
}
}).start();
}
//如果还有子线程在运行,主线程就让出CPU资源,
//直到所有的子线程都运行完了,主线程再继续往下执行
while(Thread.activeCount() > 1){
Thread.yield();
}
System.out.println("number : " + volDemo.getNumber());
}
}
AtomicInteger是一个提供原子操作的Integer类,通过线程安全的方式操作加减,因此十分适合高并发情况下的使用。
java.util.concurrent中实现的原子操作类包括:AtomicBoolean、AtomicInteger、AtomicIntegerArray、AtomicLong、AtomicReference、 AtomicReferenceArray。
在多线程中安全的使用volatile变量必须同时满足两个条件:
①对变量的写入操作不依赖其当前值,如number++不可以,boolean变量可以
②该变量没有包含在具有其他变量的不变式中,如果有多个volatile变量,则每个volatile变量必须独立于其他的volatile变量
四、synchronize 与 volatile 比较
1、volatile不需要枷锁,比synchronize更轻量级,不会堵塞程序;
2、从内存可见角度讲,volatile读相当于枷锁,volatile写相当于解锁。
3、synchronize既能保证可见性,又能保障原子性,而volatile只能保障可见性,不能保证原子性。
4、synchronize 使用更加广泛。
来自慕课网课程:细说Java多线程之内存可见性
以上是关于多线程—— 内存可见性的主要内容,如果未能解决你的问题,请参考以下文章
java多线程 -- volatile 关键字 内存 可见性