Java单例设计模式——饿汉式和懒汉式

Java单例设计模式中分为饿汉式和懒汉式,饿汉式不存在线程安全性问题,但这种方法没有实现lazy loading(懒加载)的效果。而懒汉式存在线程安全性问题,这种方法实现了lazy loading(懒加载)的效果

1、饿汉式(不存在线程安全性问题)

package cn.itcats.thread.safe.singleton;
/**
 * 单例模式中饿汉模式
 * @author fatah
 */
public class EagerSingleton {
	private static EagerSingleton instance = new EagerSingleton();
	
	//1、私有构造方法、不让外界new
	private EagerSingleton() {}
	//2、提供对外的对象访问方法
	public static EagerSingleton getInstance() {
		//原子性操作,则不出现线程安全性问题
		return instance;
	}
}

饿汉式直接在运行这个类的时候进行一次加载,之后直接访问。显然,这种方法没有实现lazy loading(懒加载)的效果,若后期不使用此对象,就会出现资源浪费的情况。

 

2、懒汉式(懒加载策略,存在非原子性操作,存在线程安全性问题)

package cn.itcats.thread.safe.singleton;

/**
 * 单例设计模式之懒汉式
 * @author fatah
 */
public class LazySingleton {
	//1、私有的构造方法
	private LazySingleton() {}
	
	//相比饿汉式,懒汉不去new创建实例
	private static LazySingleton instance;
	
	//2、提供对外的对象访问
	public static LazySingleton getInstance() {
		//非原子性操作
		if(instance == null)
			instance = new LazySingleton();
		return instance;
	}
}

对于懒汉式存在的线程安全性问题解决思路:

1、测试证明懒汉模式存在线程安全性问题,通过构建多线程环境,打印instance,若结果不同则表示创建了多个实例,并非单例

package cn.itcats.thread.safe.Test1;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import cn.itcats.thread.safe.singleton.LazySingleton;


public class LazyTest {
	public static void main(String[] args) {
		ExecutorService pool = Executors.newFixedThreadPool(20);
		
		//启动20个线程执行geyInstance()
		for(int i = 0; i < 20 ;i ++) {
			pool.execute(new Runnable() {
				public void run() {
					LazySingleton instance = LazySingleton.getInstance();
					System.out.println(instance);
				}
			});
		}
		pool.shutdown();
	}
}

 

运行结果:

cn.itcats.thread.safe.singleton.LazySingleton@7cb4e65d
cn.itcats.thread.safe.singleton.LazySingleton@7cb4e65d
cn.itcats.thread.safe.singleton.LazySingleton@7cb4e65d
cn.itcats.thread.safe.singleton.LazySingleton@7cb4e65d
cn.itcats.thread.safe.singleton.LazySingleton@7cb4e65d
cn.itcats.thread.safe.singleton.LazySingleton@7cb4e65d
cn.itcats.thread.safe.singleton.LazySingleton@33df8ddd
cn.itcats.thread.safe.singleton.LazySingleton@33df8ddd
cn.itcats.thread.safe.singleton.LazySingleton@7cb4e65d
cn.itcats.thread.safe.singleton.LazySingleton@7cb4e65d
cn.itcats.thread.safe.singleton.LazySingleton@7cb4e65d
cn.itcats.thread.safe.singleton.LazySingleton@7cb4e65d
cn.itcats.thread.safe.singleton.LazySingleton@7cb4e65d
cn.itcats.thread.safe.singleton.LazySingleton@7cb4e65d
cn.itcats.thread.safe.singleton.LazySingleton@7cb4e65d
cn.itcats.thread.safe.singleton.LazySingleton@7cb4e65d
cn.itcats.thread.safe.singleton.LazySingleton@7cb4e65d
cn.itcats.thread.safe.singleton.LazySingleton@7cb4e65d
cn.itcats.thread.safe.singleton.LazySingleton@7cb4e65d
cn.itcats.thread.safe.singleton.LazySingleton@7cb4e65d

通过结果发现创建了多个实例,于是我们从懒汉模式方法代码根源进行分析:

                //非原子性操作
		if(instance == null)
			instance = new LazySingleton();
		return instance;

假设线程A进入方法,当走到判断instance == null时候,A线程时间片用完了,此时A线程记录了这一次最后执行的位置,B线程获得cpu时间片,执行线程:B线程判断instance = null,后new了实例,此时堆中存在了一个instance实例,后A又获得了cpu所给的时间片,接着往下执行,此时又new了一个instance实例...以此出现了上述多个实例的结果,本质就是因为这段代码是非原子性操作。

2、于是我们考虑给方法加synchronized

public static synchronized LazySingleton getInstance() {
		//非原子性操作
		if(instance == null)
			instance = new LazySingleton();
		return instance;
	}

再次打印instance,看似问题解决了,但是还是有很多问题。

我们是在多个线程下执行,jdk1.6后引入了偏向锁,那么我们这个环境适用吗?显然是不适用的,偏向锁适用于只有一个线程访问,不存在多线程争用的情况。关于《Java锁分类》《synchronized原理和执行流程》这里有一篇详细的介绍,但这种情况下,显然不是。此时发生竞争,撤销偏向锁,偏向锁会升级成轻量级锁。结合代码,轻量级锁中,A线程执行代码,B线程会检查对象头标记,此时A线程获得锁,B线程则处于自旋状态,若代码体内容少,A线程执行完毕后,B线程自旋也相应结束,B线程获取锁进入方法执行,其他线程自旋,自旋过程相当消耗cpu资源(相当于while(true){}  ),所有当线程较多情况下,轻量级锁也并不能提升性能,反而可能导致性能下降。所有,我们在方法上修饰synchronized关键字,锁被升级为了重量级锁,效率更无法保证了。为了提高程序的性能,考虑缩小synchronized锁的范围(降低锁的力度)——同步代码块。

3、再次分析,在判断if(instance == null)时进行读操作,而在new时候进行了写操作,当判断instance == null后创建实例就有可能出现安全性问题,所以在有可能出现安全性问题位置加synchronized代码块。

public static LazySingleton getInstance() {
		if(instance == null){                                        //1
			synchronized (LazySingleton.class) {                    //2
				if(instance == null)                                //3
					instance = new LazySingleton();                 //4
			}
                }
		return instance;
	}

为了保证不出问题,采用双重检查加锁策略,

  • 如果检查第一个singleton不为null,则不需要执行下面的加锁动作,极大提高了程序的性能;
  • 如果第一个singleton为null,即使有多个线程同一时间判断,但是由于synchronized的存在,只会有一个线程能够创建对象;
  • 当第一个获取锁的线程创建完成后singleton对象后,其他的在第二次判断singleton一定不会为null,则直接返回已经创建好的singleton对象;

即进行二次判断,代码到这里还存在问题吗?看似没有问题,实际上它仍不能保证完全的线程安全。在jvm中为了提高字节码指令执行的性能,采用了指令重排序——在不影响程序最终结果的情况下,对指令进行了重排序,通俗来说就是代码编译后生成的字节码指令不一定按代码的行号顺序执行

实际上对象创建的过程并不是一条字节码就完成了。关于对象创建的过程,可以参考《Java虚拟机jvm之对象的创建过程》

简单来说对象创建可分为这么几步
1、申请一块内存空间

2、在这块空间实例化对象

3、instance引用指向这块空间地址

在字节码指令中有可能先执行了步骤3,后执行了步骤2。此时instance引用已经指向了申请后的内存空间,instance此时不为null。所以指令重排序在获取单例对象时可能会出现安全问题。为了避免指令重排序,使用volatile关键字可以减少虚拟机优化,这样就不会出现线程安全性问题了。最终的代码为:

package cn.itcats.thread.safe.singleton;

/**
 * 单例设计模式之懒汉式
 * @author fatah
 */
public class LazySingleton {
	//1、私有的构造方法
	private LazySingleton() {}
	
	//相比饿汉式,懒汉不去new创建实例
        //使用volatile关键字减少虚拟机优化,避免重排序
	private static volatile LazySingleton instance;
	
	//2、提供对外的对象访问
	public static LazySingleton getInstance() {
		if(instance == null){                            //1
			synchronized (LazySingleton.class) {        //2
				if(instance == null)                    //3
					instance = new LazySingleton();     //4
			}
                }
		return instance;
	}
}

 

你可能感兴趣的:(Java多线程)