因此,我将尝试定义不变性及其与线程安全性的关系。
定义 不变性
我的定义是“不可变的对象是在构造之后状态不会改变的对象”。 我故意含糊其词,因为没有人真正同意确切的定义。
线程安全
您可以在Internet上找到许多不同的“线程安全”定义。 定义它实际上非常棘手。 我会说线程安全代码是在多线程环境中具有预期行为的代码。 我让您定义“预期行为”…
字符串示例
让我们看一下String
的代码(实际上只是一部分代码……):
public class String {private final char value[];/** Cache the hash code for the string */private int hash; // Default to 0public String(char[] value) {this.value = Arrays.copyOf(value, value.length);}public int hashCode() {int h = hash;if (h == 0 && value.length > 0) {char val[] = value;for (int i = 0; i < value.length; i++) {h = 31 * h + val[i];}hash = h;}return h;}
}
String
被认为是不可变的。 看一下它的实现,我们可以推断出一件事:不可变的对象可以更改其内部状态(在这种情况下,是延迟加载的哈希码),只要它在外部不可见即可。
现在,我将以一种非线程安全的方式重写hashcode方法:
public int hashCode() {if (hash == 0 && value.length > 0) {char val[] = value;for (int i = 0; i < value.length; i++) {hash = 31 * hash + val[i];}}return hash;}
如您所见,我删除了局部变量h
并直接影响了变量hash
。 此实现不是线程安全的! 如果多个线程同时调用hashcode
,则每个线程的返回值可能不同。 问题是,这堂课是一成不变的吗? 由于两个不同的线程可以看到不同的哈希码,因此从外部角度来看,我们具有状态更改,因此它不是不可变的。
我们可以得出这样的结论: String
是不可变的, 因为它是线程安全的,而不是相反的。 所以……说“做一些不可变的对象,它是线程安全的!”有什么意义? 但是请注意,您必须使不可变对象具有线程安全性!” ?
ImmutableSimpleDateFormat示例
在下面,我写了一个类似于SimpleDateFormat的类。
public class VerySimpleDateFormat {private final DateFormat formatter = SimpleDateFormat.getDateInstance(SimpleDateFormat.SHORT);public String format(Date d){return formatter.format(d);}
}
该代码不是线程安全的,因为SimpleDateFormat.format
不是。
这个对象是不变的吗? 好问题! 我们已尽力使所有字段均不可修改,我们不使用任何设置方法或任何建议对象状态将改变的方法。 实际上,内部SimpleDateFormat
更改其状态,这就是它不安全线程的原因。 由于对象图中的某些内容发生了变化,因此即使它看起来像它也不是不变的。问题甚至不是SimpleDateFormat
更改其内部状态,而是它以一种非线程安全的方式进行操作。
总结这个例子,创建一个不可变的类并不容易。 最后一个关键字还不够,您必须确保对象的对象字段不会更改其状态,这有时是不可能的。
不可变的对象可以具有非线程安全的方法(没有魔术!)
让我们看一下下面的代码。
public class HelloAppender {private final String greeting;public HelloAppender(String name) {this.greeting = 'hello ' + name + '!\n';}public void appendTo(Appendable app) throws IOException {app.append(greeting);}
}
HelloAppender
类绝对是不可变的。 方法appendTo接受Appendable
。 由于Appendable
不能保证是线程安全的(例如StringBuilder
),因此追加到此Appendable
会在多线程环境中引起问题。
结论
在某些情况下,创建不可变对象绝对是一个好习惯,并且对创建线程安全代码有很大帮助。 但是,当我到处阅读时,这使我感到困扰。 不可变对象是线程安全的 ,显示为公理。 我明白了这一点,但是我认为对此进行一点思考总是很有益的,以便理解导致非线程安全代码的原因。
感谢Jose的评论,在本文的结尾我得出了不同的结论。 这都是关于不可变的定义。 需要澄清!
如果满足以下条件,则对象是不可变的:
- 它的所有字段在使用之前都已初始化(这意味着您可以进行延迟初始化)
- 字段的状态在初始化后不会更改(不更改表示对象图不会更改,即使子级的内部状态也是如此)
除非对象必须处理非线程安全的对象,否则不可变对象将始终是线程安全的。
参考: 不变性真的意味着线程安全吗? 从我们的JCG合作伙伴 Tibo Delor在InvalidCodeException博客中获得。
翻译自: https://www.javacodegeeks.com/2012/09/does-immutability-really-means-thread.html