浅谈单例模式和线程安全问题
作者:CrazyDragon_King 发布时间:2023-11-25 06:27:34
单例模式、多实例模式、和线程安全
单例模式
单例模式是指确保一个类仅有一个唯一的实例,并且提供了一个全局的访问点。
分类: 懒汉式、饿汉式
为什么需要单例模式?
再某些特殊的情况下,存在一个类仅能用来产生一个唯一对象的必要性。例如:打印机室有许多打印机,但是它的打印管理系统只有一个打印任务控制对象,该对象管理打印排队并分配打印任务给各个打印机。单例模式正是为了解决这样的需求而产生的。
实现思路:
为了防止客户端利用构造器创建多个对象,将构造方法声明为 private 类型。但这样会使得这个类不可用,所以必须提供一个可以获得实例的静态方法,通常称为 getInstance 方法, 该方法返回一个实例。这个方法必须是静态的,因为静态方法是根据类名调用的,否则也是无法使用的。
类图:懒汉式
类图:饿汉式
先来看一个简单的例子:
测试单例类: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));
}
}
运行结果
懒汉式和饿汉式对比
创建区别
懒汉式是在第一次调用静态方法 getInstance() 时创建单例对象。
饿汉式是在类加载时创建单例对象,即在声明静态单例对象时实例化单例类。
线程安全
懒汉式是线程不安全的,而饿汉式是线程安全的(下面会测试)。
资源占用
懒汉式是等到使用时才会创建,而饿汉式是在类加载时创建。所以懒汉式没有饿汉式快,但是饿汉式比较占用资源,如果一直不使用,会很占据资源。
多线程模式下的安全性
多线程类
import java.util.HashSet;
import java.util.Set;
public class DogThread extends Thread{
private Dog dog;
private Set<Dog> 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());
}
}
运行结果
注意:多线程的结果是很难预测的,这里涉及线程的竞争,可能多次运行结果是一样的(多次一样并不代表是绝对正确),但是只要多次测试,就能看到不一样的结果。
说明
这里我使用一点集合的技巧,利用 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<MultiInstance> 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());
}
}
运行结果
注意:如果在多线程环境下使用,也是要考虑线程安全的。感兴趣的可以自己实现一下。
单例模式一定是安全的吗?
不一定,有很多方法可以破坏单例模式!
这里举例看一看(我只能举我知道的哈!其他的感兴趣,可以去探究一下!)
使用反射:这种办法是非常有用的,通过反射即使是私有的属性和方法也可以访问了,因此反射破坏了类的封装性,所以使用反射还是要多多小心。但是反射也有许多其他的用途,这是一项非常有趣的技术(我也只是会一点点)。
使用反射破坏单例模式测试类
这里使用的还是前面的 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));
}
}
运行结果
说明
这里可以看出来通过对象序列化(这里也可以说是对象的深拷贝或深克隆),
同样也可以实现类的实例的不唯一性。这同样也算是破坏了类的封装性。对象序列化和反序列化的过程中,对象的唯一性变了。
这里具体的原因很复杂,我最近看了点深拷贝的知识,所以只是知其然不知其之所以然。(所以学习是需要不断进行的!加油诸位。)
这里我贴一下别的经验吧:(感兴趣的可以实现一下!)
为什么序列化可以破坏单例了?
答:序列化会通过反射调用无参数的构造方法创建一个新的对象。
这个东西目前超出了我的能力范围了,但也是去查看源码得出来的,就是序列化(serializable)和反序列化(externalizable)接口的详细情况了。但是有一点,它也是通过反射来做的的,所以可以看出**反射(reflect)**是一种非常强大和危险的技术了。
来源:https://blog.csdn.net/qq_40734247/article/details/102884440
猜你喜欢
- 写在前面在平时的开发之中,我们需要对于数据加载的情况进行展示:空数据网络异常加载中等等情况现在设置页面状态的方式有多种,由于笔者近期一直在使
- Handler、Message、Loopler、MessageQueen首先看一下我们平常使用Handler的一个最常见用法。Handler
- 要求环境信息:WIN2008SERVER 开发工具:VS2015 开发语言:C#要求: 1.点击同步数据后接口获取数
- 一、日志1、配置日志级别日志记录器(Logger)的行为是分等级的。如下表所示:分为:OFF、FATAL、ERROR、WARN、INFO、D
- 这次我们来说一下hibernate的层次设计,层次设计也就是实体之间的继承关系的设计。 也许这样比较抽象,我们直接看例子。&nbs
- 一、什么是 websocket 接口使用 websocket 建立长连接,服务端和客户端可以互相通信,服务端只要有数据更新,就可以主动推给客
- 1.概述在实际开发过程中,我们经常需要调用对方提供的接口或测试自己写的接口是否合适。很多项目都会封装规定好本身项目的接口规范,所以大多数需要
- 这里主要是总结一下如何监听有未接来电的问题 1.1 使用广播 * BrocastReceiver实现思路 : 静态注册监听and
- Java本身都是值传递式的调用,对于对象传递的是地址值。给地址值重新赋值等于重新指向,不会影响外层。而且这里Integer对象也有特殊性。其
- 目录介绍Version 1 - 非线程安全Version 2 - 简单的线程安全Version 4 - 不完全懒汉式,但不加锁的线程安全Ve
- 用Stopwatch分段监控了一下,发现耗时最多的函数是SaveToExcel此函数中遍列所有数据行,通过Replace替换标签生成Exce
- 1、dose not point to a valid jvm installation出错问题按照以下方法设置一定可以不会出现这个错误。我
- 本文实例为大家分享了Java Web实现简易图书管理系统的具体代码,供大家参考,具体内容如下前言首先实现的是用户的登录注册,注册成功后自动跳
- 一、项目运行环境配置:Jdk1.8 + Tomcat8.5 + mysql + Eclispe(IntelliJ IDEA,Eclispe,
- 什么是线程池线程池(thread pool)是一种线程使用模式。线程过多或者频繁创建和销毁线程会带来调度开销,进而影响缓存局部性和整体性能。
- • 创建目录和文件1、通过Path类的Combine方法可以合并路径。string activeDir = @"C:\myDir&
- 本文实例讲述了java实现word文档转pdf并添加水印的方法。分享给大家供大家参考,具体如下:前段时间,项目需要自动生成word文档,用W
- 引言最近在工作中结合线程池使用 InheritableThreadLocal 出现了获取线程变量“错误&rdqu
- 深入理解IOC思想spring本质就在于将对象全部交由给spring容器创建和管理,由容器控制对象的整个生命周期、核心就是IOC控制反转和A
- 异常算术异常类:ArithmeticExecption空指针异常类:NullPointerException类型强制转换异常:ClassCa