• Tags
  •         
  • www.breakyizhan.com
  •    

    对于单例模式,主要是有饿汉式懒汉式这两种模式,而这篇文章主要是讲

    • 饿汉式 (没有线程安全性问题)
    • 懒汉式(双重检查加锁解决线程安全性问题)

    单例模式,是一种常用的软件设计模式。在它的核心结构中只包含一个被称为单例的特殊类。通过单例模式可以保证系统中,应用该模式的类一个类只有一个实例。即一个类只有一个对象实例。现在,我们就这两种模式来举例分析线程安全性性的相关问题。

    单例饿汉式没有线程安全性的问题

    Java的并发编程:线程的安全性问题的分析 这篇文章,我们知道,线程的安全性问题要满足下面三个条件:

    • 多线程环境下
    • 多个线程共享一个资源
    • 对资源进行非原子性操作

    而对于单例饿汉式确不满足第三个条件,我们可以用下面的Java程序示例来看一下(实现是创建一个饿汉式的类,再用20个线程去调用):

    
    package com.breakyizhan.thread.t5;
    
    public class Singleton {
    	
    	// 私有化构造方法
    	private Singleton () {}
    
    	private static Singleton instance = new Singleton();
    	
    	public static Singleton getInstance() {
    		return instance;
    	}
    	
    	// 多线程的环境下
    	// 必须有共享资源
    	// 对资源进行非原子性操作
    	
    	
    }
    

    在getInstance()创建实例的方法中,没有对资源进行非原子性操作,我们所获取的对象都是一样的,可以用一个MultiThreadMain.java方法来看一下,创建20个线程去调用:

    package com.breakyizhan.thread.t5;
    
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    public class MultiThreadMain {
    	
    	public static void main(String[] args) {
    		
    		ExecutorService threadPool = Executors.newFixedThreadPool(20);
    		
    		for(int i = 0;i<20;i++) {
    			threadPool.execute(new Runnable() {
    				@Override
    				public void run() {
    					System.out.println(Thread.currentThread().getName() + ":" +Singleton.getInstance());
    				}
    			});
    		}
    		
    		threadPool.shutdown();
    		
    		
    	}
    

    结果是:

    pool-1-thread-8:com.breakyizhan.thread.t5.Singleton@39117c62
    pool-1-thread-14:com.breakyizhan.thread.t5.Singleton@39117c62
    pool-1-thread-4:com.breakyizhan.thread.t5.Singleton@39117c62
    pool-1-thread-2:com.breakyizhan.thread.t5.Singleton@39117c62
    pool-1-thread-15:com.breakyizhan.thread.t5.Singleton@39117c62
    pool-1-thread-3:com.breakyizhan.thread.t5.Singleton@39117c62
    pool-1-thread-9:com.breakyizhan.thread.t5.Singleton@39117c62
    pool-1-thread-10:com.breakyizhan.thread.t5.Singleton@39117c62
    pool-1-thread-12:com.breakyizhan.thread.t5.Singleton@39117c62
    pool-1-thread-7:com.breakyizhan.thread.t5.Singleton@39117c62
    pool-1-thread-6:com.breakyizhan.thread.t5.Singleton@39117c62
    pool-1-thread-19:com.breakyizhan.thread.t5.Singleton@39117c62
    pool-1-thread-18:com.breakyizhan.thread.t5.Singleton@39117c62
    pool-1-thread-17:com.breakyizhan.thread.t5.Singleton@39117c62
    pool-1-thread-16:com.breakyizhan.thread.t5.Singleton@39117c62
    pool-1-thread-13:com.breakyizhan.thread.t5.Singleton@39117c62
    pool-1-thread-11:com.breakyizhan.thread.t5.Singleton@39117c62
    pool-1-thread-1:com.breakyizhan.thread.t5.Singleton@39117c62
    pool-1-thread-5:com.breakyizhan.thread.t5.Singleton@39117c62
    pool-1-thread-20:com.breakyizhan.thread.t5.Singleton@39117c62
    

    可以看到,对于饿汉式的单例模式,是没有线程安全性问题的。但是饿汉式会造成对资源的浪费,比如说我没有调用这个Singleton类的时候,它已经创建好给我们了。正常情况下,我们是要用这个类才去创建的,是不是?所以,单例模式就有了懒汉式的这个模式。

    单例懒汉式的线程安全性的问题

    我们知道饿汉式没有线程安全性的问题,那么懒汉式有没有线程安全性的问题呢?那当然是有的,我们可以看一下下面这个Java示例(实现是创建一个懒汉式的类,再用20个线程去调用)

    
    package com.breakyizhan.thread.t5;
    
    public class Singleton2 {
    	
    	private Singleton2() {}
    	
    	private static Singleton2 instance;
    	
    	public static Singleton2 getInstance () {
    		if(instance == null) {
    					instance = new Singleton2(); 
    			}
    		return instance;
    	}
    
    }
    
    

    再用一个MultiThreadMain.java方法来看一下,创建20个线程去调用:

    package com.breakyizhan.thread.t5;
    
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    public class MultiThreadMain {
    	
    	public static void main(String[] args) {
    		
    		ExecutorService threadPool = Executors.newFixedThreadPool(20);
    		
    		for(int i = 0;i&lt;20;i++) {
    			threadPool.execute(new Runnable() {
    				@Override
    				public void run() {
    					System.out.println(Thread.currentThread().getName() + ":" +Singleton2.getInstance());
    				}
    			});
    		}
    		
    		threadPool.shutdown();
    		
    		
    	}
    

    结果是:

    pool-1-thread-6:com.breakyizhan.thread.t5.Singleton2@61d50c2d
    pool-1-thread-7:com.breakyizhan.thread.t5.Singleton2@61d50c2d
    pool-1-thread-13:com.breakyizhan.thread.t5.Singleton2@246f9732
    pool-1-thread-19:com.breakyizhan.thread.t5.Singleton2@246f9732
    pool-1-thread-2:com.breakyizhan.thread.t5.Singleton2@61d50c2d
    pool-1-thread-5:com.breakyizhan.thread.t5.Singleton2@61d50c2d
    pool-1-thread-15:com.breakyizhan.thread.t5.Singleton2@246f9732
    pool-1-thread-9:com.breakyizhan.thread.t5.Singleton2@61d50c2d
    pool-1-thread-12:com.breakyizhan.thread.t5.Singleton2@e9c9830
    pool-1-thread-20:com.breakyizhan.thread.t5.Singleton2@246f9732
    pool-1-thread-18:com.breakyizhan.thread.t5.Singleton2@246f9732
    pool-1-thread-16:com.breakyizhan.thread.t5.Singleton2@246f9732
    pool-1-thread-14:com.breakyizhan.thread.t5.Singleton2@246f9732
    pool-1-thread-8:com.breakyizhan.thread.t5.Singleton2@61d50c2d
    pool-1-thread-11:com.breakyizhan.thread.t5.Singleton2@c0647e4
    pool-1-thread-4:com.breakyizhan.thread.t5.Singleton2@246f9732
    pool-1-thread-3:com.breakyizhan.thread.t5.Singleton2@61d50c2d
    pool-1-thread-1:com.breakyizhan.thread.t5.Singleton2@246f9732
    pool-1-thread-10:com.breakyizhan.thread.t5.Singleton2@61d50c2d
    pool-1-thread-17:com.breakyizhan.thread.t5.Singleton2@246f9732
    

    我们可以看到,很多线程都自己创建了不少的对象,这样就造成了线程的安全性的问题。那么,我们要解决这个问题怎么办呢?很简单,就是用synchronized关键字就可以了。如下:

    package com.breakyizhan.thread.t5;
    
    public class Singleton2 {
    	
    	private Singleton2() {}
    	
    	private static Singleton2 instance;
    	
    	public static synchronized Singleton2 getInstance () {
                   //自旋 while(true)
    		if(instance == null) {
    		instance = new Singleton2(); 
    			}
    		return instance;
    	}
    
    }
    
    

    得到的结果如下:

    pool-1-thread-10:com.breakyizhan.thread.t5.Singleton2@502880f2
    pool-1-thread-12:com.breakyizhan.thread.t5.Singleton2@502880f2
    pool-1-thread-8:com.breakyizhan.thread.t5.Singleton2@502880f2
    pool-1-thread-6:com.breakyizhan.thread.t5.Singleton2@502880f2
    pool-1-thread-7:com.breakyizhan.thread.t5.Singleton2@502880f2
    pool-1-thread-3:com.breakyizhan.thread.t5.Singleton2@502880f2
    pool-1-thread-4:com.breakyizhan.thread.t5.Singleton2@502880f2
    pool-1-thread-2:com.breakyizhan.thread.t5.Singleton2@502880f2
    pool-1-thread-5:com.breakyizhan.thread.t5.Singleton2@502880f2
    pool-1-thread-1:com.breakyizhan.thread.t5.Singleton2@502880f2
    pool-1-thread-11:com.breakyizhan.thread.t5.Singleton2@502880f2
    pool-1-thread-9:com.breakyizhan.thread.t5.Singleton2@502880f2
    pool-1-thread-13:com.breakyizhan.thread.t5.Singleton2@502880f2
    pool-1-thread-14:com.breakyizhan.thread.t5.Singleton2@502880f2
    pool-1-thread-16:com.breakyizhan.thread.t5.Singleton2@502880f2
    pool-1-thread-15:com.breakyizhan.thread.t5.Singleton2@502880f2
    pool-1-thread-18:com.breakyizhan.thread.t5.Singleton2@502880f2
    pool-1-thread-17:com.breakyizhan.thread.t5.Singleton2@502880f2
    pool-1-thread-19:com.breakyizhan.thread.t5.Singleton2@502880f2
    pool-1-thread-20:com.breakyizhan.thread.t5.Singleton2@502880f2
    

    Java的懒汉式双重检查加锁

    那结合锁的概念Java的并发编程:偏向锁,轻量级锁和重量级锁,我们可以知道,有三种锁,

    • 偏向锁
    • 轻量级锁
    • 重量级锁

    由于是多线程的环境,那么偏向锁是不能用了,那么我们可以看一下轻量级锁,轻量级锁在进入代码块之后,如果有线程调用的时候,会在进入代码块的地方进行自旋,等到上一个线程执行完毕之后,才会继续执行,不过自旋很浪费CPU的资源,还不如wait,wait是不消耗CPU资源的。如果在上面的代码的话,全部都会变成重量级锁,这样子没办法保证CPU的资源。我们可以看到线程安全性的问题是出现在创建的时候instance = new Singleton2(); 那么,我们只要在出现线程安全性问题的时候加synchronized 就可以,读的操作return instance;是不存在线程的安全性问题的,所以最后优化之后的代码如下:

    
    package com.breakyizhan.thread.t5;
    
    public class Singleton2 {
    	
    	private Singleton2() {}
    	
    	private static volatile Singleton2 instance;
    	
    	/**
    	 * 双重检查加锁
    	 * 
    	 * @return
    	 */
    	public static Singleton2 getInstance () {
    		// 自旋   while(true)
    		if(instance == null) {
    			synchronized (Singleton2.class) {
    				if(instance == null) {
    					instance = new Singleton2();  // 指令重排序
    					
    					// 申请一块内存空间   // 1
    					// 在这块空间里实例化对象  // 2
    					// instance的引用指向这块空间地址   // 3
    				}
    			}
    		}
    		return instance;
    	}
    	
    	// 多线程的环境下
    	// 必须有共享资源
    	// 对资源进行非原子性操作
    
    }
    

    上面代码的volatile关键字是为了保证不会出现指令的重排序问题。

     
    转载请保留页面地址:https://www.breakyizhan.com/java/6634.html