Java内存模型:
- 了解java内存模型,便于我们更容易理解如安全发布,同步策略的规范及一致性等机制。
什么是内存模型,为什么需要它
- JMM规定了JVM必须遵循一组最小保证,这组保证规定了对变量的写入操作在何时将对其他线程可见。
平台的内存模型
- JVM通过在适当的位置上插入内存栅栏来屏蔽在JMM与底层平台内存模型之间的差异。
重排序
/**
* 如果在程序中没有包含足够的同步, 那么可能产生奇怪的结果
*/
public class PossibleReordering {
static int x = 0, y = 0;
static int a = 0, b = 0;
public static void main(String[] args) throws InterruptedException {
Thread one = new Thread(new Runnable(){
@Override
public void run() {
a = 1;
x = b;
}
});
Thread other = new Thread(new Runnable(){
@Override
public void run() {
b = 1;
y = a;
}
});
one.start();
other.start();
one.join();
other.join();
System.out.println("x = " + x + ", y = " + y);
}
}
由于重排序导致的不确定性:
Java内存模型简介
- Java内存模型是通过各种操作来定义的,包括变量的读/写操作,监视器的加锁和释放操作,以及线程的启动和合并操作。
- JMM为程序中所有的操作定义了一个偏序关系,称为Happens-Before。两个操作缺乏Happens-Before关系,则Jvm会对它们进行任意的重排序。
- Happends-Before的规则包括:
1. 程序顺序规则。若程序中操作A在操作B之前,则线程中操作A在操作B之前执行。
2. 监视器锁规则。在同一监视器锁上的解锁操作必须在加锁操作之前执行。如图所示,
3. volatile变量规则。对volatile变量的写操作必须在读操作之前执行。
4. 线程启动规则。Thread.start必须在线程中执行的操作之前被调用。
5. 线程关闭规则。线程中的任何操作必须在其他线程检测到该线程结束之前执行,或者从Thread.join中返回,或调用Threas.isAlive返回false。
6. 中断规则。当一个线程在另一个线程上调用interrupt时,必须在被中断线程检测到interrupt调用之前执行。
7. 终结器规则。对象的构造函数必须在启动该对象的终结器之前执行完成。
8. 传递性。如果操作A在操作B之前执行,操作B在操作C之前执行,则操作A必须在操作C之前执行。
借助同步
- 一个借助同步的例子
public class Sync extends AbstractQueuedSynchronizer {
private static final int RUNNING = 1, RAN = 2, CANCELLED = 4;
private Object result;
private Exception exception;
void innerSet(Object o){
while (true){
int s = getState();
if (ranOrCancelled(s)){
return;
}
if (compareAndSetState(s, RAN)){ //设置状态
break;
}
}
result = o;
releaseShared(0); //共享模式下释放锁
done();
}
Object innerGet() throws InterruptedException, ExecutionException{
acquireSharedInterruptibly(0); //共享模式下请求锁
if (getState() == CANCELLED){
throw new CancellationException();
}
if (exception != null){
throw new ExecutionException(exception);
}
return result;
}
}
类库中提供的其他Happens-Before排序包括:
- 将一个元素放入一个线程安全容器的操作将在另一个线程从该容器中获得这个元素的操作之前执行。
- 在CountDownLatch上的倒数操作将在线程从闭锁上的await方法中返回之前执行。
- 释放Semaphore许可的操作将在从该Semaphore上获得一个许可之前执行。
- Future表示的任务的所有操作将在Future.get中返回之前执行。
- 向Executor提交一个Runnable或Callable将在任务开始执行之前执行。
- 一个线程到达CyclicBarrier或Exchanger的操作将在其他到达该栅栏或交换点的线程释放之前执行。如果CyclicBarrier使用一个栅栏操作,那么到达栅栏的操作将在栅栏操作之前执行,而栅栏操作又会在线程从栅栏中释放之前执行。
发布
- 造成不正确发布的真正原因:"发布一个共享对象"与"另一个线程访问该对象"之间缺少一种Happens-Before的关系。
不安全的发布
/**
* 不安全的延迟初始化
*/
public class UnsafeLazyInitialization {
private static Object resource;
public static Object getInstance(){
if (resource == null){
resource = new Object(); //不安全的发布
}
return resource;
}
}
- 除了不可变对象以外,使用被另一个线程初始化的对象通常都是不安全的,除非对象的发布操作是在使用该对象的线程开始使用之前执行。
安全的发布
- Happens-Before排序是在内存访问级别上操作的,它是一种"并发级汇编语言",而安全发布的运行级别更接近程序设计。
安全初始化模式
通过同步实现安全发布,带来一定的性能开销
/**
* 通过同步安全发布
*/
public class UnsafeLazyInitialization {
private static Object resource;
public synchronized static Object getInstance(){
if (resource == null){
resource = new Object(); //不安全的发布
}
return resource;
}
}
提前初始化实现安全发布:
/**
* 提前初始化
*/
public class EagerInitialization {
private static Object resource = new Object();
public static Object getInstance(){
return resource;
}
}
类占位实现安全发布:
/**
* 延长初始化占位类模式
*/
public class ResourceFactory {
private static class ResourceHolder{
public static Object resource = new Object();
}
public static Object getInstance(){
return ResourceHolder.resource;
}
}
双重检查加锁
/**
* 双重检查加锁, 不安全,
* 线程可能看到无效的值, 可加上volatile修饰
*/
public class DoubleCheckedLocking {
private static Object resource;
public static Object getInstance(){
if (resource == null){
synchronized (DoubleCheckedLocking.class){
if (resource == null){
resource = new Object();
}
}
}
return resource;
}
}
初始化过程中的安全性
/**
* 不可变对象的初始化安全性
*/
public class SafeStates {
private final Map<String, String> states; //确保final,以免由于并发而被修改
public SafeStates(){
states = new HashMap<String, String>();
states.put("one", "One");
//...
states.put("two", "Two");
//...
}
public String getAbbreviation(String s){
return states.get(s);
}
}
不吝指正。