Java并发编程 - Java内存模型
您目前处于:技术核心竞争力  2014-07-03

什么是内存模型,为什么需要它

由于时钟频率越来与难以提高,因此许多处理器制造厂商都开始转而生产多核处理器,因此能够提高的只有硬件并发性。

对于并发应用程序中的线程来说,它们在大部分时间里都执行各自的任务。只有当多个线程要共享数据时,才必须协调它们之间的操作。

  • 平台的内存模型

  • 在共享内存的多处理器体系架构中,每个处理器都拥有自己的缓存,并且定期与主内存进行协调。在不同的处理器架构中提供了不同级别的缓存一致性。

    在架构定义的内存模型中将告诉应用程序可以从内存系统中获得怎样的保证,此外还定义了一些特殊的指令(称为内存栅栏或栅栏),当需要共享数据时,这些指令就能实现额外的存储协调保证。为了能使Java开发人员无须关心不同架构上内存模型之间的差异,Java还提供了自己的内存模型,并且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() {
                public void run() {
                    a = 1; x = b;
                }
            });
            Thread other = new Thread(new Runnable() {
                public void run() {
                    b = 1; y = a;
                }
            });
            one.start();
            other.start();
            one.join();
            other.join();
            System.out.println("(" + x + "," + y + ")");
        }
    }

    很容易想象PossibleReordering是如何输出(1,0)或(0,1)或(1,1)的;线程A可以在线程B开始之前就执行完成,线程B也可以在线程A开始之前执行完成,或者二者的操作交替执行。但奇怪的是,PossibleReordering还可以输出(0,0)。由于每个线程中的各个操作之间不存在的数据流依赖性,因此这些操作操作可以乱序执行。

  • Java内存模型

  • Java内存模型是通过各种操作来定义的,包括对变量的读/写操作,监视器的加锁和释放操作,以及线程的启动和合并操作。JMM为程序中所有的操作定义了一个偏序关系,称为Happens-Before。

    如果连个操作之间缺乏Happens-Before关系,那么JVM可以对它们任意地重排序。

    当一个变量被多个线程读取并且至少一个线程写入时,如果在读操作和写操作之间没有依靠Happens-Before来排序,那么就会产生数据竞争问题。

    Happens-Before规则包括:

    程序顺序规则。如果程序中操作A在操作B之前,那么在线程中A操作将在B操作之前执行。
    监视器锁规则。在监视器锁上的解锁操作必须在同一个监视器锁上的加锁操作之前执行。
    volatile变量规则。对volatile变量的写入操作必须在该变量的读操作之前执行。
    线程启动规则。在线程上对Thread.Start的调用必须在该线程中执行任何操作之前执行。
    线程结束规则。线程中的任何操作都必须在其他线程检测到该线程已经结束之前执行,或者从Thread.join中成功返回,或者在调用Thread.isAlive时返回false。
    中断规则。当一个线程在另一个线程上调用interrupt时,必须在被中断线程检测到interrupt调用之前执行(通过抛出InterruptedException,或者调用isInterrupted和interrupted)。
    终结器规则。对象的构造函数必须在启动该对象的终结器之前执行完成。
    传递性。如果操作A在操作B之前执行,并且操作B在操作C之前执行,那么操作A必须在操作C之前执行。

    发布

  • 不安全发布

  • 当缺少Happens-Before关系时,就可能出现重排序问题,这就解释了为什么在没有充分同步的情况下发布一个对象会导致另一个线程看到一个只被部分构造的对象。

    如果无法确保发布共享引用的操作在另一个线程加载改共享引用之前执行,那么对新对象的写入操作将与对象中各个域的写入操作重排序。

    @NotThreadSafe
    public class UnsafeLazyInitialization {
        private static Resources resource;
    
        public static Resource getInsatance() {
            if(resource == null)
                resource = new Resource(); // 不安全发布
            return resource;
        }
    }

    线程A写入resource的操作与线程B读取resource的操作之前不存在Happen-Before关系。在发布对象时存在数据竞争问题,因此B并不一定能看到Resource的正确状态。

    由于在两个线程中都没有使用同步,因此线程B看到的线程A中的操作顺序,可能与线程A执行这些操作时的顺序并不相同。因此,即使线程A初始化Resource实例之后在将resource设置为指向它,线程B仍可能看到对resource的写入操作将在对Resource各个域的写入操作之前发生。

  • 安全的初始化模式

  • @ThreadSafe
    public class SafeLazyInitialization {
        private static Resources resource;
    
        public synchronized static Resource getInsatance() {
            if(resource == null)
                resource = new Resource(); 
            return resource;
        }
    }

    静态初始化器是由JVM在类的初始化阶段执行,即在类被加载后并且被线程使用之前。由于JVM将在初始化期间获得一个锁,并且每个线程都至少获取一次这个锁以确保这个类已经加载,因此在静态初始化期间,内存写入操作将自动对所有线程可见。

    @ThreadSafe
    public class SafeLazyInitialization {
        private static Resources resource = new Resource();
    
        public synchronized static Resource getInsatance() 
            return resource;
        }
    }

    通过使用提前初始化,避免了在每次调用SafeLazyInitialization中的getInstance时所产生的同步开销。

  • 双重检查加锁

  • @NotThreadSafe
    public class DoubleCheckedLocking {
        private static Resources resource
    
        public static Resource getInsatance() 
            if(resource == null)
                synchronized (DoubleCheckedLocking.class) {
                    if(resource == null)
                        resource = new Resource(); 
                }
            }
            return resource;
        }
    }

    由于早期的JVM在性能还存在一些有待优化的地方,因此延迟初始化经常被用来避免不必要的高开销的操作,或者降低程序的启动时间。

    DCL的工作原理是,首先检查是否在没有同步的情况下需要初始化,如果resource引用不为空,那么就直接使用它。否则,就进行同步并在此检查Resource是否被初始化,从而确保只有一个线程对共享的Resource执行初始化。

    DCL的真正问题在于:当在没有同步的情况下读取一个共享对象时,可能发生的最糟糕事情只是看到一个失效值,此时DCL方法将通过在持有锁的情况下再次尝试来避免这种风险。然而,实际情况远比这种情况糟糕 —— 线程可能看到引用的当前值,但对象的状态值却是失效的,这意味着线程可以看到对象处于无效或错误的状态。

    在JVM的后续版本中,如果把resource声明为volatile类型,那么就能启用DCL,然而DCL的这种使用方法已经被广泛地废弃了。


转载请并标注: “本文转载自 linkedkeeper.com ”  ©著作权归作者所有