zoukankan      html  css  js  c++  java
  • 【项目实战】多线程环境下正确创建单例

    前言

    对项目代码进行扫描时,出现静态扫描严重问题,发现是由于多线程环境下没有正确创建单例所导致。

    问题分析

    本项目使用的JDK 1.7+

    项目代码如下(修改了类名,但核心没变)

    static class Singleton {
    	private static volatile Singleton cache = null;
    	private static Object mutexObj = new Object();
    		
    	private Singleton() {
    		
    	}
    		
    	public static Singleton getInstance() {
    		Singleton tmp = cache;
    	    if (tmp == null) {
    			synchronized (mutexObj) {
    				if (tmp == null) {	                	
    					tmp = new Singleton();
    					cache = tmp;  
    				}	              
    			}
    		}
    		return tmp;
    	}
    }
    

    按照项目生成单例代码,使用如下测试类进行测试

    
    public class Test {
    	public static void main(String[] args) {	
    		for (int i = 0; i < 3; i++) {
    			Thread thread = new Thread(new Runnable() {
    				public void run() {
    					System.out.println(Thread.currentThread().getName() + " " + Singleton.getInstance().toString());
    				}
    				
    			});
    			thread.setName("Thread" + i);
    			thread.start();
    		}
    		
    	}
    
    	static class Singleton {
    		private static volatile Singleton cache = null;
    		private static Object mutexObj = new Object();
    		
    		private Singleton() {
    		
    		}
    		
    		public static Singleton getInstance() {
    			Singleton tmp = cache;
    	    	if (tmp == null) {
    				synchronized (mutexObj) {
    					if (tmp == null) {	                	
    						tmp = new Singleton();
    						cache = tmp;  
    					}	              
    				}
    			}
    			return tmp;
    		}
    	}
    }
    
    
    

    输出结果如下:

    Thread1 com.hust.grid.leesf.mvnlearning.Test$Singleton@304e94a4
    Thread0 com.hust.grid.leesf.mvnlearning.Test$Singleton@304e94a4
    Thread2 com.hust.grid.leesf.mvnlearning.Test$Singleton@304e94a4

    从结果看,都生成了同一个实例,似乎不存在问题,多线程环境下确实不太好重现问题,现改动代码如下:

    
    static class Singleton {
    	private static volatile Singleton cache = null;
    	private static Object mutexObj = new Object();
    		
    	private Singleton() {
    		
    	}
    		
    	public static Singleton getInstance() {
    		Singleton tmp = cache;
    	    if (tmp == null) {
    	        System.out.println(Thread.currentThread().getName() + " in outer if block");
    	        synchronized (mutexObj) {
    				System.out.println(Thread.currentThread().getName() + " in synchronized block");
    	            if (tmp == null) {	 
    	               	System.out.println(Thread.currentThread().getName() + " in inner if block");
    	               	tmp = new Singleton();
    	                cache = tmp;  
    	            }	
    	            System.out.println(Thread.currentThread().getName() + " out inner if block");
    			}
    	        System.out.println(Thread.currentThread().getName() + " out synchronized block");
    	    }
    	    System.out.println(Thread.currentThread().getName() + " out outer if block");
    	    return cache;
    	}
    }
    
    
    

    上述代码中添加了Thread.sleep(1)这条语句,其中,Thread.sleep(1)进行休眠时,线程不会释放拥有的锁,并且打印了相关的语句,便于查看线程正运行在哪里的状态。

    再次测试,输出结果如下:

    Thread2 in outer if block
    Thread1 in outer if block
    Thread0 in outer if block
    Thread2 in synchronized block
    Thread2 in inner if block
    Thread2 out inner if block
    Thread2 out synchronized block
    Thread0 in synchronized block
    Thread2 out outer if block
    Thread2 com.hust.grid.leesf.mvnlearning.Test$Singleton@60b07af1
    Thread0 in inner if block
    Thread0 out inner if block
    Thread0 out synchronized block
    Thread1 in synchronized block
    Thread0 out outer if block
    Thread1 in inner if block
    Thread0 com.hust.grid.leesf.mvnlearning.Test$Singleton@625795ce
    Thread1 out inner if block
    Thread1 out synchronized block
    Thread1 out outer if block
    Thread1 com.hust.grid.leesf.mvnlearning.Test$Singleton@642c39d2

    从结果看,生成了3个不同的实例,并且每个线程都执行了完整的流程,并且可知单例的创建存在问题。在分析原因前简单了解下多线程模型,多线程模型如下:

    多线程内存模型

    每个线程有自己独立的工作空间,线程间进行通信是通过主内存完成的,想了解详细内容可参见如下链接:内存模型深入理解java内存模型

    知道每个线程会有一份tmp拷贝后,配合打印输出,就不难分析出原因。

    问题解决

    按照《Effective Java》一书中创建单例的推荐,可使用如下两种解决方法

    双重锁检查机制

    需要配合volatile关键字使用,并且需要JDK版本在1.5以上,核心代码如下

    
    static class Singleton {
    	private static volatile Singleton cache = null;
    	private static Object mutexObj = new Object();
    		
    	private Singleton() {
    		
    	}
    		
    	public static Singleton getInstance() {
    		Singleton tmp = cache;
    	    if (tmp == null) {
    			tmp = cache;
    			synchronized (mutexObj) {
    				if (tmp == null) {	                	
    					tmp = new Singleton();
    					cache = tmp;  
    				}	              
    			}
    		}
    		return tmp;
    	}
    }
    
    

    进行如下测试(添加打印语句方便分析):

    
    public class Test {
    	public static void main(String[] args) {	
    		for (int i = 0; i < 3; i++) {
    			Thread thread = new Thread(new Runnable() {
    				public void run() {
    					System.out.println(Thread.currentThread().getName() + " " + Singleton.getInstance().toString());
    				}
    				
    			});
    			thread.setName("Thread" + i);
    			thread.start();
    		}
    		
    	}
    	
    	static class Singleton {
    		private static volatile Singleton cache = null;
    		private static Object mutexObj = new Object();
    		
    		private Singleton() {
    		
    		}
    		
    		public static Singleton getInstance() {
    			Singleton tmp = cache;
    	        if (tmp == null) {
    	        	System.out.println(Thread.currentThread().getName() + " in outer if block");
    	            synchronized (mutexObj) {
    	            	System.out.println(Thread.currentThread().getName() + " in synchronized block");
    	            	tmp = cache;
    	                if (tmp == null) {	 
    	                	System.out.println(Thread.currentThread().getName() + " in inner if block");
    	                	tmp = new Singleton();
    	                	try {
    	                		Thread.sleep(1);
    	                	} catch (InterruptedException e) {
    	                		e.printStackTrace();
    	                	}
    	                    cache = tmp;  
    	                }	
    	                System.out.println(Thread.currentThread().getName() + " out inner if block");
    	            }
    	            System.out.println(Thread.currentThread().getName() + " out synchronized block");
    	        }
    	        System.out.println(Thread.currentThread().getName() + " out outer if block");
    	        return tmp;
    		}
    	}
    }
    
    
    

    输出结果如下:

    Thread0 in outer if block
    Thread0 in synchronized block
    Thread0 in inner if block
    Thread2 in outer if block
    Thread1 in outer if block
    Thread0 out inner if block
    Thread0 out synchronized block
    Thread1 in synchronized block
    Thread1 out inner if block
    Thread1 out synchronized block
    Thread1 out outer if block
    Thread0 out outer if block
    Thread1 com.hust.grid.leesf.mvnlearning.Test$Singleton@13883d5f
    Thread0 com.hust.grid.leesf.mvnlearning.Test$Singleton@13883d5f
    Thread2 in synchronized block
    Thread2 out inner if block
    Thread2 out synchronized block
    Thread2 out outer if block
    Thread2 com.hust.grid.leesf.mvnlearning.Test$Singleton@13883d5f

    从结果中和线程运行步骤可以看到三个线程并发的情况下,只生成了唯一实例。

    静态内部类

    JDK版本限制,也不需要使用volatile关键字即可完成单例模式,核心代码如下:

    
    static class Singleton {
    
    	private Singleton() {
    		
    	}
    		
    	private static class InstanceHolder {
            public static Singleton instance = new Singleton();
        }
    	
        public static Singleton getInstance() {
    	    return InstanceHolder.instance;  
    	}
    }
    
    

    进行如下测试:

    
    public class Test {
    	public static void main(String[] args) {	
    		for (int i = 0; i < 3; i++) {
    			Thread thread = new Thread(new Runnable() {
    				public void run() {
    					System.out.println(Thread.currentThread().getName() + " " + Singleton.getInstance().toString());
    				}
    				
    			});
    			thread.setName("Thread" + i);
    			thread.start();
    		}
    		
    	}
    	
    	static class Singleton {
    		
    		private Singleton() {
    		
    		}
    		
    		private static class InstanceHolder {
    	        public static Singleton instance = new Singleton();
    	    }
    
    	    public static Singleton getInstance() {
    	        return InstanceHolder.instance;  
    	    }
    	}
    }
    
    

    运行结果如下:

    Thread2 com.hust.grid.leesf.mvnlearning.Test$Singleton@71f801f7
    Thread1 com.hust.grid.leesf.mvnlearning.Test$Singleton@71f801f7
    Thread0 com.hust.grid.leesf.mvnlearning.Test$Singleton@71f801f7

    该模式可保证使用时才会初始化变量,达到延迟初始化目的。

    总结

    单例模式在多线程环境下不太好编写,并且不容易重现异常,编写时需要谨慎,在项目中遇到问题也需要多总结和记录。

    参考文档

  • 相关阅读:
    关于字符串转义的代码
    JAVA发布aar文件
    apache虚拟主机配置HTTPS
    font-face跨域办法
    Javascript数组方法(译)
    Python动态生成变量
    给AOP的after函数使用原函数局部变量
    stopImmediatePropagation的应用
    IE9或以上的浏览器flash值为空时,导致domready不触发
    html写法对gzip压缩率的影响
  • 原文地址:https://www.cnblogs.com/leesf456/p/7413781.html
Copyright © 2011-2022 走看看