Java之内存模型

参考博客:

https://www.cnblogs.com/nexiyi/p/java_memory_model_and_thread.html

https://www.cnblogs.com/dolphin0520/p/3920373.html

https://www.cnblogs.com/chengxiao/p/6528109.html

https://cyc2018.github.io/CS-Notes

《深入理解Java虚拟机》

Java内存模型:实现让 Java程序在各种平台下都能达到一致的内存访问效果。

主内存和工作内存

处理器上的寄存器的读写速度比内存快几个数量级,为了解决这种矛盾,在它们之间加入了高速缓存

加入高速缓存就带来了一些问题,如缓存一致性。多个缓存共享同一块主内存区域,那么多个缓存的数据可能会不一致。

           

所有变量都存储在主内存中,每个线程还有自己的工作内存,工作内存存储在高速缓存或者寄存器中,保存了该线程使用的变量的主内存副本拷贝(所有的变量都存储在主内存中)。线程对变量的所有操作(读取、赋值)都必须在工作内存中进行,而不能直接读写主内存中的变量。

线程只能直接操作工作内存中的变量,不同线程之间的变量值的传递需要通过主内存来完成。

      

 

内存间的交互操作

关于主内存与工作内存之间的具体交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步到主内存之间的实现细节。 Java内存模型中定义了8个操作来完成主内存和工作内存之间的交互操作。

                             

  • read:把一个变量从主内存传输到工作内存中。
  • load:在read之后执行,把read得到的值放入工作内存的变量副本中。
  • use:把工作内存的一个变量传递给执行引擎。
  • assign:把一个从执行引擎接收到的值赋给工作内存中的变量。
  • store:把工作内存的变量的值传送到主内存中。
  • write:在store之后执行,把store得到的值放入主内存的变量中。
  • lock:作用于主内存的变量。
  • unlock:作用于主内存的变量。

 

内存模型的三大特性

1. 原子性

Java内存模型保证了read、load、use、assign、store、write、lock和unlock操作具有原子性。但是Java内存模型允许将没有被volatile修饰的64位数据(long, double)的读写操作分为两步32位的操作来执行,即load、store、read和write操作可以不具备原子性。

关于volatile,我们来看一个问题。

有如下代码:

public class inc_exam {
    private volatile int count = 0;

    public void inc() {
        count ++;
    }

    public int getInc() {
        return count;
    }

    public static void main(String[] args) throws InterruptedException{
        inc_exam exam = new inc_exam();
        // 使用CountDownLatch来等待子线程执行完毕
        CountDownLatch countDownLatch = new CountDownLatch(1000);
        ExecutorService executorService = Executors.newCachedThreadPool();

        for(int i = 0; i < 1000; i++) {
            executorService.execute(()->{
                exam.inc();
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        executorService.shutdown();
        System.out.println(exam.getInc());
    }
}

关于countDownLatch可以看我的博客:  Java并发之CountDownLatch、CyclicBarrier、Semaphore (AQS)

上列代码的作用是,使用1000个线程来执行exam对象的inc()操作,也就是使得volatile修饰的变量count的值增加。最后执行完毕之后,输出exam对象中的int变量count的值,我们发现,每次输出的值都不一样,而且都少于1000

并且,当我们去除volatile关键字之后,发现,输出的结果也不是1000(肯定不是1000).

由此,我们有如下结论,volatile关键字只能保证变量的可见性(见下文), 但是并不能保证变量的原子性

分析原因:我们将内存间的交互操作简化为3个,load、assign、store。所以,在上述代码中,对变量count执行自增操作,其实也是执行了多个操作。 

当两个线程(这里以两个为例,T1,T2)同时对count进行操作,load、assign、store这一系列操作整体上并不具备原子性。因为在T1修改count并且还没有将修改后的值写入主内存时,T2依然可以读入旧值。两个线程虽然执行了两次自增运算,但是主内存中count的值最后还是1而不是2 (图示中的情况). 因此,对int类型读写操作满足原子性,只是说明load、assign、store这些单个操作具备原子性。

解决方案:

   (1). 使用AtomicInteger,AtomicInteger能保证多个线程修改的原子性。

使用AtomicInteger实现上面的示例,只需修改如下部分:

    private AtomicInteger count = new AtomicInteger();

    public void inc() {
        count.incrementAndGet();
    }

    public int getInc() {
        return count.get();
    }

最后输出结果为1000.

    (2). 使用synchronized来保证操作的原子性。

示例代码修改部分为:

    private int count = 0;

    public void inc() {
        synchronized (this) {
            count ++;
        }
    }

    public int getInc() {
        return count;
    }

    (3). 使用重入锁ReentrantLock来保证操作的原子性。

示例代码修改部分为:

    Lock lock = new ReentrantLock();

    private int count = 0;

    public void inc() {
        lock.lock();
        try {
            count ++;
        } finally {
            lock.unlock();
        }
    }

    public int getInc() {
        return count;
    }

 

2. 可见性

可见性指的是当一个线程修改了某个共享变量的值,其他线程能够立即得知这个修改。Java内存模型是通过在变量修改后,将新值同步回主内存,在变量读取前从主内存刷新变量值来实现可见性。

实现可见性的三种主要方式:

  • volatile, 被volatile修改的变量,就保证了该共享变量对所有线程的可见性。(不保证原子性)
  • synchronized, 对一个变量执行unlock操作之前,必须把变量值同步回主内存。
  • final,被final关键字修饰的字段在构造器中一旦初始化完成,并且没有发生this逃逸,那么其他线程就能看到final字段的值。

 

3. 有序性

有序性是指:在本线程中观察,所有操作都是有序的。在一个线程观察另一个线程,所有操作都是无序的。

Java内存模型中,允许编译器和处理器对指令进行重排序,重排序不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。

Java中可以使用volatile和synchronized来保证有序性。其中volatile通过添加内存屏障的方式来禁止指令重排,即重排序时不能把后面的指令放到内存屏障之前。  而synchronized,通过保证每个时刻只有一个线程执行同步代码,相当于是让线程顺序执行。

 

JVM之先行发生原则

JVM规定了先行发生原则,让一个操作无需控制就能优先于另一个操作完成。

  1. 单一线程原则。 在一个线程内,在程序前面的操作先行发生于后面的操作。
  2. Volatile变量规则。 对一个 volatile 变量的写操作先行发生于后面对这个变量的读操作。
  3. 线程启动规则。 Thread 对象的 start() 方法的调用先行发生于此线程的每一个动作。
  4. 线程加入规则。 Thread 对象的结束先行发生于 join() 方法的返回。
  5. 线程中断规则。 对线程 interrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过 interrupted() 方法检测到是否中断发生。
  6. 对象终结规则。 一个对象的初始化完成 ( 析构函数执行结束 )  先行发生于它的 finalize() 方法的开始。
  7. 传递性。  如果操作 A 先行发生于操作 B, 操作 B 先行发生于操作 C, 那么操作 A 先行发生于操作 C。
  8. 管程锁定规则。 一个unlock操作先行发生于后面对同一个锁的lock操作。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值