这里有一篇文章:
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));
directIntArray.setValue(0L, 10);
assertEquals(10, directIntArray.getValue(0L));
}
}
}
通过使用putIntVolatile()和getIntVolatile(),两个线程中的一个肯定会失败(第二个线程将获得10而不是0)。
通过使用putInt()和getInt(),两个线程都可能成功完成(因为如果写入缓存没有刷新或读取缓存没有更新,两个线程的本地缓存仍然可能包含0)。
volatile
修饰符的真正含义。例如,在读取时,它意味着该值不能被缓存在CPU寄存器中。无论如何,getShortVolatile
不能具有与volatile
修饰符相同的含义,因为它仅影响读取者。字段本身不需要是volatile,因此写入线程可以将该值缓存在CPU寄存器中,例如。 - Erwin BolwidtputShortVolatile()
进行写入,则无需在此处使用getShortVolatile()
,因为我们确信更新必须被刷新到内存中。 - LuborputShort()
进行写入时吗? - Luborvolatile
字段;b)使用 Unsafe 的put
和get
的 volatile 版本以确保安全。除非你了解 CPU 架构和 HotSpot 可能进行的优化,否则做出假设是不安全的。 - Erwin Bolwidt