设计模式-单例模式-指令重排思考

Posted AlaGeek

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了设计模式-单例模式-指令重排思考相关的知识,希望对你有一定的参考价值。

1、单例模式

之前写过一篇单例模式的博客,有不了解单例模式的可以看看。

2、指令重排

指令重排指的是在程序执行时,为了性能考虑,编译器和CPU可能会对指令进行重新排序,下面举个例子,比如有如下程序:

int a,b;
a = 2;
b = 2;

这个程序在执行的时候,可能执行顺序就会颠倒,变成先执行“b = 2”,再执行“a = 2”,这个就叫指令重排。
指令重排有几个基本原则,不清楚的可以看我引用的博客,这里要说的是顺序执行原则,指令重排保证在单线程内语义的串行性,举个例子:

int a,b;
a = 2;
b = a;

比如上面这个代码,如果顺序颠倒,先执行“b = a”,再执行“a = 2”,那么程序的意思就会发生改变,那么这种指令重排是不被允许的。

3、单例模式与指令重排

说完指令重排,那么说说其和单例的关系。
看到这的小伙伴想必都知道单例的饱汉模式,而饱汉模式有双重校验锁的实现方式,代码如下:

public class Singleton 
	private static Singleton singleton;
	private Singleton()
		System.out.println("生成了一个实例");
	
	public static Singleton getInstance()
		if(singleton==null)
			synchronized(Singleton.class)
				if(singleton==null)
					singleton = new Singleton();
				
			
		
		return singleton;
	

按照我所了解到的,在上述代码中,语句“singleton = new Singleton()”在程序执行时会发生指令重排,这样一个语句,实际上被分成了以下三个步骤:

  • 分配对象的内存空间
  • 初始化内存空间
  • 将对象指向该内存空间

而当指令重排的时候,三个步骤的顺序可能会变成这样:

  • 分配对象的内存空间
  • 将对象指向该内存空间
  • 初始化内存空间

那么问题就来了,假设我们现在有两个线程,A和B,当A执行到上述步骤中的第二步的时候,B执行到了第一个校验语句“if(singleton==null)”,此时对象已经指向了分配的内存空间,所以singleton不为空,那么B线程就会获得一个未经初始化的对象,从而造成程序错误。


因此需要将singleton声明为volatile类型,以此来禁止指令重排。

4、思考

昨天在写代码的时候,正好写到这个单例模式,突然间想到个问题,单例模式的双重校验锁真的会有指令重排问题吗?


按照上面的说法,B线程确实有可能会获取到未经初始化的对象,但是B线程拿这个对象做什么呢?我认为对对象的操作无非就是读写,那么就引发了另一个问题,像双重校验锁这样的写法,A线程在加了锁之后,B线程是否还能够对singleton进行操作?


于是我写了以下测试代码:

public class Main2 
    public static void main(String[] args) 
        Thread1 thread1 = new Thread1();
        thread1.start();
        Thread2 thread2 = new Thread2();
        thread2.start();
    

class Thread1 extends Thread
    @Override
    public void run() 
        Solution solution = new Solution();
        System.out.println("1:" + solution.print());
    

class Thread2 extends Thread
    @Override
    public void run() 
        Solution solution = new Solution();
        System.out.println("2:" + solution.print());
    


class Solution 
    public static Tmp tmp = null;
    public Solution()
        if(tmp == null)
            synchronized (Solution.class)
                if(tmp == null)
                    tmp = new Tmp();
                    try
                        Thread.sleep(3000);
                     catch (Exception e)
                        System.out.println(e.getMessage());
                    
                
            
        
    
    public Tmp print()
        return tmp;
    

@Data
class Tmp
    private String string;
    public Tmp()
        string = "hello";
    

我在加锁的代码里加了个3秒的等待时间,然后启动两个线程去获取tmp对象并输出,在多次测试中,我发现,当A线程在执行下列代码的时候,B线程要输出tmp对象需要等待A线程先执行完,将锁释放:

synchronized (Solution.class)
    if(tmp == null)
        tmp = new Tmp();
        try
            Thread.sleep(3000);
         catch (Exception e)
            System.out.println(e.getMessage());
        
    

为了更加明显的看出这个问题,我对修改了下代码:

public Solution()
    if(tmp == null)
        synchronized (Solution.class)
            if(tmp == null)
                tmp = new Tmp();
                try
                    System.out.println("锁内等待开始");
                    Thread.sleep(3000);
                    System.out.println("锁内等待结束");
                 catch (Exception e)
                    System.out.println(e.getMessage());
                
            
        
        try
            System.out.println("锁外等待开始");
            Thread.sleep(3000);
            System.out.println("锁外等待结束");
         catch (Exception e)
            System.out.println(e.getMessage());
        
    

emmmm,最后的测试结果推翻了我上面的结论…双重校验锁确实有指令重排问题!


其实昨天打算写这篇文章的时候,是打着推翻权威的心思的,不过今天写的时候,写着写着就觉得权威果然是权威,写博客还是有点用的,可以让自己理清思路,不愧是我!

以上是关于设计模式-单例模式-指令重排思考的主要内容,如果未能解决你的问题,请参考以下文章

从单例模式的Double-Check看指令重排

学了 JMM 指令重排序,让我明白该如何写单例模式了

单例设计模式和Java内存模型

你的单例模式真的安全吗?双重检索模式和指令重排序助你高垒单例安全防御

单例陷阱——双重检查锁中的指令重排问题

单例模式双重锁为什么要volitie修饰: