Java多线程编程:单例模式

立即加载:“饿汉模式”

立即加载就是指使用类的时候已经将对象创建完毕,常见的实现方法就是直接new实例化。也就是在调用方法前,实例就被创建了。示例代码如下所示:

classMyObject{privatestaticMyObject myObject=newMyObject();privateMyObject(){}publicstaticMyObjectgetInstance(){//如果还有其他代码,存在线程安全问题returnmyObject;    }}classMyThreadextendsThread{@Overridepublicvoidrun(){        System.out.println(MyObject.getInstance().hashCode());    }}publicclassRun{publicstaticvoidmain(String[] args){        MyThread t1=newMyThread();        MyThread t2=newMyThread();        MyThread t3=newMyThread();        t1.start();        t2.start();        t3.start();    }}

运行结果如下:

586158855861588558615885

可以发现,实现了单例模式,因为多个线程得到的实例的hashCode是一样的。

延迟加载:“懒汉模式”

延迟加载就是在调用getInstance()方法时实例才被创建,常见的方法就是在getInstance()方法中进行new实例化。实现代码如下:

classMyObject{privatestaticMyObject myObject;privateMyObject(){}publicstaticMyObjectgetInstance(){if(myObject==null){            myObject=newMyObject();        }returnmyObject;    }}classMyThreadextendsThread{@Overridepublicvoidrun(){        System.out.println(MyObject.getInstance().hashCode());    }}publicclassRun{publicstaticvoidmain(String[] args){        MyThread t1=newMyThread();        MyThread t2=newMyThread();        MyThread t3=newMyThread();        t1.start();        t2.start();        t3.start();    }}

但是由于在getInstance()中,存在多条语句,因此可能存在线程安全问题。运行结果也显示了这一点:

204153142013483456331348345633

甚至,当getInstance()中,有更多的语句,会出现不同的三个对象,在if(myObject==null)语句块中加入Thread.sleep(3000),运行结果如下所示:

21862076358615885712355351

解决方案:DCL

如果使用synchronized关键字,对整个getInstance()上锁或者对整个if语句块加锁,会存在效率问题。

最终采用了DCL(Double-Check Locking)双检查锁机制,也是大多数多线程结合单例模式使用的解决方案。第一层主要是为了避免不必要的同步,第二层判断则是为了在null情况下才创建实例。

publicclassMyObject{privatestaticMyObject myObject;privateMyObject(){    }publicstaticMyObjectgetInstance(){if(myObject ==null) {try{                Thread.sleep(3000);            }catch(InterruptedException e) {                e.printStackTrace();            }synchronized(MyObject.class) {if(myObject ==null) {                    myObject =newMyObject();                }            }        }returnmyObject;    }}

测试结果,得到的是相同的hashcode。

静态内置类

publicclassMyObject{privatestaticclassMyObjectHandler{privatestaticMyObject myObject=newMyObject();    }privateMyObject(){    }publicstaticMyObjectgetInstance(){try{            Thread.sleep(1000);        }catch(InterruptedException e) {            e.printStackTrace();        }returnMyObjectHandler.myObject;    }}

采用静态内置类的方法,是线程安全的。

使用static代码块

静态代码块的代码再使用类的时候就已经执行了,所以可以应用静态代码块的这个特性来实现单例设计模式。

publicclassMyObject{privatestaticMyObject myObject=null;static{myObject=newMyObject();}privateMyObject(){    }publicstaticMyObjectgetInstance(){try{            Thread.sleep(1000);        }catch(InterruptedException e) {            e.printStackTrace();        }returnmyObject;    }}

使用enum枚举数据类型

使用枚举类时,和静态代码块的特性相似,构造方法会被自动调用。枚举在经过javac的编译之后,会被转换成形如public final class T extends Enum的定义。也就是说,我们定义的一个枚举,在第一次被真正用到的时候,会被虚拟机加载并初始化,而这个初始化过程是线程安全的。而我们知道,解决单例的并发问题,主要解决的就是初始化过程中的线程安全问题。

所以,由于枚举的以上特性,枚举实现的单例是天生线程安全的。同时,枚举可解决反序列化会破坏单例的问题。

enumMyObject{    INSTANCE;}

SimpleDataFormat

SimpleDataFormat使用了单例模式,具有线程安全问题。SimpleDateFormat中的日期格式不是同步的。推荐(建议)为每个线程创建独立的格式实例。如果多个线程同时访问一个格式,则它必须保持外部同步。

解决方案1:需要的时候创建新实例

public class DateUtil {        public static  String formatDate(Date date)throws ParseException{        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");returnsdf.format(date);    }        public static Date parse(String strDate) throws ParseException{        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");returnsdf.parse(strDate);    }}

在需要用到SimpleDateFormat 的地方新建一个实例,不管什么时候,将有线程安全问题的对象由共享变为局部私有都能避免多线程问题,不过也加重了创建对象的负担。在一般情况下,这样其实对性能影响比不是很明显的。

解决方案2:同步SimpleDateFormat对象

publicclassDateSyncUtil{privatestaticSimpleDateFormat sdf =newSimpleDateFormat("yyyy-MM-dd HH:mm:ss");publicstaticStringformatDate(Date date)throwsParseException{synchronized(sdf){returnsdf.format(date);        }      }publicstaticDateparse(String strDate)throwsParseException{synchronized(sdf){returnsdf.parse(strDate);        }    } }

当线程较多时,当一个线程调用该方法时,其他想要调用此方法的线程就要block,多线程并发量大的时候会对性能有一定的影响。

解决方案3:使用ThreadLocal

publicclassConcurrentDateUtil{privatestaticThreadLocal threadLocal =newThreadLocal() {@OverrideprotectedDateFormatinitialValue(){returnnewSimpleDateFormat("yyyy-MM-dd HH:mm:ss");        }    };publicstaticDateparse(String dateStr)throwsParseException{returnthreadLocal.get().parse(dateStr);    }publicstaticStringformat(Date date){returnthreadLocal.get().format(date);    }}

使用ThreadLocal, 也是将共享变量变为独享,线程独享肯定能比方法独享在并发环境中能减少不少创建对象的开销。如果对性能要求比较高的情况下,一般推荐使用这种方法。

关注、转发、评论头条号每天分享java 知识,私信回复“555”赠送一些Dubbo、Redis、Netty、zookeeper、Spring cloud、分布式资料

你可能感兴趣的:(Java多线程编程:单例模式)