Java 如果写入的值总是相同的,那么延迟初始化引用是线程安全的吗?
在我的应用程序中,我需要惰性地设置一个变量,因为我在类初始化期间无法访问必要的方法,但我还需要跨多个线程访问该值。我知道我可以用它来解决这个问题,但这似乎太过分了我需要调用以获取值的方法是幂等的,并且返回值永远不会更改。我希望像在单线程环境中一样延迟初始化引用。这似乎应该是可行的,因为对引用的读写是原子的 下面是一些我正在做的示例代码Java 如果写入的值总是相同的,那么延迟初始化引用是线程安全的吗?,java,multithreading,concurrency,volatile,Java,Multithreading,Concurrency,Volatile,在我的应用程序中,我需要惰性地设置一个变量,因为我在类初始化期间无法访问必要的方法,但我还需要跨多个线程访问该值。我知道我可以用它来解决这个问题,但这似乎太过分了我需要调用以获取值的方法是幂等的,并且返回值永远不会更改。我希望像在单线程环境中一样延迟初始化引用。这似乎应该是可行的,因为对引用的读写是原子的 下面是一些我正在做的示例代码 // views should only be accessed in getViews() since it is // lazily initialized.
// views should only be accessed in getViews() since it is
// lazily initialized. Call getViews() to get the value of views.
private List<String> views;
/* ... */
private List<String> getViews(ServletContext servletContext) {
List<String> views = this.views;
if (views == null) {
// Servlet Context context and init parameters cannot change after
// ServletContext initialization:
// https://docs.oracle.com/javaee/6/api/javax/servlet/ServletContext.html#setInitParameter(java.lang.String,%20java.lang.String)
String viewsListString = servletContext.getInitParameter(
"my.views.list.VIEWS_LIST");
views = ListUtil.toUnmodifiableList(viewsListString);
this.views = views;
}
return views;
}
//视图只能在GetView()中访问,因为它是
//懒散地初始化。调用getViews()以获取视图的值。
私人列表视图;
/* ... */
私有列表GetView(ServletContext ServletContext){
列表视图=this.views;
如果(视图==null){
//Servlet上下文和init参数在
//ServletContext初始化:
// https://docs.oracle.com/javaee/6/api/javax/servlet/ServletContext.html#setInitParameter(java.lang.String,%20java.lang.String)
String viewsListString=servletContext.getInitParameter(
“my.views.list.views_list”);
views=ListUtil.toUnmodifiableList(viewsListString);
this.views=视图;
}
回访意见;
}
,但我想确认对String
s和List
s等对象的引用的行为是相同的
这似乎可以正常工作,因为每个线程要么看到null
并重新计算值(这不是问题,因为值从未更改),要么看到已经计算的值。我是不是漏掉了什么陷阱?这段代码是线程安全的吗
关于32位原语的问题与此类似,但我想确认对字符串和列表等对象的引用的行为是相同的
是的,因为写入引用始终是原子的:
对引用的写入和读取始终是原子的,无论它们是作为32位值还是64位值实现的
这从Java5开始生效
但请注意:
如果没有同步(synch block或volatile),您可能会发现每个线程都有自己的列表实例(每个线程都可以看到views==null
并初始化变量并使用自己的列表副本)
……和:
在这种实现下,每个线程可能会得到不同的
视图实例。可以吗
您的代码不一定是线程安全的。尽管“[w]引用的写入和读取始终是原子的…”,“[]Java内存模型不能保证对象在被其他线程引用时完全初始化。Java内存模型只保证对象的final
字段在任何线程看到对它的引用之前将被初始化:
只有在对象完全初始化后才能看到该对象引用的线程才能确保看到该对象最终字段的正确初始化值
因此,如果实现ListUtil.toUnmodifiableList(viewsListString)
返回包含任何非最终字段的列表
对象,在初始化非最终字段之前,其他线程可能会看到列表
引用
例如,假设toUnmodifiableList()
方法的实现类似于:
public static List<String> toUnmodifiableList(final String viewsString) {
return new AbstractList<String>() {
String[] viewsArray = viewsString.split(",");
@Override
public String get(final int index) {
return viewsArray[index];
}
};
}
当线程A执行时,线程B在线程A执行this.views=views之后调用getViews(servletContext)
代码>但在视图字符串.split(“,”)完成之前
现在线程B有一个对this.views
的引用,其中this.views.viewsArray
为null
,因此对this.views.get(index)
的任何调用都将导致NullPointerException
为了确保线程安全,由getViews()
返回的任何对象都需要确保它只有final
字段,以确保线程不会看到部分初始化的对象(或者可以确保在对象中正确处理未初始化的值,但这可能是不可能的)。我相信您需要确保getViews()
返回的对象中的所有Object
引用也只有final
字段。因此,如果您返回了一个列表
,其中包含对MyClass
的final
引用,则需要确保所有MyClass
的成员也是final
有关更多信息,请查看:。如果不进行同步(同步块或volatile),您可能会发现每个线程都有自己的列表实例(每个线程都可以看到视图==null
并初始化变量并使用自己的列表副本)。对引用和原语的访问是原子的。通过Java 5.0解决了对64位值的原子访问问题。在此实现下,每个线程都可能最终得到不同的视图实例。这样可以吗?@erickson,啊,我真的没有想过,从技术上讲,所有这些实例都会包含相同的数据,但我想这可能会导致潜在的内存问题。为了回答这个问题,让我们假设每个线程都有自己的实例是可以的,但是我很高兴考虑到这个陷阱。谢谢@Stiemankj1字符串的散列是一个延迟初始化的32位值,没有易失性语义,因为快速计算具有相同的结果。然而,除非与非常安全、性能关键的代码隔离,否则这些技巧通常是不值得的。0@PeterLawrey-谢谢!我不知道它从来没有
views = /* Reference to AbstractList<String> prior to initialization */
this.views = views;
/* new AbstractList<String>() occurs and viewsString.split(",") completes */