为什么百度不收录我的网站,创意视频制作app,重庆网络科技公司有哪些,中国建筑设计网官网目录
前言
正文
1.立即加载/饿汉模式
2.延迟加载/懒汉模式
1.延迟加载/懒汉模式解析
2.延迟加载/懒汉模式的缺点
3.延迟加载/懒汉模式的解决方案
#xff08;1#xff09;声明 synchronized 关键字
#xff08;2#xff09;尝试同步代码块
#xff08;31声明 synchronized 关键字
2尝试同步代码块
3针对某些重要的代码进行单独的同步
4使用 DCL 双检查锁机制
5双检查锁 DCL 使用 volatile 的必要性
3.使用静态内置类实现单例模式
4.序列化和反序列化的单例模式实现
5.使用 static 静态代码块实现单例模式
6.使用 enum 枚举类型实现单例模式
7.完善使用 enum 枚举类实现单例模式
总结 前言 在单例模式与多线程技术相结合的过程中我们能发现许多以前从未考虑过的问题。这些不良的程序设计如果应用在商业项目中将会带来非常大的麻烦。线程与某些技术相结合时我们需要考虑的事情会更多。总的来说在本节我们只需要考虑一件事那就是如何使单例模式与多线程结合时是安全的、正确的。 正文 在标准的23个设计模式中单例模式在应用中是比较常见的。但多数常规的该模式教学并没有结合多线程技术进行介绍这就造成在使用结合多线程的单例模式时会出现一些意外。这样的代码如果在生产环境中出现异常有可能造成灾难性的后果。 1.立即加载/饿汉模式 什么是立即加载立即加载就是使用类的时候已经将对象创建完毕。常见的实现办法就是 new 实例化。从中文的语境上来看就是 “着急” “急迫” 的含义所以也被称为 “饿汉模式”。 实现代码
public class MyObject {//立即加载方式 饿汉模式private static MyObject myObject new MyObject();private MyObject() {}public static MyObject getInstance(){return myObject;}}public class MyThread extends Thread{Overridepublic void run() {System.out.println(MyObject.getInstance().hashCode());}
}public class Run {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() 方法没有同步所以有可能出现非线程安全问题比如出现如下代码
public class MyObject {//立即加载方式 饿汉模式private static MyObject myObject new MyObject();private MyObject() {}private static String username;private static String password;public static MyObject getInstance(){username 从不同的服务器取出值有可能不一样并赋值;password 从不同的服务器取出值有可能不一样并赋值;//上面的赋值并没有被同步所以极易出现非线程安全问题导致变量值被覆盖。return myObject;}}
2.延迟加载/懒汉模式 什么是延迟加载》延迟加载就是调用 get() 方法时实例才被工厂创建。常见的实现办法就是在 get() 方法中进行 new 实例化。 延迟加载从中文的预警来看是 “缓慢” “不急迫” 的含义所以也被称为 “懒汉模式”。 1.延迟加载/懒汉模式解析
实现代码
public class MyObject {private static MyObject myObject;public MyObject() {}public static MyObject getInstance(){//延迟加载if (myObject ! null){}else {myObject new MyObject();}return myObject;}
}public class MyThread extends Thread{Overridepublic void run() {System.out.println(MyObject.getInstance().hashCode());}
}public class Run {public static void main(String[] args) {MyThread t1 new MyThread();t1.start();MyThread t2 new MyThread();t2.start();}
}程序运行结果如图 此代码虽然取得一个对象的实例但在多线程环境中会出现取出多个实例的情况与单例模式的初衷是违背的。
2.延迟加载/懒汉模式的缺点 前面两个实验虽然使用了 立即加载 和 延迟加载 实现了单例模式但在多线程环境中延迟加载 示例中的代码完全是错误的跟本不能保持单例的状态。下面来看如何在多线程环境中结合错误的单例模式创建出多个实例的。 public class MyObject {private static MyObject myObject;private MyObject(){}public static MyObject getInstance(){try {if (myObject ! null){}else {//模拟创建对象之前做一些准备工作Thread.sleep(3000);myObject new MyObject();}} catch (InterruptedException e) {e.printStackTrace();}return myObject;}}public class MyThread extends Thread{Overridepublic void run() {System.out.println(MyObject.getInstance().hashCode());}
}public class Run {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 个对象并不是单例的这就是 错误的单例模式,如何解决呢下面看一下解决方案。
3.延迟加载/懒汉模式的解决方案
1声明 synchronized 关键字 既然多个线程可以同时进入 getInstance() 方法我们只需要对 getInstance() 方法声明 synchronized 关键字即可。 public class MyObject {private static MyObject myObject;private MyObject(){}//设置同步方法效率太低//整个方法被上锁synchronized public static MyObject getInstance(){try {if (myObject ! null){}else {//模拟创建对象之前做一些准备工作Thread.sleep(3000);myObject new MyObject();}} catch (InterruptedException e) {e.printStackTrace();}return myObject;}
}public class MyThread extends Thread{Overridepublic void run() {System.out.println(MyObject.getInstance().hashCode());}
}public class Run {public static void main(String[] args) {MyThread t1 new MyThread();MyThread t2 new MyThread();MyThread t3 new MyThread();t1.start();t2.start();t3.start();}
}
运行结果如图 此方法在加入同步 synchronized 关键字后得到相同实例的对象但运行效率非常低。下一个线程想要取得对象必须等上一个线程释放完锁之后才可以执行。那换成同步代码块可以解决吗
2尝试同步代码块
创建测试用例
public class MyObject {private static MyObject myObject;private MyObject(){}//设置同步方法效率太低//整个方法被上锁public static MyObject getInstance(){try {//与同步方法等同//效率一样很低并不能减少锁的粒度// 已经是最小的范围了必须要在if判断前加锁不然进入else还是会创建多个对象// 全部代码同步运行synchronized (MyObject.class) {if (myObject ! null){}else {//模拟创建对象之前做一些准备工作Thread.sleep(3000);myObject new MyObject();}}} catch (InterruptedException e) {e.printStackTrace();}return myObject;}
}public class MyThread extends Thread{Overridepublic void run() {System.out.println(MyObject.getInstance().hashCode());}
}
public class Run {public static void main(String[] args) {MyThread t1 new MyThread();MyThread t2 new MyThread();MyThread t3 new MyThread();t1.start();t2.start();t3.start();//此版本代码虽然是正确的//但全部代码都是同步的这样做也有损效率}
}运行结果如图 此方法在加入同步 synchronized 语句块后得到相同实例的对象但运行效率也非常低和synchronized 同步方法一样是同步运行的。下面继续更改代码尝试解决这个问题。
3针对某些重要的代码进行单独的同步 同步代码块可以仅针对某些重要的代码进行单独的同步这可以大幅度提升效率 。 创建代码如下
public class MyObject {private static MyObject myObject;private MyObject(){}public static MyObject getInstance(){try {if (myObject ! null){}else {//模拟创建对象之前做一些准备工作Thread.sleep(3000);//使用部分代码被上锁//但还是有非线程安全问题//多次创建 MyObject 类的对象结果并不是单例synchronized (MyObject.class) {myObject new MyObject();}}} catch (InterruptedException e) {e.printStackTrace();}return myObject;}
}public class MyThread extends Thread{Overridepublic void run() {System.out.println(MyObject.getInstance().hashCode());}
}public class Run {public static void main(String[] args) {MyThread t1 new MyThread();MyThread t2 new MyThread();MyThread t3 new MyThread();t1.start();t2.start();t3.start();}
}
程序结果如图 此方法使同步 synchronized 语句块只对实例化对象的关键代码进行同步。从语句的结构上讲运行的效率的确得到了提升 但遇到多线程情况还是无法得到同一个实例对象。到底如何解决懒汉模式下的多线程情况呢
4使用 DCL 双检查锁机制 下面使用 DCL 双重检查锁机制来实现多线程环境中的延迟加载单例模式。 第一次检查无锁操作: 检查实例是否已经被创建如果已经被创建则直接返回实例可以减少不必要的锁竞争即每次都进入synchronized中第二次检查锁内操作: 如果第一次检查发现实例尚未创建代码会进入一个同步块但在创建实例之前会再次检查实例是否被创建。这是必要的因为在当前线程进入同步块之前可能有另一个线程已经创建了实例。 性能优化通过减少同步的使用DCL减少了不必要的性能开销因为实例一旦被创建后就不再需要同步。线程安全通过同步块确保在实例未初始化时只有一个线程能创建单例实例保持了单例的线程安全约定。资源利用最优化由于同步只在实际需要时才会发生因此在资源利用上比始终同步要有效得多。 要注意的是在Java中使用DCL的时候还需要考虑Java内存模型的因素。在多线程环境下为了确保DCL正确地工作单例对象的引用需要被声明为 volatile这样可以防止指令重排序可能导致的DCL失效问题。 public class MyObject {private volatile static MyObject myObject;private MyObject(){}//设置同步方法效率太低//整个方法被上锁public static MyObject getInstance(){try {if (myObject ! null){}else {//模拟创建对象之前做一些准备工作Thread.sleep(3000);synchronized (MyObject.class) {if (myObjectnull){myObject new MyObject();}}}} catch (InterruptedException e) {e.printStackTrace();}return myObject;}//此版本的代码称为//双重检查 Double-Check
} 使用 volatile 修饰变量 myObject 使该变量在多个线程间可见另外禁止 myObect new MyObject() 代码重排序。myObject new MyObject(); 代码包含 3 个步骤。 memory allocate() //分配对象的内存空间ctorInstance(memory); //初始化对象myObject memory; //设置 instance 指向刚分配的内存地址 JIT 编译器有可能将这三个步骤重排序成。 memory allocate() //分配对象的内存空间myObject memory; //设置 instance 指向刚分配的内存地址ctorInstance(memory); //初始化对象 这时构造方法虽然还没有执行但 myObject 对象已具有内存地址即值不是 null。当访问 myObject 对象中的值时是当前声明数据类型的默认值此知识点在后面的章节中有讲解。 创建线程类的代码如下
public class MyThread extends Thread{Overridepublic void run() {System.out.println(MyObject.getInstance().hashCode());}
}
创建运行类的代码如下
public class Run {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 使用 volatile 的必要性 前面介绍了 myObject new MyObject() 代码中的 3 个步骤会发生重排序导致取得实例变量的值不是构造方法初始化后的值。下面开始验证。 创建测试用例
package org.example.singleton;import java.util.Random;
import java.util.concurrent.CountDownLatch;public class dcl_and_volatile {static class OneInstanceService {public int i_am_has_state 0;private volatile static OneInstanceService test;public OneInstanceService() {this.i_am_has_state new Random().nextInt(200) 1;}public static OneInstanceService getTest1() {if (test null) {synchronized (OneInstanceService.class) {if (test null) {test new OneInstanceService();}}}return test;}public static void reset() {test null;}}public static void main(String[] args) throws InterruptedException {for (; ; ) {//允许一个或多个线程等待直到在其他线程中执行的一组操作完成的同步辅助。CountDownLatch latch new CountDownLatch(1);CountDownLatch end new CountDownLatch(100);for (int i 0; i 100; i) {Thread t1 new Thread() {Overridepublic void run() {try {//导致当前线程等到锁存器计数到零除非线程是 interrupted 。//创建 100 个线程在这里等待latch.await();OneInstanceService one OneInstanceService.getTest1();if (one.i_am_has_state 0) {System.out.println(one.i_am_has_state 0 进程结束);System.exit(0);}//减少锁存器的计数如果计数达到零释放所有等待的线程。end.countDown();} catch (InterruptedException e) {e.printStackTrace();}}};t1.start();}//循环完毕创建结束减掉计数1线程被唤醒开始执行latch.countDown();//等待计数为0,也就是100个线程执行完成end.await();//重置OneInstanceService.reset();}}
}程序在运行时添加 VM 参数 -server 会更容易获得预期的结果运行后控制台结果如图 说明 myObject new myObject() 确实发生了重排序。
更改代码
static class OneInstanceService{public int i_am_has_state 0;private volatile static OneInstanceService test;public OneInstanceService() {this.i_am_has_state new Random().nextInt(200)1;}public static OneInstanceService getTest1(){if (test null){synchronized (OneInstanceService.class){if (test null){test new OneInstanceService();}}}return test;}public static void reset(){test null;}}
程序运行后不再打印任何信息说明禁止重排序后实例变量 i_am_has_state 永远不是 0 了。也就是说步骤 A 开辟空间 B 来执行构造方法 C在赋值代码中插入屏障 防止 B 跑到 C 的后面这样执行顺序永远是 ABC 而且使用 volatile 还保证了变量的值在多个线程间可见。
3.使用静态内置类实现单例模式 DCL 可以解决多线程单例模式的非线程安全问题。我们还可以使用其他办法达到同样的效果。 创建新的测试用例
public class MyObject {private static class MyobjectHandler {private static MyObject myObject new MyObject();}private MyObject() {}public static MyObject getInstance() {return MyobjectHandler.myObject;}
}public class MyThread extends Thread{Overridepublic void run() {System.out.println(MyObject.getInstance().hashCode());}
}public class Run {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.序列化和反序列化的单例模式实现 如果将单例对象进行序列化使用默认的反序列行为取出对象是多例的。 创建测试用例
//实体类代码
public class Userinfo {
}//创建类 MyObject.java
import java.io.ObjectStreamException;
import java.io.Serializable;public class MyObject implements Serializable {private static final long serialVersionUID 888L;public static Userinfo userinfo new Userinfo();private static MyObject myObject new MyObject();private MyObject() {}public static MyObject getInstance() {return myObject;}/*protected Object readResolve() throws ObjectStreamException {System.out.println(调用了 readResolve方法);return MyObject.myObject;}*/
}方法 protected Object readResolve() 的作用是反序列化时不创建新的 MyObject 对象而是复用原有的 MyObject 对象。 创建业务类代码
import java.io.*;public class SaveAndRead {public static void main(String[] args) {try {MyObject myObject MyObject.getInstance();System.out.println(序列化 -myObjectmyObject.hashCode() userinfomyObject.userinfo.hashCode());FileOutputStream fosRef new FileOutputStream(new File(myObjectFile.txt));ObjectOutput oosRef new ObjectOutputStream(fosRef);oosRef.writeObject(myObject);oosRef.close();fosRef.close();} catch (FileNotFoundException e) {e.printStackTrace();} catch (IOException e) {e.printStackTrace();}try {FileInputStream fisRef new FileInputStream(new File(myObjectFile.txt));ObjectInput iosRef new ObjectInputStream(fisRef);MyObject myObject (MyObject) iosRef.readObject();iosRef.close();fisRef.close();System.out.println( 序列化 -myObjectmyObject.hashCode() userinfomyObject.userinfo.hashCode());} catch (FileNotFoundException e) {e.printStackTrace();} catch (IOException e) {e.printStackTrace();} catch (ClassNotFoundException e) {e.printStackTrace();}}
}
程序运行结果如图 从打印结果可以分析出在反序列化时创建新的 MyObject 对象但 Userinfo 对象得到复用因为 hashcode 是同一个 1163157884 。为了实现 MyObject 在内存中一直呈单例效果我们可以在反序列化中使用 readResolve() 方法对原有的 MyObject 对象进行复用
protected Object readResolve() throws ObjectStreamException {System.out.println(调用了 readResolve方法);return MyObject.myObject;}
程序运行结果如图 方法 protected Object readResolve() 的作用是在反序列化时不创建新的 MyObject 对象而是复用 JVM 内存中原有的 MyObject 单例对象即 Userinfo 对象被复用这就实现了对 MyObject 序列化与反序列化时保持单例性。
注意如果将序列化和反序列化操作分别放入两个 class反序列化时会产生新的 MyObject 对象。放在 2 个 class 类中分别执行其实相当于创建了 2 个 JVM 虚拟机每个虚拟机里有 1 个 MyObject 对象。我们想要实现的是在 1 个 JVM 虚拟机中进行序列化与反序列化时保持 MyObject 单例性而不是创建 2 个 JVM 虚拟机。 补充 在Java中对象的序列化是将对象的状态state序列化为字节流而不是重新创建对象。在序列化过程中对象的引用会被保存下来而不是对象本身。反序列化时根据保存的引用创建新的对象并将序列化的状态恢复到新对象中。 在这段代码中userinfo对象作为myObject对象的成员变量被序列化时也一起序列化了。反序列化时根据之前的引用创建新的myObject对象并将序列化的userinfo对象的状态恢复到新对象中。因此userinfo对象并没有被重新创建而是在序列化和反序列化过程中被复原了状态。 5.使用 static 静态代码块实现单例模式 静态代码块中的代码在使用类的时候就已经执行所以我们可以应用静态代码块的这个特性实现单例模式。 public class MyObject {private static MyObject instance null;private MyObject(){}static {instance new MyObject();}public static MyObject getInstance(){return instance;}
}public class MyThread extends Thread{Overridepublic void run() {for (int i 0; i 5; i) {System.out.println(MyObject.getInstance().hashCode());}}
}public class Run {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.使用 enum 枚举类型实现单例模式 枚举 enum 和静态代码块的特性相似。在使用枚举类时构造方法会被自动调用。我们也可以应用这个特性实现单例模式。 import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;public enum MyObject {connectionFactory;private Connection connection;private MyObject() {try {System.out.println(调用了 MyObject 的构造器);String url jdbc:mysql://localhost:3306/spring_boot;String username root;String password 123456;String driverName com.mysql.cj.jdbc.Driver;Class.forName(driverName);connection DriverManager.getConnection(url,username,password);} catch (ClassNotFoundException e) {throw new RuntimeException(e);} catch (SQLException e) {e.printStackTrace();}}public Connection getConnection(){return connection;}
}public class MyThread extends Thread{Overridepublic void run() {for (int i 0; i 5; i) {System.out.println(MyObject.connectionFactory.getConnection().hashCode());}}
}public class Run {public static void main(String[] args) {MyThread t1 new MyThread();MyThread t2 new MyThread();MyThread t3 new MyThread();t1.start();t2.start();t3.start();}
}
程序运行结果如图 7.完善使用 enum 枚举类实现单例模式
修改 MyObject.java
public class MyObject {public enum MyEnumSingleton{connectionFactory;private Connection connection;private MyEnumSingleton() {try {System.out.println(创建了 MyObject 对象);String url jdbc:mysql://localhost:3306/spring_boot;String username root;String password 123456;String driverName com.mysql.cj.jdbc.Driver;Class.forName(driverName);connection DriverManager.getConnection(url,username,password);} catch (ClassNotFoundException e) {throw new RuntimeException(e);} catch (SQLException e) {e.printStackTrace();}}private Connection getConnection(){return connection;}}public static Connection getConnection(){return MyEnumSingleton.connectionFactory.getConnection();}}
更改 MyThread.java 类的代码
public class MyThread extends Thread{Overridepublic void run() {for (int i 0; i 5; i) {System.out.println(MyObject.getConnection().hashCode());}}
}
运行结果如图 总结 加油