Java 中 final 内存语义

        对于final域,编译器和理器要遵守两个重排序规则
                1)在构造函数内对一个final域的写入,与随后把个被构造象的引用赋值给一个引用量,两个操作之不能重排序。
                2)初次读一个包含final域的象的引用,与随后初次读这final域,两个操作之不能重排序。
        下面通过一些示例性的代来分别说两个规则 

Java 中 final 内存语义

        写final域的重排序规则禁止把final域的写重排序到构造函数之外。规则实现包含下面2个方面。
                1)JMM禁止编译器把final域的写重排序到构造函数之外。
                2)编译器会在final域的写之后,构造函数return之前,插入一个StoreStore屏障。个屏障禁止理器把final域的写重排序到构造函数之外。
        现在让分析writer()方法。writer()方法只包含一行代finalExample=new FinalExample()行代包含两个步,如下。
                1)构造一个FinalExample类型的象。
                2)把这个对象的引用赋值给引用obj
        假设线程B读对象引用与读对象的成域之没有重排序(上会什么需要个假),3-29是一种可能的序。在下中,写普通域的操作被编译器重排序到了构造函数之外,读线B错误取了普通i初始化之前的。而写final域的操作,被写final域的重排序规则限定在了构造函数之内,读线B正确地取了final量初始化之后的
        写final域的重排序规则可以确保:在象引用任意线程可之前,象的final域已被正确初始化了,而普通域不具有个保障。以上图为例,在读线B“看到象引用obj,很可能obj没有构造完成(普通域i的写操作被重排序到构造函数外,此初始1没有写入普通域i)。 

Java 中 final 内存语义

        读final域的重排序规则是,在一个线程中,初次读对象引用与初次读该对象包含的final域,JMM禁止理器重排序两个操作(注意,规则仅仅针对处理器)。编译器会在final域操作的前面插入一个LoadLoad屏障。
        初次读对象引用与初次读该对象包含的final域,两个操作之存在接依关系。由于编译器遵守接依关系,因此编译器不会重排序两个操作。大多数理器也会遵守接依,也不会重排序两个操作。但有少数理器允许对存在接依关系的操作做重排序(比如alpha理器),规则就是专门用来针对这理器的。
        reader()方法包含3个操作。
            ·初次读引用obj
            ·初次读引用obj指向象的普通域j
            ·初次读引用obj指向象的finali
        现在假设线A没有生任何重排序,同程序在不遵守接依理器上行,下所示是一种可能的序。 

Java 中 final 内存语义

        在上图中,读对象的普通域的操作被理器重排序到读对象引用之前。普通域没有被写线A写入,是一个错误取操作。而final域的重排序规则会把读对final域的操作限定读对象引用之后,此时该final域已A线程初始化了,是一个正确的取操作。
        读final域的重排序规则可以确保:在一个象的final域之前,一定会先包含final域的象的引用。在个示例程序中,如果引用不null,那么引用象的final域一定已A线程初始化了。 

-------------------------------------------------------------------------------

        上面我们看到的final域是基数据型,如果final域是引用型,将会有什么效果?看下列示例代 

Java 中 final 内存语义

        本例final域一个引用型,它引用一个int型的数组对象。于引用型,写final域的重排序规则对编译器和理器增加了如下束:在构造函数内一个final引用的象的成域的写入,与随后在构造函数外把个被构造象的引用赋值给一个引用量,两个操作之不能重排序。
        对上面的示例程序,假设首先线AwriterOne()方法,行完后线BwriterTwo()方法,行完后线Creader()方法。下图中是一种可能的线序。
        在图中,1final域的写入,2对这final域引用的象的成域的写入,3是把被构造的象的引用赋值给某个引用量。里除了前面提到的1不能和3重排序外,23也不能重排序。
        JMM可以确保读线C至少能看到写线A在构造函数中final引用象的成域的写入。即C至少能看到数0值为1。而写线B元素的写入,读线C可能看得到,也可能看不到。JMM不保证线B的写入对读线C,因线B读线C存在数据争,此果不可知。

        如果想要确保读线程C看到写线B元素的写入,写线B读线C需要使用同步原lockvolatile)来确保内存可性。 

Java 中 final 内存语义

        前面我们提到,写final域的重排序规则可以确保:在引用任意线程可之前,引用量指向的象的final域已在构造函数中被正确初始化了。其,要得到个效果,需要一个保:在构造函数内部,不能让这个被构造象的引用其他线程所,也就是象引用不能在构造函数中逸出问题来看下面的示例代

Java 中 final 内存语义

        假设一个线Awriter()方法,另一个线Breader()方法。里的操作2使得未完成构造前就为线B。即使里的操作2是构造函数的最后一步,且在程序中操作2排在操作1后面,read()方法的线程仍然可能无法看到final域被初始化后的,因为这里的操作1和操作2可能被重排序。实际序可能如所示。

Java 中 final 内存语义

        从图中可以看出:在构造函数返回前,被构造象的引用不能其他线程所,因final域可能没有被初始化。在构造函数返回后,任意线程都将保能看到final域正确初始化之后的

JSR-133什么要增final语义

        在旧的Java内存模型中,一个最严重的缺陷就是线程可能看到final域的会改。比如,一个线程当前看到一个整型final域的值为0未初始化之前的默认值),一段时间之后线程再去读这final域的值时,却发现值变为1(被某个线程初始化之后的)。最常的例子就是在旧的Java内存模型中,String可能会改了修补这个漏洞,JSR-133final语义。通过为final域增加写和重排序规则,可以Java程序提供初始化安全保:只要象是正确构造的(被构造象的引用在构造函数中没有逸出),那么不需要使用同步(指lockvolatile的使用)就可以保任意线程都能看到final域在构造函数中被初始化之后的 

参考:

《Java 并发编程的艺术》

转载于:https://my.oschina.net/u/3687664/blog/2054893