单例模式
B站黑马程序员学习 https://www.bilibili.com/video/BV1Np4y1z7BU?p=22
简介
java中最简单的设计模式之一,属于创建者模式,它提供了一种创建对象的最佳模式。
这种模式设计到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
结构
- 单例类:只能创建一个实例的对象
- 访问类:使用单例类
实现
饿汉式:类加载就会导致该单实例对象被创建
懒汉式:类加载不会导致该单实例对象被创建,而是首次使用该对象时才会创建
饿汉式
1、饿汉式 - 方式1(静态成员变量方式)
说明
该方式再成员位置声明Singleton类型的静态变量,并创建Singleton类的对象instance。instance随着类的加载而创建,如果该对象足够大的话,而一直没有使用就会造成内存的浪费。
代码实现
/*
饿汉式 - 方式1(静态成员变量方式)
*/
public class Singleton {
//1.构造方法私有
private Singleton() {
}
//2.创建对象
private static Singleton instance = new Singleton();
//3.提供一个公共访问方式,让外界获取对象
public static Singleton getInstance() {
return instance;
}
}
测试
/*
测试类
*/
public class Client {
public static void main(String[] args) {
//1.创建两个相同的对象
Singleton instance = Singleton.getInstance();
Singleton instance1 = Singleton.getInstance();
//2.比较是否为同一个对象(内存地址)
System.out.println(instance==instance1); //true
}
}
2、饿汉式 - 方式2(静态代码块方式)
说明
该方式在成员位置声明Singleton类型对象的静态变量,而对象的创建是在静态代码中,也是对类的加载而创建。所以和饿汉式1的方式基本一样,当然该方式也存在内存浪费问题。
代码实现
/*
饿汉式 - 方式2(静态代码块方式)
*/
public class Singleton {
//1.构造方法私有
private Singleton(){
}
//2.声明Singleton类型的变量
private static Singleton instance;
//3.静态代码块中进行赋值
static {
instance=new Singleton();
}
//4.对外提供获取该类对象的方法
public static Singleton getInstance(){
return instance;
}
}
测试
/*
测试类
*/
public class Client {
public static void main(String[] args) {
//1.获取两个单例对象
Singleton instance = Singleton.getInstance();
Singleton instance1 = Singleton.getInstance();
//2.判断两个对象是否为同一个对象
System.out.println(instance == instance1);//true
}
}
懒汉式
1、懒汉式 - 方式1(线程不安全)
代码实现
/*
3、懒汉式 - 方式1(线程不安全)
*/
public class Singleton {
//1.构造方法私有
private Singleton(){
}
//2.声明Singleton类型的变量instance
private static Singleton instance;//只是声明了一个该类的变量,并没有赋值
//3.对外提供访问方式
public static Singleton getInstance(){
//instance = new Singleton();//每次都会创建一个对象,违背单例
//判断instance是否为null,如若为null,创建,否则直接返回
if (instance==null){
//如果有多个线程,都会进入到判断里面
instance=new Singleton();
}
return Singleton.instance;
}
}
2、懒汉式 - 方式2(线程安全)
加上同步锁 synchronized
说明
该方式实现了懒加载效果,同时又解决了线程安全问题,但是在getInstance()方法上添加了synchronized
关键字,导致该方法的执行效果特别低,从上面代码我们可以看出,其实就是在初始化instance的时候才会出现线程安全问题,一旦初始化完成就不存在了。
代码实现
/*
4、懒汉式 - 方式2(线程安全)
*/
public static synchronized Singleton getInstance(){
//instance = new Singleton();//每次都会创建一个对象,违背单例
//判断instance是否为null,如若为null,创建,否则直接返回
if (instance==null){
//如果有多个线程,都会进入到判断里面
instance=new Singleton();
}
return Singleton.instance;
}
3、懒汉式 - 方式3(双重检查锁)
简介
再来讨论下懒汉式中枷锁的问题,对于getInstance()
方法来说,绝大部分的操作是读操作,读操作是线程安全的,所以我们没必要让每个线程必须持有锁才能调用该方法,我们需要调整枷锁的时机,由此也产生了一种新的实现模式:双重检查锁
代码实现
/*
5、懒汉式 - 方式3(双重检查锁)
*/
public class Singleton {
//1.构造方法私有
private Singleton (){
}
//2.声明singleton对象
private static Singleton instance;
//3.对外提供访问方式
public static Singleton getInstance(){
//第一次判断,如果instance不为null,不进入枪锁阶段,直接返回实例
if (instance==null){
synchronized (Singleton.class){
//抢到锁之后再次判断
if (instance==null){
instance=new Singleton();
}
}
}
return instance;
}
}
说明
双重检查锁模式是一种非常好的单例实现模式,**解决了单例、性能、线程安全问题,**上面的双重检查锁看似完美无缺,其实是存在问题,在多线程的情况下,可能会出现空指针问题,出现问题的原因是JVM在实例化对象的时候会进行优化和指令重排序操作。
要解决双重检测锁模式带来空指针异常的问题,只需要使用**volatile
关键字,volatile
**关键字可以保证可见性和有序性。
代码实现
//2.声明singleton对象
private static volatile Singleton instance;
小结
添加volatile
关键字之后的双重检查锁模式是一种比较好的单例实现模式,能够保证在多线程的情况下线程安全也不会有性能问题。
4、懒汉式 - 方式4(静态内部类方式)
简介
静态内部类单例模式中,实例由内部类创建,由于JVM在加载外部类的过程中,是不会加载静态内部类的,只有内部类的属性/方法被调用时才会被加载,并初始化其静态属性。静态属性由于被 static
修饰,保证制备实例化一次,并且严格保证实例化顺序。
代码实现
/*
6、懒汉式 - 方式4(静态内部类方式)
*/
public class Singleton {
//1.构造方法私有
private Singleton(){
}
//2.静态内部类声明并创建对象
private static class SingletonHolder{
//只会创建一次
private static final Singleton INSTANCE=new Singleton();
}
//3.对外提供获取对象的方式
public static Singleton getInstance(){
return SingletonHolder.INSTANCE;
}
}
说明
第一次加载Singleton类时不会初始化INSTANCE,只有第一次调用getInstance()方法时,虚拟机加载SingletonHolder并初始化INSTANCE,这样不仅能确保线程安全,也能保证Singleton类的唯一性。
小结
静态内部类单例模式是一种优秀的单例模式,是开源项目中比较常用的一种单例模式。在没有加任何锁的情况下,保证了多线程下的安全,并且没有任何性能影响和空间的浪费。
枚举方式
简介
枚举类实现单例模式是极力推荐的单例实现模式,因为枚举类型是线程安全的,并且只会装载一次,设计者充分的利用了枚举的这个特性来实现单例模式,枚举的写法非常简单,而且枚举类型是所用单例实现中唯一一种不会被破坏的单例实现模式。
代码实现 枚举方式属于饿汉式方式 不考虑内存时,首选
public enum Singleton {
INSTANCE;
}
测试类
public class Client {
public static void main(String[] args) {
Singleton instance = Singleton.INSTANCE;
Singleton instance1 = Singleton.INSTANCE;
System.out.println(instance1==instance);//true
}
}
存在的问题
破坏单例模式
使上面定义的单例类Singleton可以创建多个对象,枚举方式除外。有两种方式,分别是序列化和反射
序列化反序列化
Singleton类
/*
序列化和反序列化破坏单例模式
*/
public class Singleton implements Serializable {
private Singleton(){
}
private static class SingletonHolder{
private static Singleton INSTANCE=new Singleton();
}
public static Singleton getInstance(){
return SingletonHolder.INSTANCE;
}
}
测试类
public class Client {
public static void main(String[] args) throws IOException, ClassNotFoundException {
//writeObject2File();
readObjectFromFile();
readObjectFromFile();
/*
com.peng.demo07.Singleton@58372a00
com.peng.demo07.Singleton@4dd8dc3 破坏了单例模式
*/
}
//从文件中读取对象
public static void readObjectFromFile() throws IOException, ClassNotFoundException {
//1.创建对象输入流对象
ObjectInputStream ois=new ObjectInputStream(new FileInputStream("./a.txt"));
//2.读取对象
Singleton instance= (Singleton) ois.readObject();
//输出对象
System.out.println(instance);
//3.释放资源
}
//向文件中写入对象
public static void writeObject2File() throws IOException {
//1.获取singleton对象
Singleton singleton= Singleton.getInstance();
//2.创建对象输出流
ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("./a.txt"));
//3.写对象
oos.writeObject(singleton);
//4.释放资源
oos.close();
}
}
解决方法:在Singleton类中添加一个
readResolve
方法
如果是定义了readResolve
方法,就返回true,否则false,
如果是True,就会执行desc.invokeReadResolve(obj)
,如果没有定义,就会重新new一个新的对象。
反射
Singleto类
/*
反射破坏单例模式
*/
public class Singleton {
private Singleton() {
}
private static class SingletonHolder {
private static Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance(){
return SingletonHolder.INSTANCE;
}
}
测试类
public class Client {
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
//1.获取singleton的字节码对象
Class clazz = Singleton.class;
//2.获取无参构造方法对象
Constructor constructor = clazz.getDeclaredConstructor(); //只是拿到了无参构造并无访问权限
//3.取消访问检查
constructor.setAccessible(true);//获取权限
//4.创建Singleton对象
Singleton instance = (Singleton) constructor.newInstance();
Singleton instance1 = (Singleton) constructor.newInstance();
//5.比较是否为同一个对象
System.out.println(instance == instance1); //false 破坏了单例模式
}
}
解决方法: 声明变量,在构造方法中添加一层判断
/*
反射破坏单例模式
*/
public class Singleton {
//声明变量
private static boolean flag = false;
private Singleton() {
//防止多线程
synchronized (Singleton.class) {
//判断flag的值是否为true,如果是true,说明非第一次访问,抛出异常,否则放行
if (flag) {
throw new RuntimeException("不能创建多个对象");
}
//将flag设置为true
flag = true;
}
}
private static class SingletonHolder {
private static Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
JDK源码解析-Runtime
Runtime类就是使用的单例设计模式
源码: 饿汉式(静态属性)
public class Runtime {
//静态私有变量
private static Runtime currentRuntime = new Runtime();
//对外提供获取对象的方法
public static Runtime getRuntime() {
return currentRuntime;
}
//构造方法私有
private Runtime() {}
}
测试使用
public class RuntimeDemo {
public static void main(String[] args) throws IOException {
//获取runtime对象
Runtime runtime = Runtime.getRuntime();
//调用其中的方法(exec),需要命令行参数
Process process = runtime.exec("ipconfig");
//调取process对象的获取输入流的方法
InputStream inputStream = process.getInputStream();
byte[] bytes = new byte[1024*1024*100];
//读取数据
int len=inputStream.read(bytes);//返回读到的字节的个数
//将字节数组转换为字符串输出到控制台
String s = new String(bytes, 0, len, "GBK");
System.out.println(s);
}
}