第12章 Java内存模型与线程

Java 内存模型(Java Memory Model –> JMM)

用来实现屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的并发效果。屏蔽硬件差异、保证并发。而程序的功能就是数据流的交互,所以保证数据的快速、正确访问就是Java内存模型的核心

1.主内存与工作内存

  • Java内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量

  • Java内存模型规定所有的变量存储在JVM的主内存中。每条线程还有自己的工作内存(类比高速缓存),线程对变量的所有操作(读取、赋值等都必须在工作内存中进行,而不能直接读写主内存中的变量)

volatile型变量

关键字volatile 是Java虚拟机提供的最轻量级的同步机制,但是它并不容易被正确地、完整的理解,所以在遇到多线程数据竞争的问题时,一律使用synchronized来进行同步。

volatile是轻量级同步机制,它保证被修饰的变量在修改后立即列入主内存,使用变量前必须从主内存刷新到工作内存,这样就保证所有线程的可见性,不存在隔离性

对一个变量被定义为volatile之后,将具备两种特性

  • 保证此变量对所有线程的可见性

  • 禁止指令重排序优化

保证此变量对所有线程的可见性

这里的可见性指当一个线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的。而普通变量则需要通过主内存作为桥梁:线程A修改了一个普通变量的值,需要先向主内存进行回写,线程B在线程A完成回写后才能从主内存读取到新值,而之前还是线程A修改前的值。

但是关于volatile变量的可见性,如果没有深入了解,是会被误解的,常见的一种错误描述是“volatile变量对所有线程是立即可见的,对volatile变量的所有写操作都能立刻反应到其它线程中。换句话说,volatile变量在各个线程中是一致的,所有基于volatile变量的运算在并发下是安全的”。这句话其实是错误的,不能得出“基于volatile变量的运算在并发下是安全的”这个结论.

volatile变量在各个线程的工作内存中是不存在一致性问题(是因为执行引擎在使用这个变量时,都会显式刷新后使用),但是Java运算并非原子操作,如下例子:

public class VolatileTest {
    public static volatile int race = 0;

    public static void increase() {
        race++;
    }

    private static final int THREADS_COUNT = 20;

    public static void main(String[] args) {
        Thread[] threads = new Thread[THREADS_COUNT];
        for(int i = 0; i < THREADS_COUNT; i++) {
            threads[i] = new Thread(new Runnable() {

                @Override
                public void run() {
                    for(int i = 0; i < 10000; i++) {
                        increase();
                    }
                }
            });
            threads[i].start();
        }

        while(Thread.activeCount() > 1) {
            Thread.yield();
        }

        System.out.println(race);
    }
}

这段代码发起了20个线程,每个线程对race累加10000次+1操作,如果代码正确运行则结果为200000,但是最后结果却小于200000,而且每次运行结果还不同。问题就出在race++这一行代码上,用javap反编译后看到race++实际上是由4条语句构成的(不算return):

getstatic #13;
    iconst_1
    iadd
    putstatic   #13;
    return

失败原因这时候就明了了,当getstatic把race值取到栈顶时,volatile保证了race的值在这时是正确的,但是在执行iconst_1/iadd这些指令的时候,其他线程可能修改了race的值(增大了),那么操作数栈栈顶的值就成了过期数据,所以putstatic指令执行后就会把较小的race值同步回写到主内存中,最后造成结果小于200000.

结论

由于volatile变量只保证可见性,在不符合以下两条规则的运算场景中,我们仍然需要通过加锁(使用synchronized或者java.util.concurrent中的原子类)来保证原子性

  • 运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值

  • 变量不需要与其他的状态变量共同参与不变约束

  • 正确使用volatile的场景

volatile boolean shutdownFlag;

public void shutdown() {
    shutdownFlag = true;
}

public void threadsWork() {
    while(!shutdownFlag) {
        //do something
    }
}
禁止指令重排序优化
JMM模型的三大特性
  • 原子性:JMM会保证read/load/assign/use/store/write的原子性,如果需要更大范围的原子性,可以使用lock和unlock,这个从代码层面来看就是synchronized

  • 可见性:可见性就是当一个线程修改了共享变量的值,其他线程能立即得知这个修改。而volatile就是搞这个的。JMM通过变量修改后回写主内存,读取前从主内存刷新变量值这种依赖主内存作为中介的方法实现可见性,只不过volatile的特殊规则能使新值立即同步主内存,使用前立即从主内存刷新。所以可以说volatile保证了多线程操作时变量的可见性,而普通变量不能保证这一点。除了volatile之外,Java还有两个关键字能实现可见性:synchronized和final,synchronized是由“对一个变量执行unlock操作之前,必须把此变量的值回写到主内存”这条规则实现的,细想一下,就明白了。而final是由“被final修饰的字段在构造器中一旦被初始化完成,并且构造器没有把this引用传递出去,那么在其他线程中就能看见final字段的值”

  • 有序性:JMM的有序性可以总结为一句话:本线程内观察,所有的操作都是有序的;如果是旁观者线程,被观察线程的操作都是无序的。前半句是指”线程内表现为串行的语义“,后半句是指”指令重排序“和”工作内存和主内存存在同步延迟“的现象。Java语言提供了volatile和synchronized保证线程之间操作的有序性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值