为不安全发布的java.lang.String提供恢复时间

为不安全发布的java.lang.String提供恢复时间,java,multithreading,concurrency,thread-safety,safe-publication,Java,Multithreading,Concurrency,Thread Safety,Safe Publication,String实际上是不可变的。“实践中的Java并发”的Brian Goetz说,像有效不可变对象这样的东西只有在安全发布的情况下才是线程安全的。现在,假设我不安全地发布如下字符串: public class MultiThreadingClass { private String myPath ="c:\\somepath"; //beginmt runs simultaneously on a single instance of MultiThreading class

String实际上是不可变的。“实践中的Java并发”的Brian Goetz说,像有效不可变对象这样的东西只有在安全发布的情况下才是线程安全的。现在,假设我不安全地发布如下字符串:

public class MultiThreadingClass {
    private String myPath ="c:\\somepath"; 
    //beginmt runs simultaneously on a single instance of MultiThreading class
    public void beginmt(){
        Holder h = new Holder();
        h.setPath(new File(myPath)); //line 6
        h.begin();
    }
}

public class Holder {
    private File path;
    public void setPath(File path){
        this.path = path;
    }
    public void begin(){
        System.out.println(path.getCanonicalPath()+"some string");
    }
}
在多线程类使用其构造函数初始化时,第6行的文件构造函数可能看不到myPath的值


然后,在构造非安全发布的String对象大约三秒钟后,MultiThreadingClass上的线程仍在运行。文件构造函数仍然有可能看不到myPath的值吗?

文件构造函数仍然有可能看不到myPath的值吗

答案是肯定的,这是可能的,因为Java内存模型只保证最终字段的可见性 :-

“应该提供初始化安全的新保证。如果对象构造正确(这意味着在构造过程中不会漏掉对它的引用),则所有看到该对象引用的线程也将看到构造函数中设置的最终字段的值,而无需同步。“

然而,我觉得这种情况是不可能重现的(我之前也尝试过类似的理论,但结果是徒劳的)

构造函数中存在不安全发布/转义此引用的情况,这可能导致myPath未正确初始化的情况。您提到的书的清单3.7给出了一个例子。下面是一个让类在构造函数中转义此引用的示例

public class MultiThreadingClass implements Runnable{
    public static volatile MultiThreadingClass unsafeObject;
    private String myPath ="c:\\somepath"; 

    public MultiThreadingClass() {
       unsafeObject = this;
       .....
    }
    public void beginmt(){
        Holder h = new Holder();
        h.setPath(new File(myPath)); //line 6
        h.begin();
    }
}

即使在正确设置myPath之前,上述类也会导致其他线程访问unsafeObject引用,但重新创建此场景可能会很困难。

您要询问的语句:

在多线程类使用其 构造函数,第6行的文件构造函数可能会 看不到myPath的值

答案很复杂。 您不必担心
字符串
对象中的char数组
。正如我在评论中提到的,因为它是构造函数中分配的
final
字段,而且
String
在分配
final
字段之前不会将引用传递给自身,所以它总是安全发布的。您也不需要担心
hash
hash32
字段。它们不能安全发布,但只能具有值0或有效的哈希代码。如果它们仍然为0,则方法
String.hashCode
将重新计算该值-它只会导致其他线程重新计算hashCode,而之前在其他线程中已经这样做了

multi-threadingclass
中的参考
myPath
未安全发布,因为它不是
final
。“在MultiThreadingClass使用其构造函数初始化时”,但在构造函数完成后,除运行构造函数的线程之外的其他线程可能会在
myPath
中看到值
null
,而不是对字符串的引用

在[version 8 linked]的Java内存模型部分有一个例子,但自从JMM在JSR-133中发布以来,这个例子没有改变:

示例17.5-1。Java内存模型中的最终字段

下面的程序说明了最终字段与普通字段的比较

class FinalFieldExample { 
    final int x; 
    int y; 

    static FinalFieldExample f; 

    public FinalFieldExample() { 
        x = 3; 
        y = 4; 
    } 

    static void writer() { 
        f = new FinalFieldExample(); 
    } 

    static void reader() { 
        if (f != null) { 
            int i = f.x; // guaranteed to see 3 
            int j = f.y; // could see 0 
        } 
    } 
}
FinalFieldExample类有一个final int字段x和一个非final字段 整型字段y。一个线程可能执行方法编写器,另一个线程可能执行方法编写器 可能会执行方法读取器

因为writer方法在对象的构造函数之后写入f 完成后,将保证读卡器方法能够正确地看到 f.x的初始化值:它将读取值3。然而,f.y是 不是最终的;因此,不能保证读取器方法能够看到 它的值为4

这甚至可能发生在一台有许多线程的重载机器上

解决办法/解决方案:

  • 使
    myPath
    a
    final
    字段(并且在分配字段之前不要添加传递
    this
    引用的构造函数)
  • 使
    myPath
    a
    volatile
    字段
  • 在访问
    myPath
    之前,请确保访问
    myPath
    的所有线程在同一监视器对象上同步。例如,通过使
    beginmt
    a
    synchronized
    方法,或通过任何其他方式

您能否更清楚地跟踪线程的执行情况?我不明白你想显示什么。@vandale这只适用于
final
字段,不适用于像
myPath
这样的非final字段。根据Java内存模型:“当一个对象的构造函数完成时,它被认为是完全初始化的。一个线程只有在该对象完全初始化后才能看到对该对象的引用,它才能保证看到该对象最终字段的正确初始化值。”@b16db0我认为你混淆了引用和对象。如果执行第6行时引用
myPath
,尚未指向
“c:\\somepath”
,则其值必须为
null
(除非您未显示其他代码)。这个值以后不会追溯更改。@b16db0啊,好的,我明白了,你不必担心这个。
java.lang.String中的char数组
value
是构造函数中分配的
final
字段,构造函数不允许引用
转义,因此根据java内存模型,可以保证任何线程只能看到此
final
f的正确初始化值