Java内存模型简介

Java 内存模型 (JMM)描述了线程在 Java 运行时环境中的行为。内存模型是 Java 语言语义的一部分,它描述了程序员在开发不是针对特定 Java 机器而是针对整个 Java 的软件时可以和不应该期望的内容。

1995 年开发的原始 Java 内存模型(特别是指“percolocal 内存”)被认为是失败的:许多优化无法在不失去代码安全保证的情况下进行。特别是,编写多线程“单”有几种选择:

  • 访问单例的每个行为(即使对象是很久以前创建的,并且什么都不能改变)都会导致线程间锁定;
  • 或者在某种情况下,系统会发出未完成的独行者;
  • 或者在某种情况下,系统会产生两个独行者;
  • 或者设计将取决于特定机器的行为。

因此,重新设计了内存机制。2005 年,随着 Java 5 的发布,提出了一种新方法,并随着 Java 14 的发布进一步改进。

新模型基于三个规则:

规则 #1:单线程程序伪顺序运行。这意味着:实际上,处理器可以在每个时钟执行多个操作,同时更改它们的顺序,但是,所有数据依赖性仍然存在,因此该行为与顺序操作没有区别。

规则 2:没有无处不在的值。读取任何变量(非易失性 long 和 double 除外,此规则可能不适用)将返回默认值(零)或由另一个命令写入的值。

规则3:其余事件按顺序执行,如果它们通过严格的偏序关系“先执行”(先发生)连接。

发生在之前

Leslie Lamport之前提出了 Happens 的概念。这是在原子命令(++ 和 -- 不是原子的)之间引入的严格偏序关系,并不意味着“物理上先于”。

它说第二个团队将“知道”第一个团队所做的更改。

发生在之前

例如,对于此类操作,一个先于另一个执行:

同步和监控:

  • 捕获监视器(锁定方法、同步启动)以及在它之后的同一线程上发生的任何事情。
  • 监视器的返回(方法解锁,同步结束)以及在它之前的同一线程上发生的任何事情。
  • 返回监视器,然后由另一个线程捕获它。

写作和阅读:

  • 写入任何变量,然后在同一流中读取它。
  • 在写入 volatile 变量之前在同一个线程中的所有内容,以及写入本身。volatile read 和它之后的同一个线程上的所有内容。
  • 写入 volatile 变量,然后再次读取它。易失性写入与内存交互的方式与监视器返回的方式相同,而读取则类似于捕获。事实证明,如果一个线程写入了一个 volatile 变量,而第二个线程找到了它,那么写入之前的所有内容都会在读取之后的所有内容之前执行;看图片。

对象维护:

  • 静态初始化和任何对象实例的任何操作。
  • 写入构造函数中的最终字段以及构造函数之后的所有内容。作为例外,happens-before 关系不会传递连接到其他规则,因此可能导致线程间竞争。
  • 使用对象和finalize()的任何工作。

流媒体服务:

  • 启动线程和线程中的任何代码。
  • 与线程和线程中的任何代码相关的变量清零。
  • thread 和join()中的代码;线程中的代码和isAlive() == false
  • interrupt()线程并检测到它已停止。

发生在工作细微差别之前

释放先行监视器发生在获取同一监视器之前。值得注意的是是release,而不是exit,即使用wait时不用担心安全问题。

让我们看看这些知识将如何帮助我们纠正我们的例子。在这种情况下,一切都非常简单:只需删除外部检查并保持同步不变。现在第二个线程可以保证看到所有的变化,因为它只有在另一个线程释放它之后才会得到监视器。并且由于他不会在一切都初始化之前释放它,所以我们将立即看到所有的变化,而不是分开:

public class Keeper {
    private Data data = null;

    public Data getData() {
        synchronized(this) {
            if(data == null) {
                data = new Data();
            }
        }

        return data;
    }
}

写入 volatile 变量发生在从同一变量读取之前。当然,我们所做的更改修复了错误,但它会将编写原始代码的人放回原处 - 每次都阻塞。volatile关键字可以保存。事实上,上述语句意味着当读取所有声明为 volatile 的内容时,我们将始终获得实际值。

另外,前面说了,对于volatile字段,写入总是(包括l​​ong和double)一个原子操作。另一个要点:如果你有一个 volatile 实体,它引用了其他实体(例如,数组、List 或其他一些类),那么只有对实体本身的引用将始终是“新鲜的”,而不是对它传入。

那么,回到我们的双锁公羊。使用 volatile,你可以像这样解决这个问题:

public class Keeper {
    private volatile Data data = null;

    public Data getData() {
        if(data == null) {
            synchronized(this) {
                if(data == null) {
                    data = new Data();
                }
            }
        }
        return data;
    }
}

这里我们仍然有一个锁,但前提是 data == null。我们使用 volatile read 过滤掉剩余的情况。volatile store happens-before volatile read 确保了正确性,并且构造函数中发生的所有操作对于读取字段值的人都是可见的。