当前位置: 首页 > 后端技术 > Java

Java并发——内存模型的几个明显例子

时间:2023-04-01 18:11:47 Java

并发三问:重排序、内存可见性、原子性一、重排序代码示例importjava.util.concurrent.CountDownLatch;publicclassRearrangeTest{privatestaticintx=0,y=0;私有静态整数a=0,b=0;publicstaticvoidmain(String[]args)throwsInterruptedException{inti=0;对于(;;){我++;x=0;y=0;一=0;b=0;CountDownLatchlatch=newCountDownLatch(1);线程一=newThread(()->{try{latch.await();}catch(InterruptedExceptione){}a=1;x=b;});Threadother=newThread(()->{try{latch.await();}catch(InterruptedExceptione){}b=1;y=a;});一个开始();其他.开始();latch.countDown();一个。加入();其他.join();Stringresult="th"+i+"time("+x+","+y+")";如果(x==0&&y==0){System.err.println(结果);休息;}else{System.out.println(result);}}}}运行结果:出现x=0和y=0的反常识结果二、重排序机制1.编译器优化:编译器编译时代码顺序重排。例如,在此示例中,编译器可能会更改a=1和x=b,以及b=1和y=b的代码顺序。2、指令重排序:CPU执行时改变代码的顺序。3.内存系统重排序:内存系统没有重排序,但是由于缓存的存在,程序整体会出现乱序行为。线程1修改了a的值,但是修改后,a的值可能不会写回主存,那么线程2可能会得到a=0。同理,线程2对b的赋值操作也可能不会刷新到主内存及时。三、内存可见性所有共享变量都存在于主内存中,每个线程都有自己的本地内存。线程读写共享数据也是通过本地内存进行交换,所以可见性问题依然存在。四、原子性。long和double类型的值需要占用64位内存空间。Java中64位值的写入可以拆分成两个32位的操作进行写入。整个赋值操作分为两个操作,低32位赋值和高32位赋值。如果其他线程读取中间的值,可能会导致原子性问题。五、Java并发约束规范SynchronizationOrderHappens-beforeOrderVI,synchronized线程a在进入synchronized块之前或在synchronized中对后续线程b可见,持有相同的监视器锁(monitor)用于对共享变量的操作。进入synchronized时,不保证前面的写操作一定会被flush到主存中。synchronized主要保证在退出的时候本地内存中的数据能够刷入到主存中。一个不正确的单例模式仔细检查,代码并没有重现多线程的问题,你要重新思考:publicclassSingleton{privatestaticSingletoninstance=null;私人诠释v;publicintgetV(){返回v;}publicSingleton(){this.v=1;}publicstaticSingletongetInstance(){//单例模式双重检查if(instance==null){//操作1:首先检查synchronized(Singleton.class){//操作2if(instance==null){//操作3:第二次检查instance=newSingleton();//操作4}}}返回实例;例如,有两个线程a和b调用getInstance()方法。假设a在先,一直走到操作4,即instance=newSingleton()这行代码。这行代码会先申请一个空格,然后将各个属性初始化为零(0/null),在构造方法中执行属性赋值[1],将这个对象的引用赋值给instance[2]。在此过程中,[1]和[2]可能会重新排序。这时候b线程刚执行操作1时,有可能获取到instanceisnotnull,此时b线程不会等待monitor锁,直接返回instance。问题是这个实例可能还没有执行完构造方法(此时线程a还在第4步),所以线程b得到的实例是不完整的,里面的属性值可能是初始化的零值(0/false/null),而不是线程a在构造函数中指定的值。如果所有的属性都用final修饰,之前介绍的doublecheck其实是可行的,不需要加volatile。七、volatile:内存可见性和禁止指令重排序volatile修饰符适用于以下场景:一个属性被多个线程共享,一个线程修改了这个属性,其他线程可以立即得到修改后的值。在concurrent包的源码中,用的很多。volatile属性的读写操作是无锁的,不能替代synchronized,因为它不提供原子性和互斥性。因为没有锁,不需要花时间去获取和释放锁,所以成本低。Volatile只能应用于属性。我们使用volatile来修改属性,这样编译器就不会对该属性的指令重新排序。Volatile提供可见性,一个线程对它的任何修改将立即对其他线程可见。易失性属性不会被线程缓存,并且总是从主内存中读取。Volatile提供了happens-before保证,写入volatile变量vhappens-before其他线程对v的所有后续读取。volatile使长赋值和双重赋值成为原子的。八、final1、用final修饰的类不能被继承2、用final修饰的方法不能被重写3、用final修饰的属性一旦初始化就不能修改。在对象的构造方法中设置final属性,在对象初始化完成之前,不要把这个对象的引用写到其他线程可以访问的地方(不要让引用在构造函数中逃逸)。