并发编程笔记二:java的内存模型

 2019-12-10 16:14  阅读(931)
文章分类:Java Core

内容多有疏漏,有问题欢迎提出

目录

  1. java内存模型的概念
  2. 原子性(Atomicity)
  3. 可见性(Visibility)
  4. 有序性(Ordering)
  5. 总结

java内存模型的概念

java并发程序开发比串行程序开发复杂的多,其中重要的一点就是要保证数据的正确性。对于串行来讲,输入是1,输出一定是1,这个很好保证,但是对于并行开发来讲,中间如果没有做好数据在多线程中的控制,很有可能导致输入是,输出可能是1,也可能是111。java内存模型(JMM)就是为了保证数据在多线程中正确性的一种协议。

其中,JMM最关键的技术点都是围绕着多线程的原则性、可见性和有序性来建立。下面,我们重点来讨论多线程中的这三大特性。

原子性(Atomicity)

原子性指一个操作是不可中断的,即要么全部执行成功要么全部执行失败,不接受一半成功一半失败的情况。

int a = 10; //1

a++; //2

以上两种场景,只有第一种是原则性操作,第二种虽然看起来是一步执行完成,但是实际上他执行了

  1. 读取a的值
  2. 对a进行加1的操作
  3. 把加1后的值赋给变量

在开发中,常用的保证原子性的操作即是加上sychronized的锁,加锁之后的效果就是把锁住的类、方法、对象作为一个整体,在该模块处理完之前,不允许其他操作调用该模块。

另外一种锁–volatile只能可见性和有序性,不能保证原子性,所以在下面例子中,使用volatile进行数据一致性的操作是有问题的:

​
    public class VolatileExample {
        private static volatile int counter = 0;

        public static void main(String[] args) {
            for (int i = 0; i < 10; i++) {
                Thread thread = new Thread(new Runnable() {
                    @Override
                    public void run() {
                        for (int i = 0; i < 10000; i++)
                            counter++;
                    }
                });
                thread.start();
            }
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(counter);
        }
    }

    ​

如果volatile可以保证原子性的话,实际输出结果应该是100000,但是真实的结果远小于100000。

然而想要保证上面例子的结果输出正确的话,我们可以做这样的修改:

public class VolatileExample {
        private static volatile int counter = 0;

        public static void main(String[] args) {
            for (int i = 0; i < 10; i++) {
                Thread thread = new Thread(new Runnable() {
                    @Override
                    public void run() {
                        for (int i = 0; i < 10000; i++)
                            //在counter++操作时加上synchronized锁
                            synchronized (VolatileExample.class){
                                counter++;
                            }
                    }
                });
                thread.start();
            }
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(counter);
        }
    }

或者使用AtomicInteger这样线程安全的类进行变量赋值的操作。

import java.util.concurrent.atomic.AtomicInteger;

    public class Atomicity {
        //使用保证的原子性的AtomicInteger类进行counter变量的修改操作
        static AtomicInteger counter = new AtomicInteger();

        private static void count() {
            Atomicity atomicity = new Atomicity();
            for (int i = 0; i < 10; i++) {
                Thread thread = new Thread(new Runnable() {
                    @Override
                    public void run() {
                        for (int i = 0; i < 10000; i++)
                            //addAndGet是线程安全的修改方法
                            counter.addAndGet(1);
                    }
                });
                thread.start();
            }
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(atomicity.counter);
        }

        public static void main(String[] args) {
            count();
        }
    }

这样写的话,实际的输出结果就是100000。

可见性(Visibility)

可见性是指当一个线程修改了某一个共享变量的值时,其他的线程是否能够立即知道这个修改。

即当CPU1一个线程修改了一个全局变量a,并将其缓存在cache中或者寄存器里,在此同时CPU2上的某一线程也对全局变量a进行修改,也将其及存到cache或寄存器里,这样,两个线程对于全局变量a的值可能存在不一样的情况,从而导致接下来的数据处理可能会异常。

对此,sychronized和volatile关键字都可以保证操作的可见性,即当某一全局变量一旦被修改,就会立刻被刷新到主内存中去,保证多线程中数据的一致性。

有序性(Ordering)

在多线程执行过程中,一般理解代码会按顺序从前到后依次执行,但是出于某种原因,在某些情况下,程序会对指令重新排序,导致程序操作结果异常。在单一线程中,指令执行的顺序一定是一致的(否则程序无法正常工作),只有在并行的程序中,才会发生指令重排序的情况。但是总结性的来讲,指令的重排序不是无意义的,他的目的是为了尽量少的中断流水线,提高程序的性能。

对于哪些指令不能重拍,也有既定的规则,即Happen-Before规则:

  • 程序顺序原则:一个县城内保证语义的串行性;
  • volatile规则:volatile变量的写咸鱼度发生,这保证了volatile变量的可见性;
  • 锁规则:解锁(unlock)必然发生在随后加锁(lock)之前;
  • 传递性:A先于B,B先于C,那么A必然先于C;
  • 线程的start()方法先与他的每一个动作;
  • 线程的所有操作先于线程的终结(Thread.join());
  • 线程的中断(interrupt())先于被中断线程的代码;
  • 对象的构造函数的执行,结束先于finalize()方法;

总结

本章说明了了java内存模型(JMM)是保证多线程中数据正确性的基础协议,并对多线程中原子性、可见性、有序性这三大特性进行了分析。下一章的内容,我们会从java线程讲起,阐述线程从创建到终止的一些列操作。

点赞(0)
版权归原创作者所有,任何形式转载请联系作者; Java 技术驿站 >> 并发编程笔记二:java的内存模型

相关推荐