Java基础-关键字:volatile

基础性问题

1、缓存一致性问题
程序运行,临时数据存放主存(物理内存)当中
CPU执行速率 远大于 数据在内存中读写速度
因此CPU里面就有了高速缓存
多个线程访问的变量为共享变量,存在缓存一致性问题

2、硬件层面解决方法:总线加LOCK#锁的方式、缓存一致性协议
总线加LOCK#锁的方式
对总线加锁,阻塞其他CPU对内存的访问,只能又一个CPU访问这个内存变量。
缺点:锁总线期间,其他CPU无法访问内存,导致效率地下。

缓存一致性协议
Intel的MESI协议:保证每个缓存中使用的共享变量的副本是一致的。

3、并发编程的三个基本概念:原子性,可见性,有序性
原子性:一个操作或者多个操作 要么全部执行,要么就都不执行。
可见性:多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
有序性:程序执行的顺序按照代码的先后顺序执行。

指令重排序:不会影响单个线程的执行,但是会影响到线程并发执行的正确性。
并发程序要正确地执行,必须要保证原子性、可见性以及有序性。任一条件没有被保证,就有可能会导致程序运行不正确。

Java内存模型

Java内存模型存在:缓存一致性问题 和 指令重排序问题。

所有的变量都是存在主存当中(类似于物理内存)
每个线程都有自己的工作内存(类似于高速缓存)
线程对变量的所有操作都必须在工作内存中进行,而不能直接对主存进行操作。
并且每个线程不能访问其他线程的工作内存。

原子性:
对基本数据类型的变量的读取和赋值操作是原子性操作
要实现更大范围操作的原子性,可以通过synchronized和Lock来实现

可见性:
Java提供了volatile关键字来保证可见性。
当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存;
当有其他线程需要读取时,它会去内存中读取新值。
普通的共享变量不能保证可见性。通过synchronized和Lock也能够保证可见性。

有序性:
在Java内存模型中,允许编译器和处理器对指令进行重排序;
重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
在Java里面,可以通过volatile关键字来保证一定的“有序性”。
可以通过synchronized和Lock来保证有序性,他保证每个时刻是有一个线程执行同步代码。

关键字:volatile : 保证可见性,不保证原子性,一定程度上保证有序性

1、保证可见性
示例代码如下:

public volatile static int inc = 1;
public static void main(String[] args) {
    new Thread() {
        public void run() {
            while(inc == 1) { }
        }
    }.start();
    
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) { }
    inc = 0;
    System.err.println("main is Over ... " + inc);
}

不加关键字volatile的时候:主线程修改变量,不会更新到子线程;子线程不会退出。
加入关键字,主线程更新到主存,子线程也去主存读取;子线程会退出。

2、不保证原子性
示例代码如下:

public static volatile int inc = 0;
public static void increase() { inc++; }
public static void main(String[] args) {
    for(int i = 0; i < 10; i++) {
        new Thread() {
            public void run() {
                for(int j = 0; j < 1000; j++) {
                    increase();
                }
            }
        }.start();
    }
    while(Thread.activeCount() > 1) Thread.yield();
    System.err.println(inc);
}

10个线程,每个线程对变量自加1,自加1000次;最终结果应该是:10000。
但是最终结果总是小于10000 。

如何保证原子性 ?
答:
(1)对方法 increase 同步锁:synchronized , 输出结果为: 10000 。
(2)定义锁 Lock ,更新方法 increase,输出结果为: 10000 。
如下代码:

public static Lock lock = new ReentrantLock();
public static void increase() { 
    lock.lock();
    try {
        inc++; 
    } finally {
        lock.unlock();
    }
}

(3)采用原子性的包装对象: AtomicInteger。 如下代码:

public static volatile AtomicInteger inc = new AtomicInteger();
public static void increase() {
    inc.getAndIncrement();
}

在java 1.5的java.util.concurrent.atomic包下提供了一些原子操作类
对基本数据类型的 自增,自减、以及加法操作,减法操作 进行了封装
atomic是利用CAS来实现原子性操作的(Compare And Swap)
CAS实际上是利用处理器提供的CMPXCHG指令实现的

3、一定程度上保证有序性
volatile关键字能禁止指令重排序,所以volatile能在一定程度上保证有序性

观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现;
加入volatile关键字时,会多出一个lock前缀指令
lock前缀指令实际上相当于一个内存屏障
内存屏障会提供3个功能:
确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面。
强制将对缓存的修改操作立即写入主存。
如果是写操作,它会导致其他CPU中对应的缓存行无效。

上一篇:redha7.2配置静态网卡(三步搞定)


下一篇:Java陷阱之assert关键字