单例模式的应用场景
单例模式(Singleton Pattern)是确保一个类在任何情况下都绝对只有一个实例,并提供一个方法可以获取当前实例对象。单例模式是创建型模式。例如:生活中,公司的董事长,技术首席执行官都属于单例模式。源码中,Spring框架提供的Application Context,J2EE 标准中的ServletContext等也都是单例形式。
饿汉式单例模式
饿汉式单例模式在类加载的时候就立即初始化,并且创建单例对象。它绝对线程安全,因为类加载机制本身就是线程安全的,不存在访问安全问题。
优点:没有加任何锁,执行效率高。
缺点:不管用不用都占用空间,浪费内存。
代码实现如下:
public class Solution {
private static final Solution INSTANCE = new Solution();
private Solution() {
}
public static Solution getInstance() {
return INSTANCE;
}
}
还有另一种写法,利用静态代码块的机制:
public class Solution {
private static final Solution INSTANCE;
static {
INSTANCE = new Solution();
}
private Solution() {
}
public static Solution getInstance() {
return INSTANCE;
}
}
这两种写法都比较简单,也非常好理解,下面来看懒汉式写法。
懒汉式单例模式
懒汉式单例模式的特点是,被外部类调用的时候才会加载,下面来看懒汉式简单实现:
public class Solution {
private static Solution solution;
private Solution() {
}
public static Solution getInstance() {
if (solution == null) {
solution = new Solution();
}
return solution;
}
}
然后测试代码如下:
class Test{
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
Solution instance = Solution.getInstance();
System.out.println(instance.toString());
});
Thread thread2 = new Thread(() -> {
Solution instance = Solution.getInstance();
System.out.println(instance.toString());
});
thread1.start();
thread2.start();
}
}
结果如下图所示:
上面的竟出现两个实例对象,显然不符合单例模式,意味着上面的单例模式存在线程安全隐患,那么,我们该如何优化代码,使得懒汉式单例模式在多线程环境下实现线程安全呢?来看下面的代码,给getInstance()加上synchronized关键字,使得方法成为线程同步方法:
public class Solution {
private static Solution solution;
private Solution() {
}
public synchronized static Solution getInstance() {
if (solution == null) {
solution = new Solution();
}
return solution;
}
}
当其中一个线程执行getInstance()方法时,另一个线程再调用getInstance()方法,线程的状态将会由RUNNING变成MONITOR,出现阻塞。直到第一个线程执行完,第二个线程才恢复到RUNNING继续调用getInstance()方法。
但是,用synchronized加锁时,在线程数量较多时,导致大量线程阻塞,CPU分配压力上升,导致程序性能大幅下降。那么,有没有更好的方式,既能兼顾线程安全又能提高性能呢?答案是肯定的,我们来看双重检查锁的单例模式:
public class Solution {
private static Solution solution;
private Solution() {
}
public static Solution getInstance() {
if (solution == null) {
synchronized (Solution.class) {
if (solution == null) {
solution = new Solution();
}
}
}
return solution;
}
}
但是,synchronized关键字总归是要上锁,对程序性能还是有一定影响的,难道就没有更好的方案吗?当然有,我们可以从类的初始化角度来考虑,看下面的代码,采用静态内部类的方式:
public class Solution {
private Solution() {
}
public static Solution getInstance() {
return SolutionInner.INSTANCE;
}
//默认不加载
private static class SolutionInner {
private static final Solution INSTANCE = new Solution();
}
}
这种方式兼顾了内存问题和加锁的性能问题,巧妙的避免了线程安全问题。
反射破坏单例模式
上面的案例除了使构造方法私有化之外,没做任何处理,如果我们利用反射来调用其构造方法,应该会有两个不同的实例对象,下面来写一段示例代码测试一下:
class Test {
public static void main(String[] args) {
Class<?> clazz = Solution.class;
try {
Constructor<?> c = clazz.getDeclaredConstructor(null);
c.setAccessible(true);
Object o1 = c.newInstance();
Object o2 = c.newInstance();
System.out.println(o1 == o2);
} catch (Exception e) {
e.printStackTrace();
}
}
}
运行后结果如下:
显然,创建了两个不同的实例,现在,我们在其构造方法里做一些限制,一旦出现多次重复创建,则抛出异常,来看代码:
public class Solution {
private Solution() {
if (SolutionInner.INSTANCE != null){
throw new RuntimeException("不允许创建多个实例");
}
}
public static Solution getInstance() {
return SolutionInner.INSTANCE;
}
//默认不加载
private static class SolutionInner {
private static final Solution INSTANCE = new Solution();
}
}
再运行测试代码:
这样便大功告成,还有好几种没写,明天再战,先睡觉,干不动了。。。。