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

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

    总结

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

    参考文档

  • 相关阅读:
    C#Ref和Out作用于引用对象时的理解
    Docker-.Net Core部署
    微服务-基于Grpc的进程通信-Protobuf-net.Grpc(4-3)
    微服务-基于Grpc的进程通信-Grpc服务注册与发现 (4-2)
    微服务-基于Grpc的进程通信-简单使用(4-1)
    ELK-Windows下系统安装(1)
    Visual Studio 2019中如何使用Git
    VS2019下载速度快完成时突然变得极慢
    Swagger 发布服务器导出Excel失败,本地导出没问题
    发布到服务器后导出Excel失败
  • 原文地址:https://www.cnblogs.com/leesf456/p/7413781.html
Copyright © 2011-2022 走看看