本文只需要考虑一件事:如何使单例模式遇到多线程是安全的、正确的

1.立即加载 / "饿汉模式"

什么是立即加载?立即加载就是使用类的时候已经将对象创建完毕,常见的实现办法就是直接 new 实例化。

public class MyObject {
    private static MyObject myObject = new MyObject();

    public MyObject(){

    }

    public static MyObject getInstance(){
        return myObject;
    }

    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(MyObject.getInstance().hashCode());
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(MyObject.getInstance().hashCode());
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(MyObject.getInstance().hashCode());
            }
        }).start();
    }

}

打印结果:

985396398
985396398
985396398

控制台打印的 hashCode 是同一个值,说明对象是同一个,也就实现了立即加载型单例设计模式。

此版本的缺点是不能有其他其他实例变量,因为getInstance()方法没有同步,所以有可能出现非线程安全问题。

2.延迟加载 / "懒汉模式"

什么是延迟加载?延迟加载就是在调用 get() 方法时实例才被创建,常见的实现方法就是在 get() 方法中进行 new() 实例化。

测试代码:

public class MyObject {
    private static MyObject myObject;

    public MyObject() {

    }

    public static MyObject getInstance() {
        try {
            if (myObject == null) {
                //模拟对象在创建之前做的一些准备工作
                Thread.sleep(3000);
                myObject = new MyObject();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return myObject;
    }

    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(MyObject.getInstance().hashCode());
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(MyObject.getInstance().hashCode());
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(MyObject.getInstance().hashCode());
            }
        }).start();
    }

}

打印结果:

985396398
610025186
21895028

从运行结果来看,创建了三个对象,并不是真正的单例模式。原因显而易见,3个线程同时进入了if (myObject == null) 判断语句中,最后各自都创建了对象。

3.延迟加载解决方案

3.1 声明synchronized关键字

既然多个线程可以同时进入getInstance() 方法,那么只需要对其进行同步synchronized处理即可。

public class MyObject {
    private static MyObject myObject;

    public MyObject() {

    }

    synchronized public static MyObject getInstance() {
        try {
            if (myObject == null) {
                //模拟对象在创建之前做的一些准备工作
                Thread.sleep(3000);
                myObject = new MyObject();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return myObject;
    }

    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(MyObject.getInstance().hashCode());
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(MyObject.getInstance().hashCode());
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(MyObject.getInstance().hashCode());
            }
        }).start();
    }

}

打印结果:

961745937
961745937
961745937

虽然运行结果表明,成功实现了单例,但这种给整个方法上锁的解决方法效率太低。

3.2 尝试同步 synchronized 代码块

同步方法是对方法的整体加锁,这对运行效率来讲很不利的。改成同步代码块后:

public class MyObject {
    private static MyObject myObject;

    public MyObject() {

    }

    public static MyObject getInstance() {
        try {
            synchronized (MyObject.class) {
                if (myObject == null) {
                    //模拟对象在创建之前做的一些准备工作
                    Thread.sleep(3000);
                    myObject = new MyObject();
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return myObject;
    }

    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(MyObject.getInstance().hashCode());
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(MyObject.getInstance().hashCode());
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(MyObject.getInstance().hashCode());
            }
        }).start();
    }

}

打印结果:

355159803
355159803
355159803

运行结果虽然表明是正确的,但同步synchronized语句块依旧把整个 getInstance()方法代码包括在内,和synchronize 同步方法效率是一样低下。

3.3 针对某些重要的代码进行单独同步

所以,我们可以针对某些重要的代码进行单独的同步,而其他的代码则不需要同步。这样在运行时,效率完全可以得到大幅度提升。

public class MyObject {
    private static MyObject myObject;

    public MyObject() {

    }

    public static MyObject getInstance() {
        try {
            if (myObject == null) {
                //模拟对象在创建之前做的一些准备工作
                Thread.sleep(3000);
                synchronized (MyObject.class) {
                    myObject = new MyObject();
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return myObject;
    }

    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(MyObject.getInstance().hashCode());
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(MyObject.getInstance().hashCode());
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(MyObject.getInstance().hashCode());
            }
        }).start();
    }

}

运行结果:

985396398
21895028
610025186

此方法只对实例化对象的关键代码进行同步,从语句的结构上来说,运行的效率的确得到的提升。但是在多线程的情况下依旧无法解决得到一个单例对象的结果。

3.4 使用DCL双检查锁机制

在最后的步骤中,使用DCL双检查锁机制来实现多线程环境中的延迟加载单例设计模式。

public class MyObject {
    private volatile static MyObject myObject;

    public MyObject() {

    }

    public static MyObject getInstance() {
        try {
            if (myObject == null) {
                //模拟对象在创建之前做的一些准备工作
                Thread.sleep(3000);
                synchronized (MyObject.class) {
                    if (myObject == null) {
                        myObject = new MyObject();
                    }
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return myObject;
    }

    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(MyObject.getInstance().hashCode());
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(MyObject.getInstance().hashCode());
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(MyObject.getInstance().hashCode());
            }
        }).start();
    }

}

运行结果:

860826410
860826410
860826410

使用DCL双重检查锁功能,成功地解决了“懒汉模式”遇到多线程的问题。DCL也是大多数多线程结合单例模式使用的解决方案。

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

DCL可以解决多线程单例模式的非线程安全问题。当然,还有许多其它的方法也能达到同样的效果。

public class MyObject {
    public static class  MyObjectHandle{
        private static MyObject myObject = new MyObject();
        public static MyObject getInstance() {
            return myObject;
        }
    }

    public static MyObject getInstance(){
        return MyObjectHandle.getInstance();
    }

    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(MyObject.getInstance().hashCode());
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(MyObject.getInstance().hashCode());
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(MyObject.getInstance().hashCode());
            }
        }).start();
    }
}

打印结果:

1035057739
1035057739
1035057739

静态内置类可以达到线程安全问题,但如果遇到序列化对象时,使用默认的方式运行得到的结果还是多例的。

解决方法就是在反序列化中使用readResolve()方法:

public class MyObject implements Serializable {
    //静态内部类
    public static class  MyObjectHandle{
        private static final MyObject myObject = new MyObject();
    }

    public static MyObject getInstance(){
        return MyObjectHandle.myObject;
    }

    protected Object readResolve(){
        System.out.println("调用了readResolve方法");
        return MyObjectHandle.myObject;
    }

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        MyObject myObject = MyObject.getInstance();
        FileOutputStream outputStream = new FileOutputStream(new File("myObject.txt"));
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);
        objectOutputStream.writeObject(myObject);
        objectOutputStream.close();
        System.out.println(myObject.hashCode());

        FileInputStream inputStream =  new FileInputStream(new File("myObject.txt"));
        ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);
        MyObject object = (MyObject) objectInputStream.readObject();
        objectInputStream.close();
        System.out.println(object.hashCode());
    }
}

运行结果:

621009875
调用了readResolve方法
621009875

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

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

public class MyObject {
    private static MyObject myObject = null;
    static {
        myObject = new MyObject();
    }
    public static MyObject getInstance(){
        return myObject;
    }
    public static void main(String[] args){
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(MyObject.getInstance().hashCode());
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(MyObject.getInstance().hashCode());
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(MyObject.getInstance().hashCode());
            }
        }).start();
    }

}

运行结果:

355159803
355159803
355159803

6.使用enum枚举数据类型实现单例模式

枚举enum 和静态代码块的特性相似,在使用枚举类时,构造方法会被自动调用,也可以应用其这个特性实现单例设计模式。

public enum Singleton {
    INSTANCE;
    private MyObject myObject = null;
    Singleton() {
        myObject = new MyObject();
    }
    public MyObject getInstance(){
        return myObject;
    }

    public static void main(String[] args){
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Singleton.INSTANCE.getInstance().hashCode());
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Singleton.INSTANCE.getInstance().hashCode());
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Singleton.INSTANCE.getInstance().hashCode());
            }
        }).start();
    }
}

运行结果:

1516133987
1516133987
1516133987

这样实现的一个弊端就是违反了“职责单一原则”,完善后的代码如下:

public class MyObject {
    public enum Singleton {
        INSTANCE;
        private MyObject myObject = null;

        Singleton() {
            myObject = new MyObject();
        }

        public MyObject getInstance() {
            return myObject;
        }
    }

    public static MyObject getInstance(){
        return Singleton.INSTANCE.getInstance();
    }
    public static void main(String[] args){
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(MyObject.getInstance().hashCode());
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(MyObject.getInstance().hashCode());
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(MyObject.getInstance().hashCode());
            }
        }).start();
    }

}

运行结果:

610025186
610025186
610025186

7.文末总结

本文使用若干案例来阐述单例模式与多线程结合遇到的情况与解决方案。

参看

《Java多线程编程核心技术》高洪岩著

扩展

Java多线程编程核心技术(一)Java多线程技能

Java多线程编程核心技术(二)对象及变量的并发访问

Java多线程编程核心技术(三)多线程通信

Java多线程核心技术(四)Lock的使用

Java多线程核心技术(六)线程组与线程异常

Java多线程核心技术(五)单例模式与多线程的更多相关文章

  1. JAVA学习第二十六课(多线程(五))- 多线程间的通信问题

    一.线程间的通信 实例代码: 需求是:输入一个姓名和性别后,就输出一个姓名和性别 class Resource { String name; String sex ; } class Input im ...

  2. Java第二十五天,多线程之等待唤醒机制

    当线程被创建并且被启动之后,它既不是一启动就进入了执行状态,也不是一直处于执行状态,而是具有以下多种状态: 这六种状态之间的转换关系如下: 1.等待唤醒机制 注意: (1)两个线程之间必须用同步代码块 ...

  3. Java多线程核心技术(六)线程组与线程异常

    本文应注重掌握如下知识点: 线程组的使用 如何切换线程状态 SimpleDataFormat 类与多线程的解决办法 如何处理线程的异常 1.线程的状态 线程对象在不同运行时期有不同的状态,状态信息就处 ...

  4. Java多线程核心技术(四)Lock的使用

    本文主要介绍使用Java5中Lock对象也能实现同步的效果,而且在使用上更加方便. 本文着重掌握如下2个知识点: ReentrantLock 类的使用. ReentrantReadWriteLock ...

  5. Java多线程编程核心技术-第6章-单例模式与多线程-读书笔记

    第 6 章 单例模式与多线程 本章主要内容 如何使单例模式遇到多线程是安全的.正确的. 6.1 立即加载 / “饿汉模式” 什么是立即加载?立即加载就是使用类的时候已经将对象创建完毕,常见的实现办法就 ...

  6. 从ConcurrentHashMap的演进看Java多线程核心技术 Java进阶(六)

    本文分析了HashMap的实现原理,以及resize可能引起死循环和Fast-fail等线程不安全行为.同时结合源码从数据结构,寻址方式,同步方式,计算size等角度分析了JDK 1.7和JDK 1. ...

  7. Java多线程编程(六)单例模式与多线程

    在使用多线程技术的单例模式时会出现一些意想不到的情况,这样的代码如果在生产环境中出现异常,有可能造成灾难性的后果. 一.立即加载/“饿汉模式” 立即加载就是使用类的时候已经将对象创建完毕,常见的实现方 ...

  8. java多线程系列(五)---synchronized ReentrantLock volatile Atomic 原理分析

    java多线程系列(五)---synchronized ReentrantLock volatile Atomic 原理分析 前言:如有不正确的地方,还望指正. 目录 认识cpu.核心与线程 java ...

  9. Java多线程(五)线程的生命周期

    点我跳过黑哥的卑鄙广告行为,进入正文. Java多线程系列更新中~ 正式篇: Java多线程(一) 什么是线程 Java多线程(二)关于多线程的CPU密集型和IO密集型这件事 Java多线程(三)如何 ...

随机推荐

  1. Visual Stuido Online:如何禁止多人同时签出同一文件

    这里只说操作步骤,不讨论为什么要禁止同时多个签出同一文件. 版权声明:转载请保留原文链接. 友情链接:http://www.zhoumy.cn

  2. hdu-2027题&&gets/getchar的区别

    hdu-2027题(水题~~~) 统计每个元音字母在字符串中出现的次数. Input输入数据首先包括一个整数n,表示测试实例的个数,然后是n行长度不超过100的字符串. Output对于每个测试实例输 ...

  3. Flutter Plugin开发流程

    这篇文章主要介绍了Flutter Plugin开发流程,包括如何利用Android Studio开发以及发布等. 本文主要给大家介绍如何开发Flutter Plugin中Android的部分.有关Fl ...

  4. Thrift的C++服务端(线程池和非阻塞)模式

    非阻塞模式 #include "RpcServiceHandler.h" #include <thrift/concurrency/ThreadManager.h> # ...

  5. Jupyter Notebook默认工作路径的修改

    相信每一个学习Python的童鞋,都尝试过Jupyter Notebook,所以我也就不多介绍,真的还不错哎这软件. 不过美中不足的,就是它的默认工作路径,每次打开都是系统盘的Administrato ...

  6. 任意activity作为启动页

    在androidManifest.xml中对应的activity中添加 android:exported=“true”

  7. springmvc复习笔记----springmvc最简单的第一个例子:RequestMapping试水

    结构 用到的包 web.xml <url-pattern>/</url-pattern>中可以换成其他的后缀*.do ,*. sb  …… <?xml version=& ...

  8. Java中输入字符的排列以及按从小到大的顺序输出

    今天笔试,遇到一个问题,大意就是输入一行字符,例如a b c ,按从小到大的顺序输出它们排列而成的字符串,输出就是abc acb bac bca cba cab.求这个程序怎么实现. 其实这个题很简单 ...

  9. LeetCode算法题-Ugly Number(Java实现-四种解法)

    这是悦乐书的第199次更新,第208篇原创 01 看题和准备 今天介绍的是LeetCode算法题中Easy级别的第64题(顺位题号是263).编写一个程序来检查给定的数字是否是一个丑陋的数字.丑陋的数 ...

  10. ​June 10. 2018, Week 24th, Sunday

    There is no friend as loyal as a book. 好书如挚友,情谊永不渝. From Ernest Miller Hemingway. Books are my frien ...