重要的是要了解线程安全有两个方面。
- 执行控制,以及
- 内存可见性
第一个与控制代码何时执行(包括执行指令的顺序)以及它是否可以同时执行有关,第二个与其他线程可以看到存储器中已完成操作的效果有关。由于每个CPU与主内存之间都有多个高速缓存级别,因此在不同CPU或内核上运行的线程在任何给定的时间点都可以以不同的方式查看“内存”,因为允许线程获取并使用主内存的专用副本。
使用synchronized
防止任何其他线程获取同一对象的监视器(或锁),从而防止在同一对象上受同步保护的所有代码块并发执行。同步还会创建“先于先发生”的内存屏障,从而导致内存可见性约束,使得直到某个线程释放锁的点之前所做的所有操作都在另一个线程中出现,随后又在获取该锁之前获取了相同的锁。实际上,在当前硬件上,这通常会导致在获取监视器时刷新CPU高速缓存,并在释放监视器时写入主内存,这两者都是(相对)昂贵的。
使用volatile
,而另一方面,将强制所有访问(读或写)到易失性可变发生到主存储器,有效地把挥发性变量out CPU的高速缓存。这对于某些仅要求变量的可见性正确且访问顺序不重要的操作很有用。使用volatile
还改变了对它们的处理,long
并double
要求对其进行原子访问;在某些(较旧的)硬件上,这可能需要锁,但在现代64位硬件上则不需要。在适用于Java 5+的新(JSR-133)内存模型下,就内存可见性和指令顺序而言,volatile的语义已得到增强,几乎与同步一样强大(请参见http://www.cs.umd.edu /users/pugh/java/memoryModel/jsr-133-faq.html#volatile)。出于可见性的目的,对易失字段的每次访问都像同步的一半。
在新的内存模型下,volatile变量不能彼此重新排序仍然是正确的。区别在于,现在不再很容易对它们周围的常规字段访问进行重新排序。写入易失性字段具有与监视器释放相同的存储效果,而从易失性字段中进行读取具有与监视器获取相同的存储效果。实际上,由于新的内存模型对易失性字段访问与其他字段访问(无论是否为易失性)的重新排序施加了更严格的约束,A
因此在写入易失性字段f
时线程看到的任何内容B
在读取时对线程都是可见的f
。
- JSR 133(Java的内存模型)的常见问题解答
因此,现在两种形式的内存屏障(在当前的JMM下)都会导致指令重新排序屏障,这会阻止编译器或运行时跨屏障对指令进行重新排序。在旧的JMM中,volatile不会阻止重新排序。这一点很重要,因为除了内存障碍之外,唯一的限制是,对于任何特定线程,代码的最终效果都与如果指令以它们在内存 中出现的顺序精确执行的情况相同。资源。
volatile的一种用法是在运行时重新创建共享但不可变的对象,许多其他线程在其执行周期中的某个特定时刻引用该对象。一旦发布了重新创建的对象,就需要其他线程开始使用它,但是不需要完全同步的额外开销,也不需要随之而来的争用和缓存刷新。
// Declaration
public class SharedLocation {
static public SomeObject someObject=new SomeObject(); // default object
}
// Publishing code
// Note: do not simply use SharedLocation.someObject.xxx(), since although
// someObject will be internally consistent for xxx(), a subsequent
// call to yyy() might be inconsistent with xxx() if the object was
// replaced in between calls.
SharedLocation.someObject=new SomeObject(...); // new object is published
// Using code
private String getError() {
SomeObject myCopy=SharedLocation.someObject; // gets current copy
...
int cod=myCopy.getErrorCode();
String txt=myCopy.getErrorText();
return (cod+" - "+txt);
}
// And so on, with myCopy always in a consistent state within and across calls
// Eventually we will return to the code that gets the current SomeObject.
具体来说,请讲您的读写更新问题。考虑以下不安全代码:
public void updateCounter() {
if(counter==1000) { counter=0; }
else { counter++; }
}
现在,在不同步updateCounter()方法的情况下,两个线程可以同时输入它。在可能发生的多种排列中,一个是线程1对counter == 1000进行了测试,发现它为true,然后被挂起。然后线程2进行了相同的测试,并且也看到它是正确的并被挂起。然后线程1恢复并将计数器设置为0。然后线程2恢复并再次将计数器设置为0,因为它错过了线程1的更新。即使未发生线程切换,也可能发生这种情况,这仅仅是因为两个不同的CPU内核中存在两个不同的计数器缓存副本,并且每个线程都在一个单独的内核上运行。为此,一个线程可能由于缓存而在一个值上具有计数器,而另一个线程可能在某个完全不同的值上具有计数器。
在此示例中重要的是,变量计数器是从主内存中读取到缓存中,在缓存中进行更新,并且仅在出现内存障碍或需要缓存内存用于其他内容时,才在某个不确定的时间点写回到主内存中。volatile
对于该代码的线程安全而言,使计数器不足是因为对最大值的测试和分配是离散操作,包括增量(一组非原子read+increment+write
机器指令),例如:
MOV EAX,counter
INC EAX
MOV counter,EAX
易变变量仅在对其执行的所有操作都是“原子的” 时才有用,例如在我的示例中,仅读取或写入对完全形成的对象的引用(实际上,通常仅从单个点写入)。另一个示例是支持写时复制列表的易失性数组引用,条件是仅首先通过对引用进行本地复制才能读取该数组。