单例模式
确保一个类只有一个实例,而且自动实例化并向整个系统提供这个实例。
实现
饿汉式
很简单。
- 将构造函数设置为私有的,防止外界
new
出该类的实例,从而失去了单例的意义。 - 设置类的私有静态变量,同时新建单例对象。
- 添加共有静态方法获取该单例。
该种方法的缺点是在类加载时就进行实例化,但是相较于其简单易用来说,这点缺点个人认为影响不大。
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();
}
}
注:打印时调用toString
方法,因为没有重写toString
,调用Object
类中的toString
,所以打印该对象的类名加哈希值。
我们看到控制台中打印的两个对象地址都是89ae60d
,表示同一块内存,即表示多线程时该实现方法仍能实现单例。
线程竞争
我们调用的顺序明明是thread1
的start
,然后thread2
再start
,但是为什么控制台打印的顺序却是单例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
线程不安全。既然有的类线程安全,有的类线程不安全?那为什么不都用线程安全的呢?
答案就是:为了实现线程安全,系统需要额外的开销。所以有些不需要多线程的,使用线程不全的类,通常会提高速度。
假设我们的处理器支持多个线程并行处理,当多个线程同时访问时,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;
}
应该是线程2先执行完的,所以我们猜测就是:线程2竞争到处理器资源,然后去访问getInstance()
方法,因为笔者属于多核多线程处理器,支持线程并行执行,当线程2访问getInstance()
代码块时,因为有synchronized
修饰,所以线程1会被阻塞,等待线程2执行完再才能访问该代码块。
线程2执行完创建实例,线程1可以访问该代码块,发现instance
不为空,直接返回。
使用场景
- 频繁
new
然后销毁的对象,降低了内存开支。 - 当一个对象的产生需要较多资源时,如读取配置,可以将其设置为单例,在应用启动时产生一个单例对象常驻内存。
- 单例是同一个对象,可以用该单例设置项目配置,用于几个模块之间共享。
扩展:多线程学习
为什么要使用多线程?
摩尔定律
每18个月,芯片的性能将提高一倍。
单核心
十几年前,那时还是单核的时代,各大厂商做出主频越来越高的处理器。
但是主频越高,意味着芯片中需要的晶体管越多,功耗越大,散热越多,当一定程度热量就会烧坏芯片。
2004年秋,Intel
的CEO
公开对取消4GHz
芯片的计划道歉。
这是Intel
酷睿i7 8700K
的参数,主频仅有3.70GHz
,十几年过去了,我们依然停留在4GHz
。
多核心
但是,为了满足不断增长的用户需求,虽然无法提升单个核心的时钟频率,但是厂商利用多核心实现了芯片的性能提升。
这是Intel
官网对i7 8700K
的描述,6
核心12
线程。也就是说这个处理器有6
个物理核心,因为超线程技术,可以模拟出12
个逻辑核心,即可以同时处理12
个线程任务。
超线程就是利用处理器剩余的资源模拟出一个新的核心,用于提高处理器的利用率。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。