重要的是要理解线程安全有两个方面。
1.执行控制
2.内存可见性
第一个与控制代码何时执行(包括指令执行顺序)以及是否可以并发执行有关,而第二个与其他线程何时能够看到已经完成的内存操作有关。由于每个CPU在它和主内存之间有几个级别的缓存,因此运行在不同CPU或核心上的线程在任何给定时间可能会以不同的方式看待“内存”,因为线程被允许获取和处理主内存的私有副本。
使用synchronized防止其他线程获取相同对象的监视器(或锁),从而防止受同步保护的所有代码块在同一对象上并发执行。同步还创建了“happens-before”内存屏障,导致内存可见性约束,使得在某个线程释放锁之前完成的所有操作似乎已经在另一个随后获取相同锁的线程中发生。在实际应用中,对于当前硬件来说,这通常会导致在获取监视器时刷新CPU缓存,并在释放监视器时写入主内存,这两者都是(相对)昂贵的。
使用volatile
可以强制所有对volatile变量的访问(读或写)发生在主内存中,有效地使volatile变量不进入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时对线程A可见的任何内容,在线程B读取f时也将对线程B可见。--
JSR 133 (Java Memory Model) FAQ
因此,现在在当前JMM下,两种形式的内存屏障都会导致指令重排序屏障,从而防止编译器或运行时跨越屏障重新排序指令。在旧的JMM中,易失变量无法防止重排序。这可能很重要,因为除了内存屏障之外,唯一强制执行的限制是对于任何特定的线程,代码的净效果与以源代码中出现的顺序精确执行指令的效果相同。
易失变量的一个用途是为共享但不可变对象重新创建对象,并在其执行周期的特定点上许多其他线程引用该对象。我们需要其他线程在发布后开始使用重新创建的对象,但不需要完全同步的额外开销及其伴随的争用和缓存刷新。
public class SharedLocation {
static public volatile SomeObject someObject=new SomeObject();
}
SharedLocation.someObject=new SomeObject(...);
private String getError() {
SomeObject myCopy=SharedLocation.someObject;
...
int cod=myCopy.getErrorCode();
String txt=myCopy.getErrorText();
return (cod+" - "+txt);
}
谈到你的读-更新-写问题,具体来说。考虑以下不安全的代码:
public void updateCounter() {
if(counter==1000) { counter=0; }
else { counter++; }
}
现在,由于updateCounter()方法未同步,两个线程可能同时进入该方法。在许多可能发生的排列组合中,其中之一是线程1进行计数器==1000的测试并发现其为真,然后被暂停。然后线程2进行相同的测试,也看到它为真,并被暂停。然后线程1恢复并将计数器设置为0。然后线程2恢复并再次将计数器设置为0,因为它错过了线程1的更新。即使线程切换没有如我所描述的那样发生,这也可能发生,但只是因为两个不同的CPU核心中存在两个不同的缓存副本,并且线程分别在单独的核心上运行。说起来,一个线程可以具有一个值的计数器,而另一个线程可以具有某些完全不同的值,仅因为缓存。
在这个例子中重要的是,变量counter从主内存读取到缓存中,在缓存中更新,只有在稍后出现内存屏障或需要缓存内存用于其他用途时才写回主内存。将计数器设置为volatile对于此代码的线程安全性是不足够的,因为最大值测试和赋值是离散操作,包括增量,这是一组非原子的读取+增量+写入机器指令,类似于:
MOV EAX,counter
INC EAX
MOV counter,EAX
易失变量只有在所有对它们执行的操作都是“原子”的时候才有用,比如我的例子中,对完全形成的对象的引用仅被读取或写入(实际上通常只从一个点写入)。另一个例子是易失数组引用支持写时复制列表,前提是该数组仅通过首先取本地副本引用来读取。