单例模式是设计模式中比较简单的一种。在编程开发中主要解决的问题就是在一个系统中,需要一个单例对象被一个系统的不同模块的不同对象所访问,且保证访问的都是同一个对象,因此便需要一个全局的访问指针,这便是众所周知的单例模式的应用。但是如果编写出一个线程安全且高效的单例模式,却需要考虑很多很多问题,接下来我们就一步一步的分析。
1、立即加载/“饿汉模式”
什么是立即加载?立即加载就是使用类的时候已经将对象创建完毕,常见的实现方法就是直接new实例化。而立即加载从中文的语境上看,有“着急”、“急迫”的含义,所以也称为“饿汉模式”
立即加载/“饿汉模式”是在调用方法前,实例已被创建了,来看一下示例代码
创建单例类
public class MyObject {
//立即加载==饿汉模式
private static MyObject myObject = new MyObject();
private MyObject(){
}
public static MyObject getInstance(){
return myObject;
}
}
创建线程类
public class MyThread extends Thread{
@Override
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();
}
}
运行结果
215722165
215722165
215722165
Process finished with exit code 0
我们已经创建了一个普通的单例模式的类,通过控制台的打印看到hashcode是同一个值,说明对象是同一个,也就完成我们刚刚所要求的饿汉模式。这个类很简单,代码也很容易理解,因为单例静态实例对象在编译的时候就已经完成了初始化,所以不存在线程安全问题。
2、延迟加载/“懒汉模式”
什么是延迟加载?延迟加载就是在调用get()方法时实例才被创建,常见的实现方法就是在get()方法的时候实例化。而延迟加载从中文的语境来看,是“缓慢”、“不急迫”的含义,所以也称为“懒汉模式”
1.延迟加载/“懒汉模式”解析
延迟加载/“懒汉模式”是在调用方法时实例才被创建,让我们看一下实现代码。我们只需要改变MyObject的代码即可
public class MyObject {
private static MyObject myObject;
private MyObject(){
}
public static MyObject getInstance(){
//延迟加载
if(myObject != null){
}else {
myObject = new MyObject();
}
return myObject;
}
}
这里建议大家动手去敲一下代码,因为代码比较简单,就直接说结果了。
如果是单个线程运行的话,取的对象肯定总是同一个对象,但是在多线程环境中,有时候取的对象就有可能不是同一个对象,这个时候就出现了线程安全问题,请大家耐心的往后面去看,后面有详细的解决方案。
总结:虽然懒汉模式实现了单例设计模式,但是在多线程的环境下存在线程安全问题,根本就不可能保证多线程环境下取得的是同一个对象。
接下来我们看看如何在多线程环境下如果较为安全的单例模式。
我们还是设计刚刚的那个懒汉模式,只需要修改MyObject代码即可
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){
e.printStackTrace();
}
return myObject;
}
}
结果:通过控制台运行结果,我们可以看到,我们在多线程的环境下实现了单例模式,但是通过synchronized加锁的这种方式去把整个方法锁起来,只有线程拿到锁,并释放资源,这个时候线程还得去抢夺锁,抢到了才能获取单例模式类,进行下一步操作。
针对上面synchronized锁的粒度比较大,这次我们把比较重要的一些代码段锁起来。
继续改写MyObject代码,其余的代码保持不变
public class MyObject {
private static MyObject myObject;
private MyObject(){
}
public static MyObject getInstance(){
try{
if(myObject!=null){
}else{
//模拟在创建对象之前做一些准备工作
Thread.sleep(3000);
synchronized (MyObject.class){
myObject = new MyObject();
}
}
}catch (InterruptedException e){
e.printStackTrace();
}
return myObject;
}
}
此方法使用synchronized同步代码块,只对实例化的关键对象进行了同步,从语句的结构上说,运行的效率确实得到了提升,但是如果在多线程环境下,还是无法解决线程安全问题,我们接下来推出最终解决方案。
使用DCL(Double-Check Locking)双重检查锁机制,
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();
}
return myObject;
}
}
使用双重检查锁功能,成功地解决了懒汉模式遇到的多线程问题。DCL也是大多数多线程结合单例模式使用的解决方案。