java 内存泄露修复_修复Java内存模型,第2部分

编写并发代码很难开始。 语言不应该使它变得更难。 Java平台从一开始就包括对线程的支持,包括旨在为正确同步的程序提供“一次写入,随处运行”保证的跨平台内存模型,但原始内存模型仍有一些漏洞。 尽管许多Java平台提供了比JMM所需的更强的保证,但JMM中的漏洞破坏了轻松编写可在任何平台上运行的并发Java程序的能力。 因此,在2001年5月,JSR 133成立,负责修复Java内存模型。 上个月 ,我谈到了其中一些漏洞。 这个月,我将讨论它们是如何插入的。

再次公开

理解JMM所需的关键概念之一是可见性 -您如何知道如果线程A执行someVariable = 3 ,其他线程将看到线程A在someVariable = 3写入值3? 存在另一个原因,为什么另一个线程可能不会立即为someVariable看到值3:可能是因为编译器已对指令进行了重新排序以更有效地执行,或者是someVariable被缓存在寄存器中,或者其值已被写入写入写处理器上的高速缓存,但尚未刷新到主内存,或者读处理器的高速缓存中有旧的(或陈旧的)值。 内存模型决定了线程何时可以可靠地“看到”对其他线程所做的变量的写入。 特别是,内存模型为volatilesynchronizedfinal定义了语义,从而保证了跨线程的内存操作可见性。

当线程作为释放关联监视器的一部分退出同步块时,JMM要求将本地处理器缓存刷新到主内存。 (实际上,内存模型不是在谈论高速缓存,而是在谈论抽象, 本地内存 ,其中包括高速缓存,寄存器以及其他硬件和编译器优化。)类似,作为进入同步块时获取监视器的一部分,本地缓存无效,因此后续读取将直接进入主内存,而不是本地缓存。 该过程保证了,当变量在给定监视器保护的同步块中由一个线程写入,而在同一监视器保护的同步块中由另一线程读取时,对变量的写入将由读取线程可见。 在没有同步的情况下,JMM不能保证这一点-这就是为什么每当多个线程访问相同的变量时都必须使用同步(或者它的年轻同级volatile )的原因。

挥发物的新保证

volatile的原始语义仅保证对volatile字段的读写将直接在主存储器中进行,而不是在寄存器或本地处理器高速缓存中进行,并且以线程的顺序执行对volatile变量的操作线程请求。 换句话说,这意味着旧的内存模型仅对读取或写入的变量的可见性作出保证,而对其他变量的写入的可见性不作出保证。 尽管这更容易有效实施,但结果却没有最初想象的有用。

虽然不能对易失性变量的读写进行其他易失性变量的读写排序,但仍可以对非易失性变量进行读写排序。 在第1部分中 ,您学习了清单1中的代码如何(在旧的内存模型下)不足以保证configOptions的正确值以及可以通过configOptions间接访问的所有变量(例如Map的元素)是可见的线程B,因为configOptions的初始化可能已经用volatile initialized变量的初始化重新排序了。

清单1.使用一个volatile变量作为“保护”
Map configOptions;
char[] configText;
volatile boolean initialized = false;

// In Thread A
configOptions = new HashMap();
configText = readConfigFile(fileName);
processConfigOptions(configText, configOptions);
initialized = true;

// In Thread B
while (!initialized) 
  sleep();
// use configOptions

不幸的是,这种情况是volatile的常见用例-使用volatile字段作为“保护”来指示一组共享变量已被初始化。 JSR 133专家组决定,对于易失性读写来说,不要与任何其他内存操作一起重新排序会更明智-正是为了支持这种情况和其他类似用例。 在新的内存模型下,当线程A写入易失性变量V,而线程B从V读取时,现在保证了在写入V时A可见的任何变量值对B可见。结果是volatile的更有用的语义,但代价是访问volatile字段的性能损失更高。

在什么之前发生什么?

诸如变量的读取和写入之类的操作在线程内根据所谓的“程序顺序”进行排序-程序的语义说它们应该发生的顺序。 (实际上,只要保留串行语义,编译器就可以自由地在线程中享有程序顺序的自由。)不同线程中的动作不一定完全相对于彼此进行排序-如果您开始,两个线程,它们分别执行不会对任何普通显示器同步或接触任何共同volatile变量,可以预测有关的相对顺序在一个线程中的行动将执行究竟什么 (或者变得可见第三线程)相对于行动另一个线程。

当一个线程启动,一个线程与另一个线程连接,一个线程获取或释放监视器(进入或退出同步块)或线程访问易失性变量时,会创建其他排序保证。 JMM描述了当程序使用同步或易失性变量来协调多个线程中的活动时所作出的排序保证。 非正式地,新的JMM定义了一个名为巧合-之前的顺序,它是程序中所有动作的部分顺序,如下所示:

  • 线程中的每个动作都发生-在该线程中的每个动作之前 ,该顺序在程序顺序中排在后面
  • 监视器上的解锁发生-在同一监视器上的每个后续锁定之前
  • 在每次后续读取相同的volatile 之前,都会写入volatile字段
  • 在启动线程中的任何操作之前,都会在线程上发生对Thread.start()调用
  • 线程中的所有动作发生-在任何其他线程成功从该线程上的Thread.join()返回之前

这是这些规则中的第三条,它是控制volatile变量的读写的新规则,它解决了清单1中示例的问题。因为initialized的volatile变量的写入发生在configOptions initialized之后, configOptions的读取之后发生initialized ,以及读取initialized写后发生的initialized ,您可以断定的初始化configOptions由线程A使用之前发生configOptions由线程B.因此, configOptions和变量到达过它对线程B可见。

图1。 使用同步保证跨线程的内存写入可见性
图1。使用同步保证跨线程的内存写入可见性

数据竞赛

当存在一个变量时,该程序具有数据竞争 ,因此不是“适当同步”的程序,该变量具有一个以上的线程读取,至少一个线程写入的变量,并且写入和读取都不是按事前关系排序。

这样可以解决双重检查锁定问题吗?

提出的针对双重检查锁定问题的解决方案之一是使保存延迟初始化实例的字段成为易失性字段。 (请参阅相关主题的双重检查锁定问题的描述和为什么提出的算法修正不工作的解释。)在旧的内存模型,这并没有使双重检查锁定线程安全的,因为写对volatile字段的写操作仍然可以通过对其他非易失性字段(例如,新建对象的字段)的写操作进行重新排序,因此,volatile实例引用仍然可以保留对未完全构造的对象的引用。

在新的内存模型下,对“双重检查”锁定的“修复”使该成语具有线程安全性。 但这仍然并不意味着您应该使用这个惯用法! 双重检查锁定的全部要点是,它应该是一种性能优化,旨在消除公共代码路径上的同步,这在很大程度上是因为同步在非常早期的JDK中相对昂贵。 从那以后,不仅无竞争的同步变得便宜很多 ,而且volatile语义的新变化使其在某些平台上的价格比旧语义要昂贵得多。 (实际上,对volatile字段的每次读取或写入就像“半个”同步操作—读取volatile与监视器获取具有相同的内存语义,而写入volatile与监视器释放具有相同的语义。 )因此,如果双重检查锁定的目标应该是通过更直接的同步方法提供更高的性能,则此“固定”版本也无济于事。

代替双重检查锁定,使用Initialize-on-demand Holder Class惯用语,它提供了懒惰的初始化,是线程安全的,并且比双重检查锁定更快速,更少混乱:

清单2.按需初始化Holder类成语
private static class LazySomethingHolder {
  public static Something something = new Something();
}

...

public static Something getInstance() {
  return LazySomethingHolder.something;
}

该习惯用法来自这样的事实,即它保证了类初始化的一部分操作(例如静态初始化程序)对于使用该类的所有线程都是可见的,而它的惰性初始化是由于未加载内部类的事实而得出的。直到某个线程引用其字段或方法之一。

初始化安全

新的JMM还试图为初始化安全性提供新的保证-只要正确构造了一个对象(这意味着在构造函数完成之前不会发布对该对象的引用),那么所有线程都将看到它的最终字段是在其构造函数中设置的,无论是否使用同步将引用从一个线程传递到另一个线程。 此外,也可以保证通过适当构造的对象的最终字段(例如,由最终字段引用的对象的字段)可以到达的任何变量也对其他线程可见。 这意味着,如果最后一个字段包含对LinkedList的引用,除了该引用的正确值对其他线程可见之外,该LinkedList的内容在构造时也将对其他线程可见,而无需同步。 结果显着增强了final的含义-可以安全地访问final字段而无需同步,并且编译器可以假定final字段不会改变,因此可以优化多次提取。

最终意味着最终

第1部分中概述了在旧的内存模型下,最终字段似乎可以更改其值的机制-在没有同步的情况下,另一个线程可以首先看到最终字段的默认值,然后再看到正确的值。

在新的内存模型下,在构造函数中写入final字段与在另一个线程中对该对象的共享引用的初始加载之间存在类似事前发生的关系。 构造函数完成后,对final字段(以及通过这些final字段可以间接访问的变量)的所有写入都变为“冻结”,并且保证冻结后获得对该对象的引用的任何线程都可以看到所有冻结值。冰冻的田野。 与构造函数关联的冻结之后,不会初始化初始化final字段的写入操作。

摘要

JSR 133大大增强了volatile的语义,因此volatile标志可以可靠地用作指示程序状态已被另一个线程更改的指示符。 由于使volatile更“重”,在某些情况下,使用volatile的性能成本已接近于同步的性能成本,但是在大多数平台上,性能成本仍然很低。 JSR 133还大大增强了final的语义。 如果不允许在构造过程中转义对象的引用,则一旦构造函数完成并且线程发布对对象的引用,就可以保证该对象的final字段对于所有其他线程都是可见的,正确的且恒定的,而无需同步。

这些更改极大地增强了并发程序中不变对象的实用性。 即使使用数据竞争在线程之间传递对不可变对象的引用,不可变对象最终也将本质上成为线程安全的(因为它们一直都是这样)。

关于初始化安全性的一个警告是,对象的引用一定不能“转义”其构造函数-构造函数不应直接或间接发布对正在构造的对象的引用。 这包括发布对非静态内部类的引用,并且通常会阻止从构造函数内部启动线程。 有关安全构造的更详细说明,请参阅参考资料


翻译自: https://www.ibm.com/developerworks/java/library/j-jtp03304/index.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值