Java多线程技术五——单例模式与多线程

1 概述

本章的知识点非常重要。在单例模式与多线程技术相结合的过程中,我们能发现很多以前从未考虑过的问题。这些不良的程序设计如果应用在商业项目中将会带来非常大的麻烦。本章的案例也充分说明,线程与某些技术相结合中,我们要考虑的事情会更多。在学习本章的过程中,我们只需要考虑一件事情,那就是:如果使单例模式与多线程结合时是安全、正确的。

2 单例模式与多线程

在标准的23个设计模式中,单例模式在应用中是比较常见的。但多数常规的该模式教学资料并没有结合多线程技术进行介绍,这就造成在使用结合多线程的单例模式时会出现一些意外。

3 立即加载/饿汉模式

立即加载指的是,使用类的时候已经将对象创建完毕。常见的实现办法就是new实例化,也被称为"饿汉模式"。

java 复制代码
public class MyObject {
    //立即加载方法 == 饿汉模式
    private static MyObject object = new MyObject();
    private MyObject(){

    }
    public static MyObject getInstance(){
        return object;
    }

}
java 复制代码
public class MyThread extends Thread{
    @Override
    public void run(){
        System.out.println(MyObject.getInstance().hashCode());
    }
}
java 复制代码
public class Run1 {
    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        MyThread t3 = new MyThread();
        t1.start();
        t2.start();
        t3.start();
    }
}

控制台打印的hashcode是同一个值,说明对象是一个,也就实现了立即加载型单例模式。此代码为立即加载模式,缺点是不能有其他实例变量,因为getInstance()方法没有同步,所以有可能出现非线程安全问题。

4 延迟加载/懒汉模式

延迟加载就是调用get()方法时,实例才被创建。常见的实现办法就是在get()方法中进行new实例化,也被称为"懒汉模式"。

4.1 延迟加载解析

先看下面一段代码。

java 复制代码
public class MyObject {
    private static MyObject object;

    public MyObject() {
    }
    public static MyObject getInstance(){
        if(object == null){
            object = new MyObject();
        }
        return object;
    }
}
java 复制代码
public class MyThread extends Thread{
    @Override
    public void  run(){
        System.out.println(MyObject.getInstance().hashCode());
    }
}
java 复制代码
public class Run1 {
    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        MyThread t3 = new MyThread();
        t1.start();
        t2.start();
        t3.start();
    }
}

4.2 延迟加载的缺点

前面两个实验虽然使用"立即加载"和"延迟加载"实现了单例模式,但在多线程环境中,"延迟加载"示例中的代码完全是错误的,根本不能保持单例的状态。下面来看如何在多线程环境中结合错误的单例模式创建出多个实例的。

java 复制代码
public class MyObject {
    private static MyObject object;

    public MyObject() {
    }
    public static MyObject getInstance(){
        try {
            if(object == null){
                //模拟在创建对象之前做一些准备工作
                Thread.sleep(3000);
                object = new MyObject();
            }
        }catch (InterruptedException e){
            e.printStackTrace();
        }
        return object;
    }
}
java 复制代码
public class MyThread extends Thread{
    @Override
    public void run(){
        System.out.println(MyObject.getInstance().hashCode());
    }
}
java 复制代码
public class Run1 {
    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        MyThread t3 = new MyThread();
        t1.start();
        t2.start();
        t3.start();
    }
}

控制台打印3个不同的hashCode,说明创建了3个对象,并不是单例的。这就是"错误的单例模式",如何解决呢?

4.3 延迟加载的解决方案

(1)声明synchronzied关键字

既然多个线程可以同时进入getInstance()方法,只需要对getInstance()方法声明synchronzied关键字即可。修改MyObject.java类。

java 复制代码
public class MyObject {
    private static MyObject object;

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

此方法在加入同步synchronzied关键字后得到相同实例的对象,但运行效率很低。下一个线程想要取得 对象,必须等待上一个线程释放完锁之后,才可以执行。那换成同步代码块可以解决吗?

(2)尝试同步代码块

修改MyObject.java类。

java 复制代码
public class MyObject {
    private static MyObject object;

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

        }catch (InterruptedException e){
            e.printStackTrace();
        }
        return object;
    }
}

此方法加入同步synchronzied语句块后得到相同实例对象,但运行效率也非常低,和synchronzied同步方法一样是同步运行的。下面继续更改代码,尝试解决这个问题。

(3)针对某个重要的代码进行单独的同步。

修改MyObject.java类。

java 复制代码
public class MyObject {
    private static MyObject object;

    public MyObject() {
    }
    public static MyObject getInstance(){
        try {

                if(object == null){
                    //模拟在创建对象之前做一些准备工作
                    Thread.sleep(3000);
                    synchronized (MyObject.class) {
                        object = new MyObject();
                    }
                }


        }catch (InterruptedException e){
            e.printStackTrace();
        }
        return object;
    }
}

此方法使同步synchronzied语句块只对实例化对象的关键代码进行同步。从语句的结构上讲,运行效率却是得到了提升,但遇到多线程的情况还是无法得到同一个实例对象。

(4)使用DCL双检查锁机制

java 复制代码
public class MyObject {
    private  volatile static MyObject object;

    public MyObject() {
    }
    public static MyObject getInstance(){
        try {
            if(object == null){
                Thread.sleep(2000);
                synchronized (MyObject.class){
                    if(object == null){
                        object = new MyObject();
                    }
                }
            }
        }catch (InterruptedException e){
            e.printStackTrace();
        }
        return object;
    }
}

使用volatile修改变量object,使该变量在多个线程间可见,另外禁止 object = new MyObject()代码重排序。object = new MyObject()包含3个步骤:

1、memory = allocate();//分配对象的内存空间

2、ctorInstance(memory);//初始化对象

3、object = memory;//设置instance指向刚分配的内存地址

JIT编译器有可能将这三个步骤重新排序。

1、memory = allocate();//分配对象的内存空间

2、object = memory;//设置instance指向刚分配的内存地址

3、ctorInstance(memory);//初始化对象

这时,构造方法虽然还没有执行,但object对象已具有内存地址,即值不是null。当访问object对象中的值时,是当前声明数据类型的默认值。

创建线程类MyThread.java代码如下。

java 复制代码
public class MyThread extends Thread{
    @Override
    public void run(){
        System.out.println(MyObject.getInstance().hashCode());
    }
}
java 复制代码
public class Run1 {
    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        MyThread t3 = new MyThread();
        t1.start();
        t2.start();
        t3.start();
    }
}

可见,使用DCL双检查锁成功解决了懒汉模式下的多线程问题。DCL也是大多数多线程结合单例模式使用的解决方案。

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

DCL可以解决多线程单例模式的非线程安全问题。还可以使用其他办法达到同样的效果。

java 复制代码
public class MyObject {
    private static class MyObjectHandler{
        private static MyObject object = new MyObject();
    }

    public MyObject() {
    }
    public static MyObject getInstance(){
        return MyObjectHandler.object;
    }
}
java 复制代码
public class MyThread extends Thread{
    @Override
    public void run(){
        System.out.println(MyObject.getInstance().hashCode());
    }
}
java 复制代码
public class Run1 {
    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        MyThread t3 = new MyThread();
        t1.start();
        t2.start();
        t3.start();
    }
}

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

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

java 复制代码
public class MyObject {
    private static MyObject object = null;

    public MyObject() {
    }
    static {
        object = new MyObject();
    }
    public static MyObject getInstance(){
        return  object;
    }
}
java 复制代码
public class MyThread extends Thread{
    @Override
    public void run(){
        for (int i = 0; i < 5; i++) {
            System.out.println(MyObject.getInstance().hashCode());
        }
    }
}
java 复制代码
public class Run1 {
    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        MyThread t3 = new MyThread();
        t1.start();
        t2.start();
        t3.start();
    }
}
相关推荐
电饭叔23 分钟前
《python语言程序设计》2018版第8章19题几何Rectangle2D类(下)-头疼的几何和数学
开发语言·python
Eternal-Student24 分钟前
everyday_question dq20240731
开发语言·arm开发·php
极客先躯28 分钟前
高级java每日一道面试题-2024年10月3日-分布式篇-分布式系统中的容错策略都有哪些?
java·分布式·版本控制·共识算法·超时重试·心跳检测·容错策略
卑微求AC40 分钟前
(C语言贪吃蛇)11.贪吃蛇方向移动和刷新界面一起实现面临的问题
c语言·开发语言
夜月行者1 小时前
如何使用ssm实现基于SSM的宠物服务平台的设计与实现+vue
java·后端·ssm
程序猿小D1 小时前
第二百六十七节 JPA教程 - JPA查询AND条件示例
java·开发语言·前端·数据库·windows·python·jpa
Yvemil71 小时前
RabbitMQ 入门到精通指南
开发语言·后端·ruby
潘多编程1 小时前
Java中的状态机实现:使用Spring State Machine管理复杂状态流转
java·开发语言·spring
_阿伟_1 小时前
SpringMVC
java·spring
代码在改了2 小时前
springboot厨房达人美食分享平台(源码+文档+调试+答疑)
java·spring boot