JIT与可见性

原文地址 (被墙移到墙内)

 Overview

Many developers of multi-threaded code are familiar with the idea that different threads can have a different view of a value they are holding, this not the only reason a thread might not see a change if it is not made thread safe.  The JIT itself can play a part.

Why do different threads see different values?

When you have multiple threads, they will attempt to minimise how much they will interact e.g. by trying to access the same memory.  To do this they have a separate local copy e.g. in Level 1 cache.  This cache is usually eventually consistent.  I have seen short periods of between one micro-second and up to 10 milli-seconds where two threads see different values.  Eventually the thread is context switched, the cache cleared or updated.  There is no guarantee as to when this will happen but it is almost always much less than a second.

How can the JIT play a part?

The Java Memory Model says there is no guarantee that a field which is not thread safe will ever see an update.  This allows the JIT to make an optimisation where a value only read and not written to is effectively inlined into the code.  This means that even if the cache is updated, the change might not be reflected in the code.

An example

This code will run until a boolean is set to false.

01 <br />
02 static class MyTask implements Runnable {<br />
03     private final int loopTimes;<br />
04     private boolean running = true;<br />
05     boolean stopped = false;</p>
06 <p>    public MyTask(int loopTimes) {<br />
07         this.loopTimes = loopTimes;<br />
08     }</p>
09 <p>    @Override<br />
10     public void run() {<br />
11         try {<br />
12             while (running) {<br />
13                 longCalculation();<br />
14             }<br />
15         } finally {<br />
16             stopped = true;<br />
17         }<br />
18     }</p>
19 <p>    private void longCalculation() {<br />
20         for (int i = 1; i &lt; loopTimes; i++)<br />
21             if (Math.log10(i) &lt; 0)<br />
22                 throw new AssertionError();<br />
23     }<br />
24 }</p>
25 <p>public static void main(String... args) throws InterruptedException {<br />
26     int loopTimes = Integer.parseInt(args[0]);<br />
27     MyTask task = new MyTask(loopTimes);<br />
28     Thread thread = new Thread(task);<br />
29     thread.setDaemon(true);<br />
30     thread.start();<br />
31     TimeUnit.MILLISECONDS.sleep(100);<br />
32     task.running = false;<br />
33     for (int i = 0; i &lt; 200; i++) {<br />
34         TimeUnit.MILLISECONDS.sleep(500);<br />
35         System.out.println("stopped = " + task.stopped);<br />
36         if (task.stopped)<br />
37             break;<br />
38     }<br />
39 }<br />

This code repeatedly performs some work which has no impact on memory. The only difference it makes is how long it takes. By taking longer, it determines whether the code in run() will be optimised before or after running is set to false.

If I run this with 10 or 100 and -XX:+PrintCompilation I see

111    1     java.lang.String::hashCode (55 bytes)
112    2     java.lang.String::charAt (29 bytes)
135    3     vanilla.java.perfeg.threads.OptimisationMain$MyTask :longCalculation (35 bytes)
204    1 % ! vanilla.java.perfeg.threads.OptimisationMain$MyTask :run @ 0 (31 bytes)
stopped = false
stopped = false
stopped = false
stopped = false
... many deleted ...
stopped = false
stopped = false
stopped = false
stopped = false
stopped = false

If I run this with 1000 you can see that the run() hasn’t been compiled and the thread stops

112    1     java.lang.String::hashCode (55 bytes)
112    2     java.lang.String::charAt (29 bytes)
133    3     vanilla.java.perfeg.threads.OptimisationMain $MyTask::longCalculation (35 bytes)
135    1 %   vanilla.java.perfeg.threads.OptimisationMain $MyTask::longCalculation @ 2 (35 bytes)
stopped = true

Once the thread has been compiled, the change is never seen even though the thread will have context switched etc. many times.

How to fix this

The simple solution is to make the field volatile.  This will guarantee the field’s value consistent, not just eventually consistent which is what the cache might do for you.

Conclusion

While there are many examples of question like; Why doesn’t my thread stop? The answer has more to do with Java Memory Model which allows the JIT “inline” the fields that it does the hardware and having multiple copies of the data in different caches.

时间: 2024-08-18 23:47:45

JIT与可见性的相关文章

.Net Jit层脱壳机的实现原理

本文将在 .Net 反射脱壳机核心源代码 的基础上介绍,如何实现 Jit层脱壳 机. 首先我们选择使用 C++/CLI 来完成这个工作.反射部分需要用到 .Net的相关类库,jit 层 hook 需要使用native c++ 方面的功能.本文假设您已经完成了 hook jit的工作,并截获到了相关结构体. 首先介绍一下代码主要流程: 入口函数 void DumpAssembly(Assembly ass,string path) 枚举所有type,调用 void DumpType(Type tp

JIT编译

编译 在MSIL执行前,必须通过.NET框架的JIT编译器编译成本地代码,这个本地代码是基于特定CPU的,而且必须和JIT运行在同一套计算机体系结构上.由于运行时对于其运行的每一个CPU架构都提供了一个JIT编译器,开发人员可以编写一套可以通过JIT编译从而运行在不同架构的计算机上的MSIL代码(如果你的受控代码调用了特定平台的API或者类库,那么你的代码就只能在特定的计算机上执行了).    JIT编译的一个想法是基于认识到一些代码可能在执行过程中从来都没有被调用过这样一个事实,因而与其花费时

用WinDbg探索CLR世界 [3] 跟踪方法的 JIT 过程

过程 本来想按照 sos 的帮助文件上命令的分类逐步介绍 WinDbg 下使用 sos 调试 CLR 程序,但发现这样实在不够直观.索性改成根据我分析 CLR 的实际案例,step by step 介绍功能,这样结构上虽然混乱一点,但更加直观,也易于上手 :P 前面两篇文章里面分别介绍了 WinDbg 的调试配置和线程的基本概念,这篇文章将针对 JIT 编译对象方法的流程进行分析,逐步介绍如何使用 WinDbg 调试 CLR 程序. 用WinDbg探索CLR世界 [1] - 安装与环境配置用Wi

深入理解.NET 的JIT编译方式

编译 CLR只执行本机的机器代码.有两种方式产生本机的机器代码:实时编译(JIT)和预编译方式(产生native image).下面,我想谈谈JIT.CLR使用类型的方法表来路由所有的方法调用.类型的方法表由多个入口项组成.每个入口项指向一个唯一的存根例程(stub routine).初始化时,每个存根例程包含一个对于CLR的JIT编译器的调用(它由内部的PreStubWorker程序公开).在JIT编译器生成本机代码后,它会重写存根例程,插入一个jmp指令跳转到刚才JIT编译器的代码.只有当要

控制共享库的符号可见性(一) 符号可见性简介

什么是符号和符号可见性 符号是谈及对象文件.链接等内容时的基本术语之一.实际上,在 C/C++ 语言中,符号是很多用户定义的变量.函数名称以 及一些名称空间.类/结构/名称等的对应实体.例如,当我们定义非静态全局变量或非静态函数时,C/C++ 编译器就会在对象文 件中生成符号,这些符号对于链接器(linker)确定不同模块(对象文件.动态共享库.可执行文件)是否会共享相同的数据或 代码很有用. 尽管变量和函数都可能会在模块之间共享,但是对象文件之间的变量共享更为常见.例如,程序员可能会在 a.c

你可能错过的东西:.NET 3.5 SP1中的JIT增强

一个主要的影响就是内联函数(Inlining Function)调用.之前,JIT对内联方法的处理非常保守,Vance Morrison解释了个中缘由, 它对内联的处理并不是很好.内联总是减少指令执行的数量(这是由于最低限度的调用和返回指令没有被执行),但是它能(并经常)让结果代码变得很大.大部分人都能直觉地理解,内联大的方法(比如1Kb的)不是很有意义,而内联非常小的方法可以让调用的占用空间更小(由于调用指令才5字节),这样的选择总是正确的,但是介于两者之间的方法要如何处理呢? 有趣的是,当你

.Net Discovery系列之六 Me JIT(下)

接上文 在初始化时,HashTable中各个方法指向的并不是对应的内存入口地址,而是一个JIT预编译代理,这个函数负责将方法编译为本地代码.注意,这里JIT还没有进行编译,只是建立了方法表! 下表(表1)为首次加载调用时HashTable的情况: 表1 方法表示意 方法槽 方法描述 a1() PreJitStub a2() PreJitStub a3() PreJitStub 好了有了这个HashTable后,JIT开始编译第一个被调用的方法A.a1("First"),这是由一个JIT

.Net Discovery系列之五 Me JIT(上)

JIT(Just In Time简称JIT)是.Net边运行边编译的一种机制,这种机制的命名来源于丰田汽车在20世纪60年代实行的一种生产方式,中文译为"准时制". .Net 的JIT编译器在设计初衷和运行方式来上讲,都与丰田汽车的这种"准时生产"思想体系有着很大的相似之处,所以让我们先来透过"准时生产"方式来理解.Net的JIT机制吧. "准时生产"的基本思想可概括为"在需要的时候,按需要的量生产所需的产品&quo

探讨Java内部类的可见性

在Java中,当生成一个内部类的对象时,此对象与制造它的外部类通过外部类的.this保持着联系,因此该内部类对象可以访问其外部类对象的所有成员,包括private成员. 而该内部类对象对于其他类的对象的访问,遵照常规的访问权限语法,这一点也没有什么特别支持.这里需要探讨的是,外部类以及其他类的对象可以如何访问到某个内部类对象,即内部类的可见性问题. 下面是一个示例程序Out.java,其中包含了4个不同访问权限的内部类(private,default,protected,public),在每个内