字符串的plus运算符的线程安全性,包括优化

这篇文章说a += b相当于

 a = new StringBuilder() .append(a) .append(b) .toString(); 

假设我有这个代码:

 public class MultiThreadingClass extends SomeThirdPartyClassThatExtendsObject{ public void beginmt(String st) throws IOException { //st is a thread number st = new File("c:\\somepath").getCanonicalPath()+"\\"+st; System.out.println(st); } } 

假设beginmt在MultiThreading类的单个实例上同时运行多次(线程号为1到15500)。 可能有这样的情况,它可以打印以下,即一些线程数丢失,一些数字加倍?

 c:\somepath\2 c:\somepath\1 c:\somepath\1 c:\somepath\4 c:\somepath\5 c:\somepath\6 c:\somepath\7 c:\somepath\8 c:\somepath\8 c:\somepath\10 ... 

编辑:

是否安全地说+运算符不会进入某些不安全的发布问题? 我认为StringBuilder可以优化为类似于实例变量的东西,在这种情况下它可能会被不安全地发布。

编辑2:

就JLS,上述post以及上述代码的类似类文件进行检查,要使用的StringBuilders似乎必须包含在不同的堆栈帧中。 但是,我仍然想检查某种forms的激进优化是否会导致StringBuilders以某种方式被集中式StringBuilder替换。 这听起来是可能的,因为当优化器预测对象刚刚以非常数方式实现时实际上这样的对象可以是常量时,优化器优化是合乎逻辑的。

找到了stringopts.cpp,但还没有找到时间来完全检查它。 我希望找到涉及此源文件详细信息的答案。

编辑3:

我仍然在寻找包含有关可变对象的主动内联代码的答案。

每个线程将始终具有单独的StringBuilder实例。 线程不共享实例时,线程安全不成问题。

那么,以下简单方法……

 public class MyThreadSafeClass { public String myMethod(String field1, String field2, String field3) { return field1 + field2 + field3; } } 

…将被编译为使用本地StringBuilder。

 public class MyThreadSafeClass { public String myMethod(String field1, String field2, String field3) { return new StringBuilder(field1).append(field2).append(field3).toString(); } } 

每次输入方法时,都会创建一个新的StringBuilder实例。 此实例仅用于此线程的范围。

你是正确的,但StringBuilders并不总是线程安全的。 (见下文)如果多个线程开始调用saveEvent方法,则它们可能同时使用构建器。

 public class History { // thread-safety issues !!!! // In fact, here you should use a StringBuffer or some locking. private StringBuilder historyBuilder = new StringBuilder(); public void saveEvent(String event) { historyBuilder.append(event).append('\n'); } public String getHistoryString() { return historyBuilder.toString(); } } 

但编译器优化不会创建这种结构。 StringBuilder始终只在同一个线程中创建和使用。

我们可以尝试使事情变得更复杂(静态字段,多个类加载器……)但总是再次,每个StringBuilder实例仅由1个线程创建和使用。

编辑:

或许有用的知识:这种优化发生在字节码的生成过程中 。 稍后在JIT编译期间还有其他优化,但这种优化不是其中之一。 但是,JIT编译器确实对最终性能产生了重要影响 。

不,没有状态在不同线程之间共享,因此您描述的情况不会发生。

如果相反, st是该类的成员变量,而不是作为参数传递,并且递增 – 这是一个不同的故事。

它现在如何工作是st将被放在执行堆栈上,每个线程都有自己的执行堆栈,并且它们不会从那里共享内容。 因此每个线程都有自己的st值。 当它是类的成员变量时,它在内存中(单值),并且所有线程都会尝试使用它(相同的一个)。

@Edit:嗯,我想如果你用相同的值多次调用该方法可能的:-))

可能有这样的情况,它可以打印以下,即一些线程数丢失,一些数字加倍?

st方法局部变量 ,st也不会转义方法的范围,因此它是线程安全的。 因此,multithreading对st没有影响。 消息可以按顺序打印,具体取决于哪个线程在什么时间运行该方法。

由于您不在线程之间共享任何字段,因此打印顺序可能不同,但不应出现有关线程安全(竞争条件)的任何问题。

Java语言规范说明

字符串连接的结果是对String对象的引用,该对象是两个操作数字符串的串联。 左侧操作数的字符位于新创建的字符串中右侧操作数的字符之前。

因此,虽然编译器可以自由地优化串联的发生方式,但是必须遵循该规则, "a" + "b"变为"ab" 。 在一个非线程安全的共享StringBuilder ,实现可能不是这种情况。 因此,该实现不正确,不能被视为Java。

我不得不部分不同意。 这句话不完整/误导:

“如果相反,st是该类的成员变量,而不是作为参数传递,并且增加了 – 这是一个不同的故事。”

原始示例中最重要的是右侧的表达式是右值 如果不是,结果会有所不同。 我会解释一下。

所以是的,字符串是不可变的,并且beginmt()接收对String的最终引用,这意味着对不可变堆内存区域的最终引用。 JVM将复制这个最终引用,无论你在beginmt()中做什么,它都是在这个副本上完成的,这个副本在字符串被修改后立即(st = …),将指向另一个内存区域。 现在重点是这个最终的堆内存区域没有指向它的指针,因为它是在方法内部创建的,似乎没有任何指向它的东西。 好吧,差不多! JVM可以实习字符串,如果另一个线程指向与实习值相同的值,则它们实际上可能共享相同的堆地址。 现在完全具有竞争条件是非常难以检测的,所以我将做一个合成示例来说明如果右边的表达式是左值(由JVM的String intern-al引起)会发生什么:

public class AnExample {private static final int N = 20000;

 private static class Foo { static HashMap foos = new HashMap(N); static synchronized Foo createInstance(String i) { if (foos.containsKey(i)) return foos.get(i); foos.put(i, new Foo(i)); return foos.get(i); } String i; private Foo(String i) { this.i = i; } Foo inc() { synchronized(Foo.class){ i += "1"; return createInstance(i); } } @Override public String toString() { return i; } } private static class Bar { public void bar(Foo st) { st = st.inc(); System.out.println(st); } } public static void main(String... args) { final Bar cucu = new Bar(); for (int i = 0; i < N; i++) { final Foo st = Foo.createInstance(i + ""); new Thread(new Runnable(){ @Override public void run() { cucu.bar(st); } }).start(); } try { Thread.sleep(10000); } catch (InterruptedException e) { e.printStackTrace(); } } 

}

这将产生大约40%的重复(我有少于10100个唯一值)。 请注意,在bar()方法中st = some_expression(st)在我的示例中,我故意生成一个左值,以显示在JVM将实例化表达式的情况下可能发生的情况,并且它恰好有一个引用(它被发回在另一个线程中使用相同的方法)。

结论是你的代码不正确,因为“st不是该类的成员变量”,“st变成本地的,复制的引用”等等 - 但是因为右边的表达式是一个右值。