并发验证单例模式与多例模式的适用范围

本文通过并发测试对比单例与多例模式在多线程环境下的表现,揭示了单例模式下公共变量易受污染的问题,并提出了相应的解决方案。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言:

对于单例和多例,很多时候只是看到如下文字的的区别,如果用并发来验证一下到底单例的对象和多例的对象有何肉眼的区别,那样会更有利于理解何时使用单例模式对象,何时使用多例模式对象

验证以下疑问:
1.单例对象中如果只有一个打印参数方法,如果第一个线程获取了实例开始调用访问打印方法,在进打印方法前,线程挂起,此时第二个线程进来获取实例完成打印之后,当第一个线程结束挂起状态,可以执行时,
问: 此时打印会不会报错或者他打印时使用的是哪个对象
2.当该对象中有公共的可变变量时,单例对象在多线程时会导致公共变量值会被莫名其妙的篡改吗,产生这种问题如何解决

概念解析:
什么是单例、多例:
所谓单例就是所有的请求都用一个对象来处理,比如我们常用的service和dao层的对象通常都是单例的,而多例则指每个请求用一个新的对象来处理,比如action;

单例模式和多例模式说明:
1. 单例模式和多例模式属于对象模式。
2. 单例模式的对象在整个系统中只有一份,多例模式可以有多个实例。
3. 它们都不对外提供构造方法,即构造方法都为私有。

在此单例模式挑选饿汉模式的单例模式,因为饿汉模式简单便于理解同时饿汉模式自带线程安群属性
经典的单例饿汉模式如下

public class Singleton{
    private static final Singleton instance = new Singleton();
    private Singleton(){}
    public static Singleton newInstance(){
        return instance;
    }

其他单例模式看这

1. 最关键的单例方法如下

其中模拟修改单例的唯一值 defultKey 来判断当前执行操作的是哪个单例对象,正常情况下,又是单例模式,又想修改单例对象中的公共属性变量,那修改单例对象中唯一值的 defultKey 肯定是需要加锁 synchronized 的,由于我们就是想改变这个唯一值来判断这是哪个对象,所以这里就没有加 synchronized 关键字了
以下代码中:
defultKey 代表单例对象Bean 的唯一值
uniqueKey 代表单例对象返回的 公有参数
getSingleInstance() 为单例对象获取方法
getNewInstance() 为多例对象获取方法
printStr() 为打印方法

package com.example.java.modules.staticFactoryBean.utils;

import java.util.HashMap;
import java.util.Map;

public class StaticFactoryBean {
    private static final StaticFactoryBean singleinstance = new StaticFactoryBean();
    private static StaticFactoryBean newinstance ;

    //模拟修改单例对象唯一值
    public String defultKey = "id";
    private String copyDfultKey = "id";

    //模拟放置单例对象返回的 公有参数
    public Map<String,String> uniqueKey = new HashMap<>();

    //获取单例对象方法
    public static StaticFactoryBean getSingleInstance( String defultKey ) {
        //defultKey为null 取singleinstance得defultKey
        singleinstance.defultKey = null != defultKey ? defultKey : singleinstance.defultKey;
        singleinstance.copyDfultKey = singleinstance.defultKey;
        return singleinstance;
    }
    //获取多例对象方法
    public static StaticFactoryBean getNewInstance( String defultKey ) {
        newinstance = new StaticFactoryBean();
        newinstance.defultKey = null != defultKey ? defultKey : newinstance.defultKey;
        newinstance.copyDfultKey = newinstance.defultKey;
        return newinstance;
    }

    private StaticFactoryBean(){}

    public String printStr( String str ){
        System.out.println("defultKey : "+defultKey+" printStr : "+ str);
        return defultKey;
    }
}

2. 经典生产消费模式模拟并发

2.1 以下代码

testSingleInstance()和singleInstanceMultithread()是一对模拟单例对象并发方法,
testNewInstance()和newInstanceMultithread()是一对模拟多例对象并发方法
public BlockingQueue queue = new LinkedBlockingQueue(10);
模拟生产消费时的阻塞,queue中默认缓存10个String对象,
producer()方法的 queue.put(“生产者产蛋一枚!”) 模拟生产
getInstance()和getInstanceMultithread()中的 queue.take()模拟消费,当queue 中没有值,模拟消费导致线程挂起,直到queue 有值才结束线程挂起状态执行接下来操作,对BlockingQueue还有不明白的
移步这里

2.2 getInstance(boolean getSingleFlag)方法具体做了如下操作:

getSingleFlag用来区分当前要获取单例对象还是多例对象
1.获取单例(多例)StaticFactoryBean 对象并将 defultKey 赋值为 singleId 的实例
2.强制改变StaticFactoryBean 对象中的公共变量 uniqueKey赋值为 getInstance()中参数
3.queue此时为空对象,queue.take()使getInstance()挂起,等待queue中有String对象才再次执行
4.结束挂起状态,在执行StaticFactoryBean 对象打印方法前先查询当前打印对象是哪个
5.调用StaticFactoryBean 打印方法并返回当前打印此方法的对象
6.返回result

2.3 getInstanceMultithread(boolean getSingleFlag)方法具体做了如下操作:

getSingleFlag用来区分当前要获取单例对象还是多例对象
1.获取之前存在的单例(多例)StaticFactoryBean 默认对象(defultKey == null)
2.查看之前默认StaticFactoryBean 对象的defultKey 和uniqueKey公共变量的值,判断实例化对象时单例和多例的区别
3.强制再次获取单例(多例)StaticFactoryBean 对象并将 defultKey 赋值为 multithreadId的实例
4.在执行StaticFactoryBean 对象打印方法前先查询当前打印对象是哪个
5.调用StaticFactoryBean 打印方法并返回当前打印此方法的对象
6…queue此时为空对象,queue.take()使getInstanceMultithread()挂起,等待queue中有String对象才再次执行
7.返回result

package com.example.java.modules.staticFactoryBean.controller.impl;

import com.example.java.modules.staticFactoryBean.utils.StaticFactoryBean;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
@RestController
public class FactoryBeanController {

    public BlockingQueue<String> queue = new LinkedBlockingQueue<String>(10);

    @RequestMapping(value = "/testSingleInstance",method = RequestMethod.GET)
    public Map<String, Object> testSingleInstance() throws InterruptedException {
        boolean getSingleFlag = true ;
        return getInstance( getSingleFlag );
    }

    @RequestMapping(value = "/singleInstanceMultithread",method = RequestMethod.GET)
    public Map<String, Object> singleInstanceMultithread()throws InterruptedException {
        boolean getSingleFlag = true ;
        return getInstanceMultithread(getSingleFlag);
    }

    @RequestMapping(value = "/testNewInstance",method = RequestMethod.GET)
    public Map<String, Object> testNewInstance() throws InterruptedException {
        boolean getSingleFlag = false ;
        return getInstance(getSingleFlag);
    }

    @RequestMapping(value = "/newInstanceMultithread",method = RequestMethod.GET)
    public Map<String, Object> newInstanceMultithread() throws InterruptedException {
        boolean getSingleFlag = false ;
        return getInstanceMultithread(getSingleFlag);
    }

    public Map<String,Object> getInstance(boolean getSingleFlag) throws InterruptedException {
        LinkedHashMap<String,Object> result = new LinkedHashMap<>();
        //获取单个 defultKey 赋值为 singleId 的实例
        StaticFactoryBean staticFactoryBean;
        if( getSingleFlag ){
            staticFactoryBean= StaticFactoryBean.getSingleInstance("singleId");
        }else {
            staticFactoryBean= StaticFactoryBean.getNewInstance("singleId");
        }
        System.out.println("testSingleInstance  defultKey : "+ staticFactoryBean.defultKey);
        result.put("testInstance defultKey",staticFactoryBean.defultKey);
        Map<String,String> uniqueKey = new HashMap<>();
        uniqueKey.put("key1","1");
        uniqueKey.put("key2","2");
        //模拟修改变量值  给staticFactoryBean中的uniqueKey赋值,
        staticFactoryBean.uniqueKey = uniqueKey;
        result.put("testInstance uniqueKey" , uniqueKey) ;

        //模拟获取对象线程导致暂时不能打印   初始quene中没有值,模拟消费导致线程挂起
        queue.take();
        result.put("defultKey befor printStr" ,  staticFactoryBean.defultKey) ;
        String defultKey = staticFactoryBean.printStr("get singleInstance  defultKey");
        result.put("printStr " ,  "get singleInstance  defultKey") ;
        result.put("defultKey after printStr" ,  defultKey) ;
        return result;
    }

    public Map<String,Object> getInstanceMultithread(boolean getSingleFlag) throws InterruptedException {
        LinkedHashMap<String,Object> result = new LinkedHashMap<>();
        //期望获得默认的 defultKey 为 id 的实例
        StaticFactoryBean staticFactoryBeanBefor ;
        if( getSingleFlag ){
            staticFactoryBeanBefor= StaticFactoryBean.getSingleInstance(null);
        }else {
            staticFactoryBeanBefor= StaticFactoryBean.getNewInstance(null);
        }
        System.out.println("get defultKey befor Multithread: "+ staticFactoryBeanBefor.defultKey);
        result.put("get defultKey Befor Multithread",staticFactoryBeanBefor.defultKey);
        result.put("get uniqueKey Befor Multithread" , staticFactoryBeanBefor.uniqueKey) ;

        //再次强制重新获取 defultKey 为 id 的实例
        StaticFactoryBean staticFactoryBeanAgain;
        if( getSingleFlag ){
            staticFactoryBeanAgain= StaticFactoryBean.getSingleInstance("multithreadId");
        }else {
            staticFactoryBeanAgain= StaticFactoryBean.getNewInstance("multithreadId");
        }
        System.out.println("get instanceMultithread  defultKey  again: "+ staticFactoryBeanAgain.defultKey);
        result.put("get defultKey again",staticFactoryBeanAgain.defultKey);
        result.put("get duniqueKey again" , staticFactoryBeanAgain.uniqueKey) ;
        result.put("get defultKey befor printStr" ,  staticFactoryBeanAgain.defultKey) ;
        String defultKey = staticFactoryBeanAgain.printStr("get instance Multithread  defultKey  again");
        result.put("printStr " ,  "get instance Multithread  defultKey  again") ;
        result.put("get defultKey after printStr" ,  defultKey) ;
        //模拟打印完之后不释放对象   初始quene中没有值,模拟消费导致线程挂起
        queue.take();
        return result;
    }

    @RequestMapping(value = "/producer",method = RequestMethod.GET)
    public Map<String, BlockingQueue> producer() throws InterruptedException {
        Map<String,BlockingQueue> result = new HashMap<>();
        queue.put("生产者产蛋一枚!");
        result.put("queue",queue);
        return result;
    }
}
2.4 启动程序后调用顺序
1.验证单例对象下并发的执行现象

1.1 先调用testSingleInstance()方法
http://localhost:8080/testSingleInstance
此时postman模拟调用平台没有马上返回值,但是控制台有打印如下参数
testSingleInstance defultKey : singleId
1.2 再调用singleInstanceMultithread()方法
http://localhost:8080/singleInstanceMultithread
此时postman模拟调用平台依旧没有马上返回值,因为线程都被queue的空栈挂起了嘛.
看控制台会依次有参数打出
1.3 模拟生产调用producer()一次
http://localhost:8080/producer
调用一次之后会看到第一次调用的testSingleInstance()方法有返回值了,返回值如下

{
  "testInstance defultKey": "singleId",
  "testInstance uniqueKey": {
    "key1": "1",
    "key2": "2"
  },
  "defultKey befor printStr": "multithreadId",
  "printStr ": "get singleInstance  defultKey",
  "defultKey after printStr": "multithreadId"
}

前面4个返回值都是预期到的正常值,最后一个返回值有点意思,调用StaticFactoryBean 单例对象的打印方法的对象居然是 multithreadId,可我们之前的对象就是 singleId ,只是线程被别人抢去了,怎么对象也被改了,但是好在还是执行了打印操作,而且打印出来的参数没有问题

结论1:

对开始提出的第一个疑问进行了解答,同时也从侧面看出,如果某一个单例 StaticFactoryBean中,如果只是单纯的通过单例StaticFactoryBean调用打印printStr()方法而在该打印方法中不涉及对公共变量的使用,则该StaticFactoryBean不加同步synchronized 关键字也没关系,因为不管打印这个方法何时被抢断都会正常执行,只是有可能执行的引用被"修改了"

1.3 再次模拟生产调用producer()一次
http://localhost:8080/producer
再次调用一次之后会看到第二次调用的singleInstanceMultithread()方法有返回值了,返回值如下

{
  "get defultKey Befor Multithread": "singleId",
  "get uniqueKey Befor Multithread": {
    "key1": "1",
    "key2": "2"
  },
  "get defultKey again": "multithreadId",
  "get duniqueKey again": {
    "key1": "1",
    "key2": "2"
  },
  "get defultKey befor printStr": "multithreadId",
  "printStr ": "get instance Multithread  defultKey  again",
  "get defultKey after printStr": "multithreadId"
}

从第一个参数看出,第一次获取的默认StaticFactoryBean单例还是之前调用的 singleId,从第三个参数和第四个参数看出,当强制重新获取单例StaticFactoryBean后,即使defultKey 变为了multithreadId,但是公共变量 uniqueKey 还是之前的singleId实例的,这样就会存在问题,既单例对象中,多线程会导致公共变量莫名其妙被污染,从而导致结果异常或者直接程序报错

结论2:

对开始提出的第二个疑问进行了解答,对于有公共变量的单例对象,以上结果肯定是有问题的,有如下解决办法
1.如果StaticFactoryBean对象中存在需要被修改的公共变量,比如模拟作为返回值的uniqueKey ,那么要么在获取单例对象时就要加锁,但是这样就会使调用该方法的使用方变为串行调用
2.如果该对象确实占用内存不大,可以存在多份,使之成为多例是最优解决方案,因为如果是多例,既每次调用getNewInstance( String defultKey )时会重 new 一个 StaticFactoryBean实例,那在初始化时,公共变量就会被默认变为原来StaticFactoryBean没有被任何操作时的状态,这点很重要

2.验证多例对象下并发的执行现象

再次按照执行单例的步骤依次执行以下链接
http://localhost:8080/testNewInstance
http://localhost:8080/newInstanceMultithread
http://localhost:8080/producer 执行两次

testNewInstance返回结果:

{
  "testInstance defultKey": "singleId",
  "testInstance uniqueKey": {
    "key1": "1",
    "key2": "2"
  },
  "defultKey befor printStr": "singleId",
  "printStr ": "get singleInstance  defultKey",
  "defultKey after printStr": "singleId"
}

newInstanceMultithread返回结果:

{
  "get defultKey Befor Multithread": "id",
  "get uniqueKey Befor Multithread": {},
  "get defultKey again": "multithreadId",
  "get duniqueKey again": {},
  "get defultKey befor printStr": "multithreadId",
  "printStr ": "get instance Multithread  defultKey  again",
  "get defultKey after printStr": "multithreadId"
}

从testNewInstance返回结果 defultKey after printStr 看,不管testNewInstance被打断多少次,执行打印方法的对象永远是singleId,
从newInstanceMultithread返回结果 get defultKey Befor Multithread 和 get duniqueKey again 看,只要是多例的,StaticFactoryBean中的所有对象都是该对象定义之前的初始化状态,既 new 的 StaticFactoryBean中, defultKey = “id”
uniqueKey = {}

代码上传地址在这: git下载地址

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值