当前位置:

首页 > 编程开发 > java单例模式和线程安全问题怎么解决

java单例模式和线程安全问题怎么解决

单例模式、多实例模式、和线程安全单例模式单例模式是指确保一个类仅有一个唯一的实例,并且提供了一个全局的访问点。分类:懒汉式、饿汉式为什么需要单例模式?再某些特殊的情况下,存在一个类仅能用来产生一个唯一对象的必要性。例如:打印机室有许多打印机,但是它的打印管理系统只有一个打印任务控制对象,该对象管理打印排队并分配打印任务给各个打印机。单例模式正是为了解决这样的需求而产生的。实现思路:为了防止客户端利用构造器创建多个对象,将构造方法声明为private类型。但这样会使得这个类不可用,所以必须提供一个可以获得实

本文目录

      单例模式、多实例模式、和线程安全

      单例模式

      单例模式是指确保一个类仅有一个唯一的实例,并且提供了一个全局的访问点。

      分类: 懒汉式、饿汉式

      为什么需要单例模式?

      再某些特殊的情况下,存在一个类仅能用来产生一个唯一对象的必要性。例如:打印机室有许多打印机,但是它的打印管理系统只有一个打印任务控制对象,该对象管理打印排队并分配打印任务给各个打印机。单例模式正是为了解决这样的需求而产生的。

      实现思路:

      为了防止客户端利用构造器创建多个对象,将构造方法声明为 private 类型。但这样会使得这个类不可用,所以必须提供一个可以获得实例的静态方法,通常称为 getInstance 方法, 该方法返回一个实例。这个方法必须是静态的,因为静态方法是根据类名调用的,否则也是无法使用的。

      类图:懒汉式

      java单例模式和线程安全问题怎么解决

      类图:饿汉式

      java单例模式和线程安全问题怎么解决

      先来看一个简单的例子:

      测试单例类:Dog’

      //懒汉式
      public class Dog {
      	private static Dog dog;
      	private String name;
      	private int age;
      	
      	//私有的构造器
      	private Dog() {}
      	
      	public String getName() {
      		return name;
      	}
      
      	public void setName(String name) {
      		this.name = name;
      	}
      
      	public int getAge() {
      		return age;
      	}
      
      	public void setAge(int age) {
      		this.age = age;
      	}
      
      	//静态工厂方法
      	public static Dog getInstance() {
      		if (dog == null) {
      			dog = new Dog();
      		}
      		return dog;
      	}
      
      	@Override
      	public String toString() {
      		return "Dog [name=" + name + ", age=" + age + "]";
      	}
      }

      测试单例类:Cat

      //饿汉式
      public class Cat {
      	private static Cat cat = new Cat();
      	private String name;
      	private int age;
      	
      	//私有构造器
      	private Cat() {}
      	
      	public String getName() {
      		return name;
      	}
      
      	public void setName(String name) {
      		this.name = name;
      	}
      	
      	public int getAge() {
      		return age;
      	}
      	
      	public void setAge(int age) {
      		this.age = age;
      	}
      
      	//静态工厂方法
      	public static Cat getInstance() {
      		return cat;
      	}
      
      	@Override
      	public String toString() {
      		return "Cat [name=" + name + ", age=" + age + "]";
      	}
      }

      测试类

      import java.util.HashSet;
      import java.util.Set;
      
      public class Client {
      
      	public static void main(String[] args) {
      		//单线程模式测试
      		Dog dog1 = Dog.getInstance();
      		Dog dog2 = Dog.getInstance();
      		System.out.println("dog1 == dog2: "+(dog1 == dog2));
      		
      		Cat cat1 = Cat.getInstance();
      		Cat cat2 = Cat.getInstance();
      		System.out.println("cat1 == cat2: "+(cat1 == cat2));
      	}
      }

      运行结果

      java单例模式和线程安全问题怎么解决

      懒汉式和饿汉式对比

      创建区别

      懒汉式是在第一次调用静态方法 getInstance() 时创建单例对象。
      饿汉式是在类加载时创建单例对象,即在声明静态单例对象时实例化单例类。

      线程安全

      懒汉式是线程不安全的,而饿汉式是线程安全的(下面会测试)。

      资源占用

      懒汉式是等到使用时才会创建,而饿汉式是在类加载时创建。所以懒汉式没有饿汉式快,但是饿汉式比较占用资源,如果一直不使用,会很占据资源。

      多线程模式下的安全性

      多线程类

      import java.util.HashSet;
      import java.util.Set;
      
      public class DogThread extends Thread{
      	private Dog dog;
      	private Set set;
      	
      	public DogThread() {
      		set = new HashSet<>();
      	}
      	
      	//这个方法是为了测试添加的。
      	public int getCount() {
      		return set.size();
      	}
      	
      	@Override
      	public void run() {
      		dog = Dog.getInstance();
      		set.add(dog);
      	}
      }

      多线程测试类

      import java.util.HashSet;
      import java.util.Set;
      
      public class Client {
      
      	public static void main(String[] args) {
      		//单线程模式测试
      		Dog dog1 = Dog.getInstance();
      		Dog dog2 = Dog.getInstance();
      		System.out.println("dog1 == dog2: "+(dog1 == dog2));
      		
      		Cat cat1 = Cat.getInstance();
      		Cat cat2 = Cat.getInstance();
      		System.out.println("cat1 == cat2: "+(cat1 == cat2));
      		
      		//多线程模式测试
      		DogThread dogThread = new DogThread();
      		Thread thread = null;
      		for (int i = 0; i < 10; i++) {
      			thread = new Thread(dogThread);
      			thread.start();	
      		}
      		
      		try {
      			Thread.sleep(2000); //主线程等待子线程完成!
      		} catch (InterruptedException e) {
      			e.printStackTrace();
      		}
      		System.out.println("dog's number: "+dogThread.getCount());
      	}
      }

      运行结果
      注意:多线程的结果是很难预测的,这里涉及线程的竞争,可能多次运行结果是一样的(多次一样并不代表是绝对正确),但是只要多次测试,就能看到不一样的结果。

      java单例模式和线程安全问题怎么解决

      java单例模式和线程安全问题怎么解决

      说明

      这里我使用一点集合的技巧,利用 Set 集合的特性,把每次产生的 dog 对象存入 Set集合中,最后只要调用集合的 size() 方法就行了。可以看出来产生了两个 dog 对象,这就是产生了错误,这就是属于编程错误了。还要明白多线程下不一定会出错,所以产生的 dog 对象小于线程数。
      由于 饿汉式单例 是线程安全的,这里就不测试了,有兴趣的可以测试一下。

      解决懒汉式单例线程安全的方法:同步
      注意:同步有很多种方法,也可以使用 Lock 进行处理,同步是一种方法,不是特指 synchronzied 这个关键字,感兴趣的人可以多探究一下。
      并且同步的方法通常比较慢,性能方面也要权衡。

      	//静态同步工厂方法
      	public synchronized static Dog getInstance() {
      		if (dog == null) {
      			dog = new Dog();
      		}
      		return dog;
      	}

      多实例模式

      这里补充一个多实例的模式,就是对象数量是固定数目的。可以看出单例模式的推广。当然了实现方式也有很多,大家可以尝试以下,这里是我的方式。

      多实例模式类

      //固定数目实例模式
      public class MultiInstance {
      	//实例数量,这里为四个
      	private final static int INSTANCE_COUNT = 4;
      	private static int COUNT = 0;
      	private static MultiInstance[] instance = new MultiInstance[4];
      	
      	private MultiInstance() {};
      	
      	public static MultiInstance getInstance() {
      		//注意数组的下标只能为 COUNT - 1
      		if (MultiInstance.COUNT <= MultiInstance.INSTANCE_COUNT - 1) {
      			instance[MultiInstance.COUNT] = new MultiInstance();
      			MultiInstance.COUNT++;
      		}
      		//返回实例前,执行了 COUNT++ 操作,所以 应该返回上一个实例
      		return MultiInstance.instance[MultiInstance.COUNT-1];  
      	}
      }

      测试类

      import java.util.HashSet;
      import java.util.Set;
      
      public class Test {
      	public static void main(String[] args) {
      		
      		System.out.println("------------------------");
      		testMultiInstance();
      	}
      
      	//测试多实例模式(单例的扩展,固定数目实例)
      	public static void testMultiInstance() {
      		Set instanceSet = new HashSet<>();
      		MultiInstance instance = null;
      		for (int i = 0; i < 10; i++) {
      			instance = MultiInstance.getInstance();
      			instanceSet.add(instance);
      		}
      		System.out.println("8个实例中,不同的实例有:"+instanceSet.size());   
      	}
      }

      运行结果
      注意:如果在多线程环境下使用,也是要考虑线程安全的。感兴趣的可以自己实现一下。

      java单例模式和线程安全问题怎么解决

      单例模式一定是安全的吗?

      不一定,有很多方法可以破坏单例模式!

      这里举例看一看(我只能举我知道的哈!其他的感兴趣,可以去探究一下!)
      使用反射:这种办法是非常有用的,通过反射即使是私有的属性和方法也可以访问了,因此反射破坏了类的封装性,所以使用反射还是要多多小心。但是反射也有许多其他的用途,这是一项非常有趣的技术(我也只是会一点点)。

      使用反射破坏单例模式测试类

      这里使用的还是前面的 Dog 实体类。注意我这里的**包名:**com。
      所有的类都是在 com包 下面的。

      import java.lang.reflect.Constructor;
      import java.lang.reflect.InvocationTargetException;
      
      public class Client {
      	public static void main(String[] args) throws 
      	ClassNotFoundException, 
      	NoSuchMethodException, 
      	SecurityException, 
      	InstantiationException, 
      	IllegalAccessException, 
      	IllegalArgumentException, 
      	InvocationTargetException {
      	
      		Class clazz = Class.forName("com.Dog");
      		Constructor con = clazz.getDeclaredConstructor();
      		//设置可访问权限
      		con.setAccessible(true);
      		Dog dog1 = (Dog) con.newInstance();
      		Dog dog2 = (Dog) con.newInstance();
      		System.out.println(dog1 == dog2);
      	}
      }

      说明:反射的功能是很强大的,从这里既可以看出来,正是有了反射,才使得Java 语言具有了更多的特色,这也是Java的强大之处。

      使用对象序列化破坏单例模式

      测试实体类:Dog(增加一个对象序列化接口实现)

      import java.io.Serializable;
      //懒汉式
      public class Dog implements Serializable{
      	private static final long serialVersionUID = 1L;
      	
      	private static Dog dog;
      	private String name;
      	private int age;
      	
      	//私有的构造器
      	private Dog() {}
      	
      	public String getName() {
      		return name;
      	}
      
      	public void setName(String name) {
      		this.name = name;
      	}
      
      	public int getAge() {
      		return age;
      	}
      
      	public void setAge(int age) {
      		this.age = age;
      	}
      
      	//静态工厂方法
      	public synchronized static Dog getInstance() {
      		if (dog == null) {
      			dog = new Dog();
      		}
      		return dog;
      	}
      
      	@Override
      	public String toString() {
      		return "Dog [name=" + name + ", age=" + age + "]";
      	}
      }

      对象序列化测试类

      import java.io.ByteArrayInputStream;
      import java.io.ByteArrayOutputStream;
      import java.io.IOException;
      import java.io.ObjectInputStream;
      import java.io.ObjectOutputStream;
      
      public class Client {
      	public static void main(String[] args) throws IOException, ClassNotFoundException {
      		Dog dog1 = Dog.getInstance();
      		dog1.setName("小黑");
      		dog1.setAge(2);
      		System.out.println(dog1.toString());
      		
      		ByteArrayOutputStream bos = new ByteArrayOutputStream();
      		ObjectOutputStream oos = new ObjectOutputStream(bos);
      		oos.writeObject(dog1);
      		
      		
      		ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
      		ObjectInputStream ois = new ObjectInputStream(bis);
      		Dog dog2 = (Dog) ois.readObject();
      		System.out.println(dog2.toString());
      		System.out.println("dog1 == dog2: "+(dog1 == dog2));
      		
      	}
      }

      运行结果

      java单例模式和线程安全问题怎么解决

      说明
      这里可以看出来通过对象序列化(这里也可以说是对象的深拷贝或深克隆),
      同样也可以实现类的实例的不唯一性。这同样也算是破坏了类的封装性。对象序列化和反序列化的过程中,对象的唯一性变了。

      这里具体的原因很复杂,我最近看了点深拷贝的知识,所以只是知其然不知其之所以然。(所以学习是需要不断进行的!加油诸位。)
      这里我贴一下别的经验吧:(感兴趣的可以实现一下!)

      为什么序列化可以破坏单例了?
      答:序列化会通过反射调用无参数的构造方法创建一个新的对象。

      这个东西目前超出了我的能力范围了,但也是去查看源码得出来的,就是序列化(serializable)和反序列化(externalizable)接口的详细情况了。但是有一点,它也是通过反射来做的的,所以可以看出**反射(reflect)**是一种非常强大和危险的技术了。

      本站声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系bd@zhengruan.com
      作者最新文章
      编程开发 Java
      相关文章 更多
      Java单元测试入门:使用JUnit 5编写第一个测试用例
      Java单元测试入门:使用JUnit 5编写第一个测试用例

      面向初学者的JUnit 5实用入门指南。从测试概念、Maven依赖配置,到手把手编写首个单元测试用例及断言验证,助你掌握Java单元测试的核心流程。

      Java变量未初始化报错解析:局部变量与成员变量默认值差异
      Java变量未初始化报错解析:局部变量与成员变量默认值差异

      深入解析Java中局部变量未初始化编译报错与成员变量自动赋予默认值的机制差异,涵盖JVM内存分配、编译期确定性赋值检查与实际排查修复方案。

      Java获取当前时间并格式化输出:DateTimeFormatter与SimpleDateFormat
      Java获取当前时间并格式化输出:DateTimeFormatter与SimpleDateFormat

      详细讲解在Java中获取当前系统时间并进行格式化输出的两种核心方案:基于Java 8全新日期时间API的DateTimeFormatter与传统遗留的SimpleDateFormat,包含完整代码示例、线程安全机制对比及生产选型建议。

      Java中final与static final常量的定义及初始化规则
      Java中final与static final常量的定义及初始化规则

      深入解析Java中final变量与static final常量的定义、内存语义、初始化时机及合法赋值路径,涵盖编译期常量优化与实战避坑指南。

      Java抽象方法与接口方法的区别及选型建议
      Java抽象方法与接口方法的区别及选型建议

      深入对比Java中抽象类抽象方法与接口方法的语法演进、设计意图及运行期差异,并提供清晰的架构选型决策流程与实战范例。

      Java实现HTTP请求与JSON解析的完整代码示例
      Java实现HTTP请求与JSON解析的完整代码示例

      本文提供Java标准库HttpClient发起HTTP GET/POST请求并结合Jackson进行JSON数据反序列化与序列化的完整代码示例与工程实践指南。

      Java递归出现StackOverflowError的原因分析及栈空间优化方案
      Java递归出现StackOverflowError的原因分析及栈空间优化方案

      详细分析Java递归调用出现StackOverflowError的原因,结合代码说明线程栈、栈帧和递归深度之间的关系,并介绍终止条件排查、迭代改写、减少栈帧开销以及通过-Xss调整JVM栈空间的方法。

      Java空指针异常(NPE)的代码排查与修复实例
      Java空指针异常(NPE)的代码排查与修复实例

      针对Java中常见的NullPointerException(NPE),本文从堆栈日志定位、典型代码触发场景、具体修复代码对比到防御体系进行系统讲解,提供实用且易上手的排查与修复方案。

      selenium上传文件教程本地文件路径设置方法
      selenium上传文件教程本地文件路径设置方法

      深入解析Selenium处理文件上传的技术细节,涵盖标准HTML控件的路径直接注入法与非标准控件的系统级模拟方案,提供Windows与Linux环境下的路径设置最佳实践及常见异常排查。

      大数据开发语言选型:Java对比Scala与Python的优缺点及适用场景
      大数据开发语言选型:Java对比Scala与Python的优缺点及适用场景

      针对大数据工程场景,系统对比Java、Scala与Python在执行性能、生态结合、开发效率和维护成本上的核心差异,给出清晰的架构选型依据与落地方案。

      查看更多
      精品专题 更多
      装机必备
      装机必备

      正软商城装机必备专区,精选办公、浏览器、安全防护、影音播放、压缩解压、设计创作和系统工具等电脑常用正版软件,帮助用户快速完成新电脑软件配置。

      Windows
      Windows

      正软商城Windows软件专区,汇集适用于Windows电脑的办公、设计、安全防护、影音播放、开发工具和系统优化软件,提供软件介绍、系统要求、正版授权及购买下载服务。

      PDF教程
      PDF教程

      正软商城PDF教程频道提供PDF编辑、转换、合并、拆分、压缩及格式处理方法,同时介绍常用PDF软件和工具的使用技巧。

      Mac软件 更多
      讯飞智作 网页版
      讯飞智作 网页版
      Windows/ macOS/ Linux

      讯飞智作是一款基于语音合成技术、虚拟人驱动技术与数字人形象声音构建能力的 AI 数字人音视频创作平台。

      火山引擎 网页版
      火山引擎 网页版
      Windows/ macOS/ Linux

      火山引擎是字节跳动旗下企业级云与AI服务平台。聚焦大模型核心能力,依托字节自研豆包多模态大模型,凭借火山方舟一站式平台,提供模型精调、推理部署、智能应用搭建等全链路服务,结合高性能AI算力,助力各行业快速落地大模型数字化与智能化应用。

      Aperty macOS版
      Aperty macOS版
      macOS

      Aperty 是 Skylum 推出的 AI 人像修图软件,支持自然肤质修饰、瑕疵处理、面部与身体调整、预设及批量同步。

      WINDOWS 更多
      讯飞智作 网页版
      讯飞智作 网页版
      Windows/ macOS/ Linux

      讯飞智作是一款基于语音合成技术、虚拟人驱动技术与数字人形象声音构建能力的 AI 数字人音视频创作平台。

      火山引擎 网页版
      火山引擎 网页版
      Windows/ macOS/ Linux

      火山引擎是字节跳动旗下企业级云与AI服务平台。聚焦大模型核心能力,依托字节自研豆包多模态大模型,凭借火山方舟一站式平台,提供模型精调、推理部署、智能应用搭建等全链路服务,结合高性能AI算力,助力各行业快速落地大模型数字化与智能化应用。

      PixTV 网页版
      PixTV 网页版
      Windows

      PixTV 是一款在浏览器中使用的 AI 视觉内容创作平台,提供图片、视频、音频、文字生成、AI 分镜、局部重绘、提示词反推和无限画布等工具,适合短剧、漫剧、广告及社交媒体内容制作。