经典Java面试题学习笔记2

单例模式

什么是Singleton?

  1. Singleton:在Java中即单例设计模式,它是软件开发中最常用的设计模式之一。
  2. 单例模式中,单是唯一的意思,例就是实例。
  3. 单例设计模式,即某个类在整个系统中只能有一个实例对象可被获取和使用的代码模式。
  4. 例如:代表JVM运行环境的Runtime类,在Spring中创建的Bean实例默认都是单例模式存在的。

应用场景

单例模式出现目的是为了保证一个类在系统中只有一个实例,并提供一个访问它的全局访问点。从这点可以看出,单例模式的出现是为了可以保证系统中一个类只有一个实例而且该实例又易于外界访问,从而方便对实例个数的控制并节约系统资源而出现的解决方案。
在下面几个场景中适合使用单例模式:

  1. 有频繁实例化然后销毁的情况,也就是频繁的 new 对象,可以考虑单例模式
  2. 创建对象时耗时过多或者耗资源过多,但又经常用到的对象
  3. 频繁访问 IO 资源的对象,例如数据库连接池或访问本地文件具体

具体例子

  1. 网站在线人数统计
    其实就是全局计数器,也就是说所有用户在相同的时刻获取到的在线人数数量都是一致的。要实现这个需求,计数器就要全局唯一,也就正好可以用单例模式来实现。当然这里不包括分布式场景,因为计数是存在内存中的,并且还要保证线程安全。下面代码是一个简单的计数器实现。
public class Counter {
  
    private static class CounterHolder{
        private static final Counter counter = new Counter();
    }

    private Counter(){
        System.out.println("init...");
    }

    public static final Counter getInstance(){
        return CounterHolder.counter;
    }

    private AtomicLong online = new AtomicLong();

    public long getOnline(){
        return online.get();
    }

    public long add(){
        return online.incrementAndGet();
    }
}  
  1. 配置文件访问类;
    项目中经常需要一些环境相关的配置文件,比如短信通知相关的、邮件相关的。比如 properties 文件,这里就以读取一个properties 文件配置为例,如果你使用的 Spring ,可以用 @PropertySource 注解实现,默认就是单例模式。如果不用单例的话,每次都要 new 对象,每次都要重新读一遍配置文件,很影响性能,如果用单例模式,则只需要读取一遍就好了。以下是文件访问单例类简单实现:
public class SingleProperty {

    private static Properties prop;

    private static class SinglePropertyHolder{
        private static final SingleProperty singleProperty = new SingleProperty();
    }

    /**
    * config.properties 内容是 test.name=kite 
    */
    private SingleProperty(){
        System.out.println("构造函数执行");
        prop = new Properties();
        InputStream stream = SingleProperty.class.getClassLoader()
                .getResourceAsStream("config.properties");
        try {
            prop.load(new InputStreamReader(stream, "utf-8"));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static SingleProperty getInstance(){
        return SinglePropertyHolder.singleProperty;
    }
    
	
    public String getName(){
        return prop.get("test.name").toString();
    }

    public static void main(String[] args){
        SingleProperty singleProperty = SingleProperty.getInstance();
        System.out.println(singleProperty.getName());
    }
}
  1. 数据库连接池的实现,也包括线程池。
    为什么要做池化,是因为新建连接很耗时,如果每次新任务来了,都新建连接,那对性能的影响实在太大。所以一般的做法是在一个应用内维护一个连接池,这样当任务进来时,如果有空闲连接,可以直接拿来用,省去了初始化的开销。所以用单例模式,正好可以实现一个应用内只有一个线程池的存在,所有需要连接的任务,都要从这个连接池来获取连接。如果不使用单例,那么应用内就会出现多个连接池,那也就没什么意义了。如果你使用 Spring 的话,并集成了例如 druid 或者 c3p0 ,这些成熟开源的数据库连接池,一般也都是默认以单例模式实现的。

实现

要点

  1. 该类只能有一个实例——构造器私有化
  2. 必须自行创建这个实例——含有一个该类静态变量来保存这个唯一实例
  3. 必须自行向整个系统提供这个实例——对外提供获取该实例对象的方法
    1)直接暴露
    2)用静态变量的get方法获取

饿汉式

直接创建对象,不管是否需要这个对象,不存在线程安全问题。

直接实例化
public class Singleton1 {

    public static final Singleton1 INSTANCE = new Singleton1();

    private Singleton1() {

    }
}

这种方法简单直观,为了对外提供,将这个单例的静态变量设为public,为了强调是个单例,用final修饰。

枚举式
public enum Singleton2 {
    INSTANCE
}

枚举类型表示该类的对象是有限的几个,当限定为一个时就成了单例。这是最简洁的一种实现方式。
直接实例化和枚举式的获取实例方法类似,可用类似下面方法获取。

	Singleton1 s = Singleton1.INSTANCE;
	Singleton2 s = Singleton2.INSTANCE;
静态代码块
public class Singleton3 {

    public static final Singleton3 INSTANCE;

    static {
        INSTANCE = new Singleton3();
    }

    private Singleton3(){
        
    }
}

以上是最简的实现方式,但是这种实现方式对比直接实例化的方法更复杂。但是这种方法有特殊的应用场景,它可以在实例化的时候进行初始化。

public class Singleton3 {

    public static final Singleton3 INSTANCE;
    private String info;

    static {
        Properties pro = new Properties();
        try {
            pro.load(Singleton3.class.getClassLoader().getResourceAsStream("single.properties"));
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        INSTANCE = new Singleton3(pro.getProperty("info"));
    }

    private Singleton3(String info){
        this.info = info;
    }

    public String getInfo() {
        return info;
    }

    public void setInfo(String info) {
        this.info = info;
    }

    @Override
    public String toString() {
        return "Singleton3{" +
                "info='" + info + '\'' +
                '}';
    }
}

比如说如上所示的,给他加入一个info的属性,并且可以根据properties的配置文件进行读取初始化内容。在读取文件时,通过类加载器去获得类路径下的properties文件,也就是src下的文件。比如我读取的single.properties中的内容如下。

info=ChouDaichi
public class TestSingleton3 {
    public static void main(String[] args) {
        Singleton3 instance = Singleton3.INSTANCE;
        System.out.println(instance);
    }
}

在运行测试代码后,得到的结果如下。

Singleton3{info='ChouDaichi'}

这种方法适合复杂实例化。

小结

这三种饿汉式的单例模式实现,都是通过类加载器调用的时候是线程安全的来保证其线程安全的特性。

懒汉式

懒汉式在创建单例的时候是延时创建的,并不是像饿汉式一样在类加载的时候就创建对应类的单例。

线程不安全
public class Singleton4 {

    private static Singleton4 instance;

    private Singleton4() {
    }

    public static Singleton4 getInstance() {
        if (instance == null) {
            instance = new Singleton4();
        }
        return instance;
    }
}

这种形式的简单实现方法如上所示,只有在未创建实例时调用getInstance()方法的时候才会去创建一个实例,之后再调用该方法时就直接从静态变量中返回这个实例。这种方法在单线程的情况下是不会出现问题的。

public class TestSingleton4 {
    public static void main(String[] args) {
        Singleton4 s1 = Singleton4.getInstance();
        Singleton4 s2 = Singleton4.getInstance();
        System.out.println(s1 == s2);
}

比如在这种单线程的情况下,输出的结果永远为true。但是如果把类改动一下,加入一个让线程sleep的方法,并且试用多线程的方式就有可能出问题。

public class Singleton4 {

    private volatile static Singleton4 instance;

    private Singleton4() {
    }

    public static Singleton4 getInstance() {
        if (instance == null) {
         	try {
            	Thread.sleep(100);
     		} catch (InterruptedException e) {
            	e.printStackTrace();
          	}
      		instance = new Singleton5();
        }
        return instance;
    }
}
public class TestSingleton4 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
    
        Callable<Singleton4> c = new Callable<Singleton4>() {
            @Override
            public Singleton4 call() throws Exception {
                return Singleton4.getInstance();
            }
        };

        ExecutorService es = Executors.newFixedThreadPool(2);
        Future<Singleton4> f1 = es.submit(c);
        Future<Singleton4> f2 = es.submit(c);

        Singleton4 s1 = f1.get();
        Singleton4 s2 = f2.get();

        System.out.println(s1 == s2);

        es.shutdown();
    }
}

在这种情况下,就不能保证输出结果百分之百是true。因为可能当第一个线程在调用的时候判断没有该实例然后线程sleep了100毫秒,第二个线程跑到这一步的时候也判断没有实例,于是乎两者都建立了一个新的实例,但是这两个实例显然不是同一个。为了解决这个问题,出现了下面的方法。

线程安全

上述的一种方法满足不了线程安全的问题,所以可以给它加上锁,就可以解决这个问题。

public class Singleton5 {

    private static Singleton5 instance;

    private Singleton5() {

    }

    public static Singleton5 getInstance() {
        synchronized (Singleton5.class) {
         	if (instance == null) {
         		try {
                  	Thread.sleep(1000);
                } catch (InterruptedException e) {
             		e.printStackTrace();
           		}
                   	instance = new Singleton5();
           	}
        }
        return instance;
    }
}

在这种情况下一般就不会出现线程安全的问题,但是这种方法还有点小问题。就是每次调用获得实例的方法的时候都要去进行锁的判断,大大影响效率,所以可以将其改进。

public class Singleton5 {

    private static Singleton5 instance;

    private Singleton5() {

    }

    public static Singleton5 getInstance() {
        if (instance == null) {
            synchronized (Singleton5.class) {
                if (instance == null) {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    instance = new Singleton5();
                }
            }
        }
        return instance;
    }
}

这样简单的调整一下就可以解决该类获得实例的效率问题。

静态内部类形势
public class Singleton6 {

    private Singleton6() {

    }

    private static class Inner {
        private static final Singleton6 INSTANCE = new Singleton6();
    }

    public static Singleton6 getInstance() {
        return Inner.INSTANCE;
    }
}

这种方法在内部类被加载和初始化的时候,才创建INSTANCE实例对象。静态内部类不会自动随着外部类的加载和初始化而初始化,它是要单独去加载和初始化的。因为是在内部类加载和初始化时候创建的,所以是线程安全的。

小结

第一种不是线程安全的,适用于单线程。而后面两种满足线程安全的要求,适用于多线程。

总结

如果需要饿汉式,枚举形式是最简单的形势。如果需要懒汉式,静态内部类形式最简单。以上的六种方法,饿汉式全部是线程安全的,而懒汉式有一种不是线程安全的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java面试60道笔记是一本针对Java开发岗位的面试集合。本书分为六个部分:基础知识与语法、面向对象、集合类、IO流与多线程、网络编程和数据库基础。每个部分都包括了该领域的常见问和解答,以及一些实际场景中的应用目。 在基础知识与语法部分,包括Java的数据类型、算术运算符、流程控制和异常处理等基本语法知识的问。这些问有助于检验应聘者对Java语言的熟悉程度。 面向对象部分则关注于类、对象、继承、多态和接口等面向对象的概念。这些问旨在考察应聘者对面向对象编程的理解和实践经验。 集合类部分包括了常用的集合类容器,如List、Set和Map等,以及它们的特点和应用场景。这些问可以考察应聘者对Java集合类的熟练掌握程度。 在IO流与多线程部分,问主要涉及Java中的输入输出流和多线程编程。这些问可以考察应聘者对Java的并发编程和IO操作的理解和应用能力。 网络编程部分主要涉及Java中的网络通信和Socket编程等知识。这些问可以考察应聘者对网络编程的了解和实践经验。 最后,数据库基础部分主要涉及Java与数据库交互的知识,包括连接数据库、执行SQL语句和事务管理等内容。这些问可以考察应聘者对数据库操作和数据持久化的了解和应用能力。 总之,Java面试60道笔记是一本综合性的Java面试集,涵盖了基础知识、面向对象、集合类、IO流与多线程、网络编程和数据库基础等方面的内容,有助于应聘者在面试中更好地展示自己的技能和经验。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值