Java Synchronized keyword

Java Synchronized keyword

java-memory-architecture 本文是关于Synchronized的可见性问题。网上几乎所有的资料关于Synchronized的,有两个。一是多线程代码同步锁,一个是可见性,Synchronized保证了出去的线程把变量同步到主内存区,进来的线程用的是主内存同步的变量。

问题在后一个,表面意思好像是,Synchronized自己是道门。线程出去的时候,把线程自己内存区块的变量上交,线程进来的时候按主内存更新自己内存区块的变量。表面上是这个意思,其实本质上不是这样的。老线程出Synchronized之前,主内存更新变量就已经发生,新线程进Synchronized之前,自己内存分区的内存跟新就已经发生。是后台JVM的机制,不是Synchronized自己的机制。

代码演示

public class Singleton {  
    public static Singleton instance;

    public static int i = 0;

    public static Singleton getInstance() {
        if (instance == null)              
        {                                  
            if (Thread.currentThread().getName().equals("thread 2")) {
                while (instance == null) {
                    System.out.println("null");
                    System.out.println(i);
                }
                System.out.println("not null");
                System.out.println(i);
            }

            synchronized (Singleton.class) {  
                if (instance == null) {       
                    try {
                        Thread.sleep(50);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    instance = new Singleton();  
                    i = 1;
                    System.out.println(Thread.currentThread().getName() + " initialized------------------------");
                }
            }
        }
        return instance;
    }

    public static void main(String[] args) {
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                Singleton.getInstance();
            }
        }, "thread 1");

        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                Singleton.getInstance();
            }
        }, "thread 2");
        thread1.start();
        thread2.start();

    }

试验多次,结果不同的频率非常高。

输出结果1

...
null  
0  
null  
0  
null  
1  
thread 1 initialized------------------------  
not null  
1

Process finished with exit code 0  

输出结果2

...
null  
0  
null  
0  
null  
0  
thread 1 initialized------------------------  
not null  
1

Process finished with exit code 0  

输出结果3

...
null  
0  
null  
0  
null  
thread 1 initialized------------------------  
1  
not null  
1  
Process finished with exit code 0  

两个线程,线程1负责,初始化单例实例,线程2负责不停地打印共享变量的值。结果发现,可能两个线程的共享变量同步发生在Synchronized中变量被赋值后的一瞬间。(目前,没找到官方的具体文档。)所以没有volatile的double-check(双重检查)的问题不是由可见性造成的,而是重新排序造成。(双重检查的细节,自行查找)volatile在double-check中起的作用不是可见性,而是保持volatile修饰的变量不被重新排序。

  • 可见性造成的问题是,(可见性就是共享变量更新及时不及时,变量是否对其他线程可见。)可见性造成的问题是,由于共享变量不可见,同步不及时,第二个线程进入Synchronized之后,发现instance仍然为null,而重复进行初始化。 目前发现Synchronized可以做到保证块内变量是同步过的最新状态。 原因,示例代码可见。
  • 重排序造成的问题是,第二个线程第一次判断instance是否为null时,发现已经不为null了,getInstance方法直接返回并开始调用。但其实instance指向的内存空间还没有实例,还未初始化完成。 重排序是个什么问题,重排序是JVM为提高性能,采取的对没有前后依赖的字节码(不是)的执行顺调整,以到达程序优化,比如 a=1;b=2;虚拟机分析两段程序没有依赖性,便认为不会影响执行结果,所以就可以调整执行顺序。new动作本身还可以分解,而且JVM认为可以对其调整顺序,单线程不影响结果。new动作可以被分解为1、在堆内存开辟一块空间,并放入初始化实例。2、在栈内存开辟一块空间并将其指向堆内存的一个地址(就是示例地址)。这是正常顺序,判断当instance不为空时,这个实例确实被初始化了,可以正常使用。但是,JVM为优化程序可以将12调整顺序,反过来,JVM认为两者没有依赖关系。先在栈内开辟一块内存,指向堆内一个地址(即将初始化实例的地址),然后在堆内一块空间实例化。如果第一步完成后,第二个线程开始判断instance是否为空,这时发现不为空了,于是直接返回,然后调用发现出问题了。 volatile关键字保证其修饰的变量不会被调整顺序。之前的有可能调整,之后的也有可能调整,只是该变量不会被调整顺序。所以解决了重排列造成的问题。

References