getXXXVolatile与getXXX在Java Unsafe中有什么区别?

6

我正在尝试理解Java Unsafe中的两种方法:

   public native short getShortVolatile(Object var1, long var2);

对比

   public native short getShort(Object var1, long var2);

这里的真正区别是什么? volatile在这里真正起作用的是什么?我在这里找到了API文档:http://www.docjar.com/docs/api/sun/misc/Unsafe.html#getShortVolatile(Object,%20long),但它并没有真正解释两个函数之间的区别。我的理解是,对于volatile,只有在写入时才重要。对我来说,调用putShortVolatile,然后在读取时,我们可以简单地调用getShort(),因为volatile写入已经保证新值已刷新到主内存中。如果有任何错误,请指出。谢谢!

有趣的问题。您是否尝试寻找这些方法的原生实现? - Jacob G.
2
“对于volatile,只有在写入时才会产生影响” - 这是不正确的 - JLS中的Java内存模型部分描述了volatile修饰符的真正含义。例如,在读取时,它意味着该值不能被缓存在CPU寄存器中。无论如何,getShortVolatile不能具有与volatile修饰符相同的含义,因为它仅影响读取者。字段本身不需要是volatile,因此写入线程可以将该值缓存在CPU寄存器中,例如。 - Erwin Bolwidt
@ErwinBolwidt感谢您的评论,我认为您的评论倾向于解决我的困惑。那么,您能否帮助我看看这个陈述是否正确?对于特定的short值,如果程序只使用putShortVolatile()进行写入,则无需在此处使用getShortVolatile(),因为我们确信更新必须被刷新到内存中。 - Lubor
@ErwinBolwidt 在你的句子中,“所以写入线程可以将值缓存在CPU寄存器中,例如。” 你是指当我们使用 putShort() 进行写入时吗? - Lubor
1
sun.misc.Unsafe 不提供任何保证;它不是 Java 内存模型的一部分,而且它可以做一些破坏 Java 内存模型的事情。使用 sun.misc.Unsafe 的标准 API 遵循 Java 内存模型,但据我所知,没有官方文档说明它们如何实现。你最好的选择是:a)不要使用 sun.misc.Unsafe,而是使用常规反射来访问 volatile 字段;b)使用 Unsafe 的 putget 的 volatile 版本以确保安全。除非你了解 CPU 架构和 HotSpot 可能进行的优化,否则做出假设是不安全的。 - Erwin Bolwidt
@JacobG。确实是一个好问题,我在我的回答中假设这只是一个带有volatile语义的short类型读取。 - Eugene
2个回答

6
这里有一篇文章:http://mydailyjava.blogspot.it/2013/12/sunmiscunsafe.html Unsafe支持所有原始值,甚至可以使用方法的volatile形式写入值而不会命中线程本地缓存。
getXXX(Object target,long offset):将从指定偏移量的目标地址读取类型为XXX的值。
getXXXVolatile(Object target,long offset):将从指定偏移量的目标地址读取类型为XXX的值,并且不会命中任何线程本地缓存。 putXXX(Object target,long offset,XXX value):将在指定偏移量的目标地址处放置值。 putXXXVolatile(Object target,long offset,XXX value):将在指定偏移量的目标地址处放置值,并且不会命中任何线程本地缓存。
更新:
您可以在此文章中找到有关内存管理和易失性字段的更多信息:http://cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html(其中还包含一些重新排序的示例)。
在多处理器系统中,处理器通常具有一层或多层内存缓存,这提高了数据访问速度(因为数据更接近处理器)并减少了共享内存总线上的流量(因为许多内存操作可以通过本地缓存满足)。内存缓存可以极大地提高性能,但它们提出了许多新的挑战。例如,当两个处理器同时检查同一内存位置时会发生什么?在什么条件下,它们将看到相同的值?
某些处理器表现出强大的内存模型,在这种模型中,所有处理器始终以任何给定内存位置的完全相同值。其他处理器表现出较弱的内存模型,其中需要使用称为内存屏障的特殊指令来刷新或使本地处理器缓存失效,以便看到其他处理器进行的写入或使该处理器进行的写入对其他人可见。
写入何时对另一个线程可见的问题由编译器对代码的重新排序所加剧。如果编译器推迟了操作,则另一个线程将无法看到它,直到执行它;这反映了缓存的效果。此外,对内存的写入可以在程序中提前移动;在这种情况下,其他线程可能会在程序中实际“发生”之前看到写入。
Java包括几个语言构造,包括易失性、最终和同步,旨在帮助程序员向编译器描述程序的并发要求。Java内存模型定义了易失性和同步的行为,并更重要的是确保正确同步的Java程序在所有处理器体系结构上运行正确。
正如您可以在此处看到的那样。
易失性字段是用于在线程之间通信状态的特殊字段。每次读取易失性字段都会看到任何线程对该易失性字段的最后一次写入;实际上,它们被程序员指定为永远不能看到缓存或重新排序的“过时”值的字段。编译器和运行时禁止将它们分配到寄存器中。它们还必须确保在写入后,将它们从缓存刷新到主内存,以便其他线程可以立即看到它们。同样,在读取易失性字段之前,必须使缓存无效,以便主内存中的值而不是本地处理器缓存中的值成为可见的。
对易失变量的访问还有额外的重排序限制。无法将对易失变量的访问彼此重新排序。现在不再那么容易将普通字段访问重新排序。写入易失性字段具有与监视器释放相同的内存效果,从易失性字段读取具有与监视器获取相同的内存效果。实际上,由于新的内存模型对易失性字段访问与其他字段访问(易失性或非易失性)的重新排序施加了更严格的约束,因此当线程A写入易失性字段f时,对线程A可见的任何内容都会在线程B读取f时变得可见。
因此,区别在于setXXX()和getXXX()可以重新排序或者可能使用尚未在线程之间同步的缓存值,而setXXXVolatile()和getXXXVolatile()不会重新排序,并且始终使用最后一个值。
线程本地缓存是Java中用于提高性能的临时存储:数据将在写入/读取到/从缓存中之前被写入/读取到/从内存中。
在单个线程上下文中,您可以同时使用非易失性和易失性版本的这些方法,没有区别。当您写入某些内容时,无论它是否立即写入内存还是仅写入线程本地缓存中,都无关紧要:当您尝试读取它时,您将在相同的线程中,因此您肯定会获得最后一个值(线程本地缓存包含最后一个值)。
相反,在多线程上下文中,缓存可能会给您带来一些麻烦。如果您初始化了一个不安全的对象,并在两个或多个线程之间共享它,则每个线程都会在其本地缓存中拥有一个副本(两个线程可以在不同的处理器上运行,每个处理器都有其自己的缓存)。
如果您在线程上使用setXXX()方法,则新值可能会写入线程本地缓存中,但尚未写入内存。因此,可能会发生只有多个线程中的一个包含新值,而内存和其他线程的本地缓存包含旧值的情况。这可能会导致意外结果。setXXXVolatile()方法将直接将新值写入内存,因此其他线程也将能够访问新值(如果它们使用getXXXVolatile()方法)。

如果你使用getXXX()方法,你将得到本地缓存值。因此,如果另一个线程已经更改了内存中的值,当前线程的本地缓存仍可能包含旧值,你将得到意外的结果。如果你使用getXXXVolatile()方法,你将直接访问内存,并确保得到最新的值。

以前面链接的示例为例:

class DirectIntArray {
 
  private final static long INT_SIZE_IN_BYTES = 4;
   
  private final long startIndex;
 
  public DirectIntArray(long size) {
    startIndex = unsafe.allocateMemory(size * INT_SIZE_IN_BYTES);
    unsafe.setMemory(startIndex, size * INT_SIZE_IN_BYTES, (byte) 0);
    }
  }
 
  public void setValue(long index, int value) {
    unsafe.putInt(index(index), value);
  }
 
  public int getValue(long index) {
    return unsafe.getInt(index(index));
  }
 
  private long index(long offset) {
    return startIndex + offset * INT_SIZE_IN_BYTES;
  }
 
  public void destroy() {
    unsafe.freeMemory(startIndex);
  }
}

这个类使用putInt和getInt方法将值写入在内存中分配的数组(因此在堆空间之外)。 正如之前所说,这些方法会将数据写入线程本地缓存,而不是立即写入内存。因此,当您使用setValue()方法时,本地缓存将立即更新,分配的内存将在一段时间后更新(这取决于JVM的实现)。 在单线程环境下,该类将可以正常工作。 在多线程环境下可能会出现问题。

DirectIntArray directIntArray = new DirectIntArray(maximum);
Runnable t1 = new MyThread(directIntArray);
Runnable t2 = new MyThread(directIntArray);
new Thread(t1).start();
new Thread(t2).start();

MyThread是什么:

public class MyThread implements Runnable {
    DirectIntArray directIntArray;
    
    public MyThread(DirectIntArray parameter) {
        directIntArray = parameter;
    }

    public void run() {
        call();
    }
    
    public void call() {
        synchronized (this) {
            assertEquals(0, directIntArray.getValue(0L));  //the other threads could have changed that value, this assert will fails if the local thread cache is already updated, will pass otherwise
            directIntArray.setValue(0L, 10);
            assertEquals(10, directIntArray.getValue(0L));
        }
    }
}

通过使用putIntVolatile()和getIntVolatile(),两个线程中的一个肯定会失败(第二个线程将获得10而不是0)。 通过使用putInt()和getInt(),两个线程都可能成功完成(因为如果写入缓存没有刷新或读取缓存没有更新,两个线程的本地缓存仍然可能包含0)。


1
@debe 我不知道这是好还是坏;首先,你对“本地缓存”和“主内存”的术语(理解)有些错误,你可能实际上是在指重新排序和缓存一致性协议。然后我不明白你关于 putIntVolatilegetIntVolatile 的观点。你是说一旦 ThreadA 执行了 putIntVolatile,另一个 ThreadB 执行了 getIntVolatile,它应该看到 ThreadA 设置的最新值吗? - Eugene
1
在一个线程中写入一个volatile字段,并不意味着读取它将得到最新的值。 如果一个线程观察到了另一个线程所做的写操作 - 这意味着该线程将看到在此之前完成的所有其他写操作 - 只有这一点是有保证的,而且这个答案在根本上是错误的。而且你也把其他概念搞混了... - Eugene
当然,如果您使用易失性方法编写代码,但是使用非易失性方法读取代码,则读取可能会得到缓存的旧值。或者,您是否考虑过同步、加入、锁、信号量、原子包装器、线程安全集合和其他同步多线程应用程序的方法? - debe
@Eugene,感谢你的文章,我稍后会阅读。关于没有v == true的示例,由于x不是volatile,所以无法保证x == 42。即使x应该是volatile,我认为即使没有读取,它也应该被同步。难道不是这样吗? - debe
@Eugene 好的,我同意你的观点。正如文章所解释的那样,x == 42是有保证的,因为v是易失性变量,它的读取避免了语句x=42在重新排序后被移动到语句v=true之后。 - debe
显示剩余10条评论

1
我认为getShortVolatile从一个对象中读取一个普通的short,但将其视为volatile; 就像读取一个普通变量并自行插入所需的屏障(如果有的话)。
更简化的说(在某种程度上是错误的,但只是为了让你理解)。释放/获取语义:
Unsafe.weakCompareAndSetIntAcquire // Acquire
update some int here
Unsafe.weakCompareAndSetIntRelease // Release

关于为什么需要这个(这是针对getIntVolatile,但情况仍然存在),可能是为了强制执行非重排序。再次说明,这超出了我的能力范围,Gil Tene解释得更适合

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接