单例模式

确保一个类只有一个实例,而且自动实例化并向整个系统提供这个实例。

实现

饿汉式

很简单。

  1. 将构造函数设置为私有的,防止外界new出该类的实例,从而失去了单例的意义。
  2. 设置类的私有静态变量,同时新建单例对象。
  3. 添加共有静态方法获取该单例。

该种方法的缺点是在类加载时就进行实例化,但是相较于其简单易用来说,这点缺点个人认为影响不大。

package com.mengyunzhi;

/**
 * @author zhangxishuo on 2018/6/18
 */
public class Singleton {

    private static Singleton instance = new Singleton();

    private Singleton() {

    }

    public static Singleton getInstance() {
        return instance;
    }
}

这种实现的单例模式是最简单的,同时多个线程操作该单例时也不会有问题。

package com.mengyunzhi;

public class Main {

    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            Singleton singleton1 = Singleton.getInstance();
            System.out.println("Singleton1:" + singleton1);
        });
        Thread thread2 = new Thread(() -> {
            Singleton singleton2 = Singleton.getInstance();
            System.out.println("Singleton2:" + singleton2);
        });
        thread1.start();
        thread2.start();
    }
}

clipboard.png

注:打印时调用toString方法,因为没有重写toString,调用Object类中的toString,所以打印该对象的类名加哈希值。

clipboard.png

我们看到控制台中打印的两个对象地址都是89ae60d,表示同一块内存,即表示多线程时该实现方法仍能实现单例。

线程竞争

我们调用的顺序明明是thread1start,然后thread2start,但是为什么控制台打印的顺序却是单例2和单例1呢?

这两个线程会竞争处理器的资源,这里打印的顺序是单例2、单例1,说明处理器处执行线程时,先执行完thread2线程,后执行完thread1线程。这两个线程可能是同时执行,也可能是来回切换执行,这取决于处理器的核心与线程。

代码讲解

函数式接口

Thread类的构造函数接收的是一个Runnable接口类型的参数,所以之前创建线程的代码长这样。

Thread myThread = new Thread(new Runnable() {
    @Override
    public void run() {

    }
});

看下面的代码,因为Runnable接口只有一个抽象的run方法需要去实现,所以就不需要去@Override声明我要实现run方法,直接传一个函数体不就可以吗?这就是函数式接口。

package java.lang;

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}

lamda表达式

相信很多人都听说过lamda表达式,但是总是觉得这个很高大上,其实我们接触过lamda表达式,只是没有注意到。

self.init = function() {

};

JavaScript的世界里,我们可以将一个函数传来传去。

self.init = () => {

};

然后人们发现,写function太麻烦了,他们发明了箭头函数。用这种写法代替一个函数。

Java为什么不可以?

如果刚刚的代码这么写,那你应该瞬间就明白了。

Runnable runnable = () -> {
    Singleton singleton1 = Singleton.getInstance();
    System.out.println("Singleton1:" + singleton1);
};
Thread thread1 = new Thread(runnable);

懒汉式

这是懒汉式的写法,当需要这个实例的时候,再去新建实例。

package com.mengyunzhi;

/**
 * @author zhangxishuo on 2018/6/18
 */
public class Singleton {

    private static Singleton instance;

    private Singleton() {

    }

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

但是这是这种写法是线程不安全的,我们再运行一下主函数中多个线程同时访问单例的方法。

上次向晨澍请教:StringBuffer线程安全,StringBuilder线程不安全。既然有的类线程安全,有的类线程不安全?那为什么不都用线程安全的呢?

答案就是:为了实现线程安全,系统需要额外的开销。所以有些不需要多线程的,使用线程不全的类,通常会提高速度。

clipboard.png

假设我们的处理器支持多个线程并行处理,当多个线程同时访问时,thread1获取实例,然后判断if (instance == null),创建实例;另一个线程同时执行,instance依然是空,然后thread2调用getInstance时又创建了一个实例。这就违反了单例模式。

synchronized

解决该问题的方案就是用synchronized修饰该代码块。

音标:['sɪŋkrənaɪzd]

只允许一个线程访问synchronized修饰的代码块,其他线程会被阻塞,等待该线程执行完再执行。

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

clipboard.png

应该是线程2先执行完的,所以我们猜测就是:线程2竞争到处理器资源,然后去访问getInstance()方法,因为笔者属于多核多线程处理器,支持线程并行执行,当线程2访问getInstance()代码块时,因为有synchronized修饰,所以线程1会被阻塞,等待线程2执行完再才能访问该代码块。

线程2执行完创建实例,线程1可以访问该代码块,发现instance不为空,直接返回。

使用场景

  • 频繁new然后销毁的对象,降低了内存开支。
  • 当一个对象的产生需要较多资源时,如读取配置,可以将其设置为单例,在应用启动时产生一个单例对象常驻内存。
  • 单例是同一个对象,可以用该单例设置项目配置,用于几个模块之间共享。

扩展:多线程学习

为什么要使用多线程?

摩尔定律

每18个月,芯片的性能将提高一倍。

单核心

十几年前,那时还是单核的时代,各大厂商做出主频越来越高的处理器。

但是主频越高,意味着芯片中需要的晶体管越多,功耗越大,散热越多,当一定程度热量就会烧坏芯片。

为什么CPU的频率止步于4G?我们触到频率天花板了吗?

2004年秋,IntelCEO公开对取消4GHz芯片的计划道歉。

clipboard.png

这是Intel酷睿i7 8700K的参数,主频仅有3.70GHz,十几年过去了,我们依然停留在4GHz

多核心

但是,为了满足不断增长的用户需求,虽然无法提升单个核心的时钟频率,但是厂商利用多核心实现了芯片的性能提升。

clipboard.png

这是Intel官网对i7 8700K的描述,6核心12线程。也就是说这个处理器有6个物理核心,因为超线程技术,可以模拟出12个逻辑核心,即可以同时处理12个线程任务。

超线程就是利用处理器剩余的资源模拟出一个新的核心,用于提高处理器的利用率。


张喜硕
2.1k 声望423 粉丝

浅梦辄止,书墨未浓。