目录
单例模式、多实例模式、和线程安全
单例模式
懒汉式和饿汉式对比
多线程模式下的安全性
多实例模式
首页 Java java教程 java单例模式和线程安全问题怎么解决

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

May 12, 2023 pm 11:07 PM
java

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

    单例模式

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

    分类: 懒汉式、饿汉式

    为什么需要单例模式?

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

    实现思路:

    为了防止客户端利用构造器创建多个对象,将构造方法声明为 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<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&#39;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<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());   
    	}
    }
    登录后复制

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

    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)**是一种非常强大和危险的技术了。

    以上是java单例模式和线程安全问题怎么解决的详细内容。更多信息请关注PHP中文网其他相关文章!

    本站声明
    本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn

    热AI工具

    Undresser.AI Undress

    Undresser.AI Undress

    人工智能驱动的应用程序,用于创建逼真的裸体照片

    AI Clothes Remover

    AI Clothes Remover

    用于从照片中去除衣服的在线人工智能工具。

    Undress AI Tool

    Undress AI Tool

    免费脱衣服图片

    Clothoff.io

    Clothoff.io

    AI脱衣机

    AI Hentai Generator

    AI Hentai Generator

    免费生成ai无尽的。

    热门文章

    R.E.P.O.能量晶体解释及其做什么(黄色晶体)
    1 个月前 By 尊渡假赌尊渡假赌尊渡假赌
    R.E.P.O.最佳图形设置
    1 个月前 By 尊渡假赌尊渡假赌尊渡假赌
    威尔R.E.P.O.有交叉游戏吗?
    1 个月前 By 尊渡假赌尊渡假赌尊渡假赌

    热工具

    记事本++7.3.1

    记事本++7.3.1

    好用且免费的代码编辑器

    SublimeText3汉化版

    SublimeText3汉化版

    中文版,非常好用

    禅工作室 13.0.1

    禅工作室 13.0.1

    功能强大的PHP集成开发环境

    Dreamweaver CS6

    Dreamweaver CS6

    视觉化网页开发工具

    SublimeText3 Mac版

    SublimeText3 Mac版

    神级代码编辑软件(SublimeText3)

    Java 中的完美数 Java 中的完美数 Aug 30, 2024 pm 04:28 PM

    Java 完美数指南。这里我们讨论定义,如何在 Java 中检查完美数?,示例和代码实现。

    Java中的Weka Java中的Weka Aug 30, 2024 pm 04:28 PM

    Java 版 Weka 指南。这里我们通过示例讨论简介、如何使用weka java、平台类型和优点。

    Java 中的史密斯数 Java 中的史密斯数 Aug 30, 2024 pm 04:28 PM

    Java 史密斯数指南。这里我们讨论定义,如何在Java中检查史密斯号?带有代码实现的示例。

    Java Spring 面试题 Java Spring 面试题 Aug 30, 2024 pm 04:29 PM

    在本文中,我们保留了最常被问到的 Java Spring 面试问题及其详细答案。这样你就可以顺利通过面试。

    突破或从Java 8流返回? 突破或从Java 8流返回? Feb 07, 2025 pm 12:09 PM

    Java 8引入了Stream API,提供了一种强大且表达力丰富的处理数据集合的方式。然而,使用Stream时,一个常见问题是:如何从forEach操作中中断或返回? 传统循环允许提前中断或返回,但Stream的forEach方法并不直接支持这种方式。本文将解释原因,并探讨在Stream处理系统中实现提前终止的替代方法。 延伸阅读: Java Stream API改进 理解Stream forEach forEach方法是一个终端操作,它对Stream中的每个元素执行一个操作。它的设计意图是处

    Java 中的时间戳至今 Java 中的时间戳至今 Aug 30, 2024 pm 04:28 PM

    Java 中的时间戳到日期指南。这里我们还结合示例讨论了介绍以及如何在java中将时间戳转换为日期。

    Java程序查找胶囊的体积 Java程序查找胶囊的体积 Feb 07, 2025 am 11:37 AM

    胶囊是一种三维几何图形,由一个圆柱体和两端各一个半球体组成。胶囊的体积可以通过将圆柱体的体积和两端半球体的体积相加来计算。本教程将讨论如何使用不同的方法在Java中计算给定胶囊的体积。 胶囊体积公式 胶囊体积的公式如下: 胶囊体积 = 圆柱体体积 两个半球体体积 其中, r: 半球体的半径。 h: 圆柱体的高度(不包括半球体)。 例子 1 输入 半径 = 5 单位 高度 = 10 单位 输出 体积 = 1570.8 立方单位 解释 使用公式计算体积: 体积 = π × r2 × h (4

    如何在Spring Tool Suite中运行第一个春季启动应用程序? 如何在Spring Tool Suite中运行第一个春季启动应用程序? Feb 07, 2025 pm 12:11 PM

    Spring Boot简化了可靠,可扩展和生产就绪的Java应用的创建,从而彻底改变了Java开发。 它的“惯例惯例”方法(春季生态系统固有的惯例),最小化手动设置

    See all articles