如何保证单例模式在多线程中的线程安全性

Posted whymoney1000

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了如何保证单例模式在多线程中的线程安全性相关的知识,希望对你有一定的参考价值。

   如何保证单例模式在多线程中的线程安全性

        对大数据、分布式、高并发等知识的学习必须要有多线程的基础。这里讨论一下如何在多线程的情况下设计单例模式。在23中设计模式中单例模式是比较常见的,在非多线程的情况下写单例模式,考虑的东西会很少,但是如果将多线程和单例模式结合起来,考虑的事情就变多了,如果使用不当(特别是在生成环境中)就会造成严重的后果。所以如何使单例模式在多线程中是安全的显得尤为重要,下面介绍各个方式的优缺点以及可用性:

       1.立即加载(饿汉模式)

        立即加载模式就是在调用getInstance()方法前,实例就被创建了,例:

public class MyObject
 // 立即加载方式  ==饿汉模式
private static MyObject myObject=new MyObject();
private MyObject()

public static MyObject getInstance()
return myObject;

-------------------------------------------------------------------

public class MyThread extends Thread
public void run()
System.out.println(MyObject.getInstance().hashCode());

------------------------------------------------------------------

public class Run
   public static void main(String[] args)
 MyThread t1=new MyThread();
 MyThread t2=new MyThread();
 MyThread t3=new MyThread();
 t1.start();
 t2.start();
 t3.start();

     控制台打印:

714682869
714682869
714682869

    控制台打印出3个相同的hashCode,说明只有一个对象,这就是立即加载的单例模式。但是这种模式有一个缺点,就是不能有其他的实例变量,因为getInstance()方法没有同步,所以可能出现非线程安全问题。

   2.延迟加载(懒汉模式)

    延迟加载就是在getInstance()方法中创建实例,例:

     public class MyObject
private static MyObject myObject;
private MyObject()
     
 public static MyObject getInstance()
// 延迟加载
 if(myObject!=null)  
else
myObject=new MyObject();
 
return myObject;
     

-------------------------------------------------------------------

public class MyThread extends Thread
public void run()
System.out.println(MyObject.getInstance().hashCode());

-------------------------------------------------------------------

public class Run
public static void main(String[] args)
MyThread t1=new MyThread();
t1.start();

     控制台打印:

1701381926

    控制台打印出一个实例。缺点:在多线程的环境中,就会出现取多个实例的情况,与单例模式的初衷相背离。所以在多线程的环境中,此实例代码是错误的。

    3.延迟加载中使用synchronized修饰方法

    public class MyObject
private static MyObject myObject;
private MyObject()

synchronized public static MyObject getInstance()
try
if(myObject!=null)
else
Thread.sleep(3000);
myObject=new MyObject();

catch (InterruptedException e)
// TODO: handle exception
e.printStackTrace();

return myObject;

 -------------------------------------------------------------------

public class MyThread extends Thread
public void run()
System.out.println(MyObject.getInstance().hashCode());

      

-------------------------------------------------------------------

public class Run
public static void main(String[] args)
MyThread t1=new MyThread();
MyThread t2=new MyThread();
MyThread t3=new MyThread();
t1.start();
t2.start();
t3.start();

     控制台打印:

 1069480624
1069480624
1069480624

     虽然得到了相同的实例,但是我们知道synchronized是同步的,一个线程必须等待另一个线程释放锁之后才能执行,影响了效率。

      4.延迟加载中使用同步代码块,对类加锁

        public class MyObject
private static MyObject myObject;
private MyObject()

public static MyObject getInstance()
try
synchronized(MyObject.class)
if(myObject!=null)
else
Thread.sleep(3000);
myObject=new MyObject();


catch (InterruptedException e)
// TODO: handle exception
e.printStackTrace();

return myObject;

 -------------------------------------------------------------------

public class MyThread extends Thread
public void run()
System.out.println(MyObject.getInstance().hashCode());

 -------------------------------------------------------------------

public class Run
public static void main(String[] args)
MyThread t1=new MyThread();
MyThread t2=new MyThread();
MyThread t3=new MyThread();
t1.start();
t2.start();
t3.start();

       控制台打印:

1743911840
1743911840
1743911840

       此代码虽然是正确的,但getInstance()方法里的代码都是同步的了,其实也和第三种方式一样会降低效率

       5.使用DCL双检查锁机制

        DCL双检查锁机制即使用volatile关键字(使变量在多个线程中可见)修改对象和synchronized代码块

       public class MyObject
    private volatile static MyObject myObject;
    private MyObject()
   
    public static MyObject getInstance()
    try
if(myObject!=null)
else
Thread.sleep(3000);
synchronized(MyObject.class)
if(myObject==null)
myObject=new MyObject();



catch (InterruptedException e)
e.printStackTrace();
// TODO: handle exception

    return myObject;
   

 -------------------------------------------------------------------

public class MyThread extends Thread
public void run()
System.out.println(MyObject.getInstance().hashCode());

    

 -------------------------------------------------------------------

public class Run
public static void main(String[] args)
MyThread t1=new MyThread();
MyThread t2=new MyThread();
MyThread t3=new MyThread();
t1.start();
t2.start();
t3.start();

     控制台打印:

798941612
798941612
798941612

      使用DCL双检查锁机制,成功解决了延迟加载模式中遇到的多线程问题,实现了线程安全。其实大多数多线程结合单例模式情况下使用DCL是一种好的解决方案。

       6.使用静态内置类实现单例模式

       public class MyObject
// 内部类方式
private static class MyObjectHandler
private static MyObject myObject=new MyObject();

private MyObject()


public static MyObject getInstance()
return MyObjectHandler.myObject;

-------------------------------------------------------------------

public class MyThread extends Thread
public void run()
System.out.println(MyObject.getInstance().hashCode());

 -------------------------------------------------------------------

public class Run
public static void main(String[] args)
MyThread t1=new MyThread();
MyThread t2=new MyThread();
MyThread t3=new MyThread();
t1.start();
t2.start();
t3.start();


    控制台打印:

1743911840
1743911840
1743911840

         使用静态内置类可以解决多线程中单例模式的非线程安全的问题,实现线程安全,但是如果对象是序列化的就无法达到效果了。

       7.序列化与反序列化的单例模式

 需要readResolve方法

        public class MyObject implements Serializable
private static final long serialVersionUID=888L;
// 内部类
private static class MyObjectHandler
private static final MyObject myObject=new MyObject();

private MyObject()


public static MyObject getInstance()
return MyObjectHandler.myObject;

 protected Object readResolve() throws ObjectStreamException
 System.out.println("调用了readResolve方法");
return MyObjectHandler.myObject;
 

-------------------------------------------------------------------

public class SaveAndRead
public static void main(String[] args)
try
MyObject myObject=MyObject.getInstance();
FileOutputStream fosRef=new FileOutputStream(new File("myObjectFile.txt"));
ObjectOutputStream oosRef=new ObjectOutputStream(fosRef);
oosRef.writeObject(myObject);
oosRef.close();
fosRef.close();
System.out.println(myObject.hashCode());
catch (FileNotFoundException e)
// TODO: handle exception
catch(IOException e)
e.printStackTrace();

try
FileInputStream fisRef=new FileInputStream(new File("myObjectFile.txt"));
ObjectInputStream iosRef=new ObjectInputStream(fisRef);
MyObject myObject=(MyObject) iosRef.readObject();
iosRef.close();
fisRef.close();
System.out.println(myObject.hashCode());
catch (FileNotFoundException e)
// TODO: handle exception
catch(IOException e)
e.printStackTrace();
catch(ClassNotFoundException e)
e.printStackTrace();


  

      控制台打印:

 1988716027
调用了readResolve方法
1988716027

      调用了readResolve方法后就是单例了,如果我们注释掉readResolve方法,

      控制台打印:

977199748
536468534

       8.使用static代码块实现单例模式

        public class MyObject
private static MyObject instance=null;
private MyObject()


static
instance=new MyObject();

public static MyObject getInstance()
return instance;

-------------------------------------------------------------------

public class MyThread extends Thread

public void run()
for (int i = 0; i <5; i++)
System.out.println(MyObject.getInstance().hashCode());


-------------------------------------------------------------------

public class Run
public static void main(String[] args)
MyThread t1=new MyThread();
MyThread t2=new MyThread();
MyThread t3=new MyThread();
t1.start();
t2.start();
t3.start();


   控制台打印:

798941612
798941612
798941612

 

 

https://blog.csdn.net/gan785160627/article/details/81946242

应用单例模式时,类只能有一个对象实例,这么做的目的是避免不一致状态。

饿汉式单例:(立即加载)

  1.  
    // 饿汉式单例
  2.  
    public class Singleton1
  3.  
     
  4.  
    // 指向自己实例的私有静态引用,主动创建
  5.  
    private static Singleton1 singleton1 = new Singleton1();
  6.  
     
  7.  
    // 私有的构造方法
  8.  
    private Singleton1()
  9.  
     
  10.  
    // 以自己实例为返回值的静态的公有方法,静态工厂方法
  11.  
    public static Singleton1 getSingleton1()
  12.  
    return singleton1;
  13.  
  14.  

懒汉式单例:(延迟加载)

  1.  
    // 懒汉式单例
  2.  
    public class Singleton2
  3.  
     
  4.  
    // 指向自己实例的私有静态引用
  5.  
    private static Singleton2 singleton2;
  6.  
     
  7.  
    // 私有的构造方法
  8.  
    private Singleton2()
  9.  
     
  10.  
    // 以自己实例为返回值的静态的公有方法,静态工厂方法
  11.  
    public static Singleton2 getSingleton2()
  12.  
    // 被动创建,在真正需要使用时才去创建
  13.  
    if (singleton2 == null)
  14.  
    singleton2 = new Singleton2();
  15.  
  16.  
    return singleton2;
  17.  
  18.  

 

多线程下线程安全的懒汉式单例(饿汉式本身是线程安全的):

1)、同步延迟加载 — synchronized方法

  1.  
    // 线程安全的懒汉式单例
  2.  
    public class Singleton2
  3.  
     
  4.  
    private static Singleton2 singleton2;
  5.  
     
  6.  
    private Singleton2()
  7.  
     
  8.  
    // 使用 synchronized 修饰,临界资源的同步互斥访问
  9.  
    public static synchronized Singleton2 getSingleton2()
  10.  
    if (singleton2 == null)
  11.  
    singleton2 = new Singleton2();
  12.  
  13.  
    return singleton2;
  14.  
  15.  

 

2)、同步延迟加载 — synchronized块

  1.  
    // 线程安全的懒汉式单例
  2.  
    public class Singleton2
  3.  
     
  4.  
    private static Singleton2 singleton2;
  5.  
     
  6.  
    private Singleton2()
  7.  
     
  8.  
     
  9.  
    public static Singleton2 getSingleton2()
  10.  
    synchronized(Singleton2.class) // 使用 synchronized 块,临界资源的同步互斥访问
  11.  
    if (singleton2 == null)
  12.  
    singleton2 = new Singleton2();
  13.  
  14.  
  15.  
    return singleton2;
  16.  
  17.  

3)、同步延迟加载 — 使用内部类实现延迟加载

  1.  
    // 线程安全的懒汉式单例
  2.  
    public class Singleton5
  3.  
     
  4.  
    // 私有内部类,按需加载,用时加载,也就是延迟加载
  5.  
    private static class Holder
  6.  
    private static Singleton5 singleton5 = new Singleton5();
  7.  
  8.  
     
  9.  
    private Singleton5()
  10.  
     
  11.  
  12.  
     
  13.  
    public static Singleton5 getSingleton5()
  14.  
    return Holder.singleton5;
  15.  
  16.  

 

4)双重检测

  1.  
    // 线程安全的懒汉式单例
  2.  
    public class Singleton3
  3.  
     
  4.  
    //使用volatile关键字防止重排序,因为 new Instance()是一个非原子操作,可能创建一个不完整的实例
  5.  
    private static volatile Singleton3 singleton3;
  6.  
     
  7.  
    private Singleton3()
  8.  
  9.  
     
  10.  
    public static Singleton3 getSingleton3()
  11.  
    // Double-Check idiom
  12.  
    if (singleton3 == null)
  13.  
    synchronized (Singleton3.class) // 1
  14.  
    // 只需在第一次创建实例时才同步
  15.  
    if (singleton3 == null) // 2
  16.  
    singleton3 = new Singleton3(); // 3
  17.  
  18.  
  19.  
  20.  
    return singleton3;
  21.  
  22.  

 

5)ThreadLocal

  1.  
    public class Singleton
  2.  
     
  3.  
    // ThreadLocal 线程局部变量,将单例instance线程私有化
  4.  
    private static ThreadLocal<Singleton> threadlocal = new ThreadLocal<Singleton>();
  5.  
    private static Singleton instance;
  6.  
     
  7.  
    private Singleton()
  8.  
     
  9.  
  10.  
     
  11.  
    public static Singleton getInstance()
  12.  
     
  13.  
    // 第一次检查:若线程第一次访问,则进入if语句块;否则,若线程已经访问过,则直接返回ThreadLocal中的值
  14.  
    if (threadlocal.get() == null)
  15.  
    synchronized (Singleton.class)
  16.  
    if (instance == null) // 第二次检查:该单例是否被创建
  17.  
    instance = new Singleton();
  18.  
  19.  
  20.  
    threadlocal.set(instance); // 将单例放入ThreadLocal中
  21.  
  22.  
    return threadlocal.get();
  23.  
  24.  

 

以上是关于如何保证单例模式在多线程中的线程安全性的主要内容,如果未能解决你的问题,请参考以下文章

Spring 单例 多例 线程安全等问题,想请教大家

Controller是单例模式的吗?如何保证线程安全?

C++ 线程安全的单例模式总结

并发编程:单例与多线程

单例模式中的饿汉和懒汉模式

怎么实现一个线程安全的单例模式