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();
    }
}
相关推荐
折哥的程序人生 · 物流技术专研3 小时前
Java 23 种设计模式:从踩坑到精通 | 抽象工厂 —— 支付/收款如何成套创建?跨平台 UI 如何一键换肤?
java·开发语言·后端·设计模式
方也_arkling3 小时前
【Java-Day11】抽象类和抽象方法
java·开发语言
XS0301063 小时前
并发编程 七
java
就叫_这个吧3 小时前
JavaScript中常用事件示例展示附源码
开发语言·javascript·html
YikNjy4 小时前
string(c++)
java·服务器·c++
代码N年归来仍是新手村成员4 小时前
【AWS】Lambda 初识与服务部署
javascript·react.js·ai·node.js·云计算·ai编程·aws
云水一下4 小时前
JavaScript 从零基础到精通系列:流程控制、函数与作用域
前端·javascript
丷丩4 小时前
MapLibre GL JS第28课:PMTiles源和协议
javascript·gis·map·mapbox·maplibre gl js
小江的记录本4 小时前
【Spring AI】Spring AI中RAG误触发与系统提示词泄露问题解决方案(完整版+代码方案)
java·人工智能·spring boot·后端·python·spring·面试
勇往直前plus4 小时前
Python 属性访问与操作全解析:内置函数、魔法方法与描述符深度指南
java·网络·python