单例模式简单说即一个类只有一个对象实例。
从具体实现角度来说,就是以下三点:
- 单例模式的类只提供私有的构造函数
- 类定义中含有一个该类的静态私有对象
- 该类提供了一个静态的公有的函数用于创建或获取它本身的静态私有对象,并返回该对象。
单例模式的好处:保证对象的唯一性,所谓单例,我的理解就是使用同一个对象,不能出现第二个一模一样的对象。
单例模式又分饿汉模式和懒汉模式
- 懒汉模式:实例在第一次使用时创建
- 饿汉模式:实例在类装载时创建
创建单例模式的步骤:
- 1,私有化该类构造方法
- 2,通过new在本类创建一个本类对象
- 3,定义一个公有的方法,创建将对象返回
//单例模式的饿汉模式,类一加载,对象就存在
class Single{
static Single single=new Single();
private Single() {
}
public static Single getInstance(String name) {
/*逻辑判断*/
return single; //返回对象
}
}
//单例模式的懒汉模式,类加载是没有对象,调用getInstance才有对象
class Single{
static Single single=null;
private Single() {
}
public static Single getInstance(String name) {
/*逻辑判断*/
if(single==null){
single=new Single();
return single;
}
}
}
/*-------------------------分割线----------------------------*/
//使用单例与不使用单例的区别
public class SingleModel {
public static void main(String[] args) {
//不使用Single ss=Single s;和set、get一样的道理,可控
Single ss=Single.getInstance("单例模式");
//来看看唯一性,使用单例模式
useSingle us1=useSingle.getInstance();
useSingle us2=useSingle.getInstance();
us1.setNum(10);
us2.setNum(20);
System.out.println(us1==us2);
System.out.println("使用单例模式,us1:"+us1.getNum());
System.out.println("使用单例模式,us2:"+us2.getNum());
//不使用单例模式
notUseSingle nus1=new notUseSingle();
notUseSingle nus2=new notUseSingle();
nus1.setNum(10);
nus2.setNum(20);
System.out.println(nus1==nus2);
System.out.println("不使用单例模式,nus1:"+nus1.getNum());
System.out.println("不使用单例模式,nus2:"+nus2.getNum());
}
}
class useSingle{
private int num;
private static useSingle us=new useSingle();
private useSingle() {
}
public static useSingle getInstance(){
return us;
}
public void setNum(int num){
this.num=num;
}
public int getNum(){
return num;
}
}
class notUseSingle{
public int num;
public void setNum(int num){
this.num=num;
}
public int getNum(){
return num;
}
}
输出结果:
true
使用单例模式,us1:20
使用单例模式,us2:20
false
不使用单例模式,nus1:10
不使用单例模式,nus2:20
——————————————————————分割线——————————————————————————————
class Single{
private static Single single;
private Single(){}
public static Single getInstance(){
if(single==null){
single=new Single();
}
return single;
}
}
需要注意的是这种实现方式是线程不安全的。假设在单例类被实例化之前,有两个线程同时在获取单例对象,线程1在执行完第5行 if (instance == null) 后,线程调度机制将 CPU 资源分配给线程2,此时线程2在执行第5行 if (instance == null) 时也发现单例类还没有被实例化,这样就会导致单例类被实例化两次。为了防止这种情况发生,需要对 getInstance() 方法同步。下面看改进后的懒汉模式:
class Single{
private static Single single;
private Single(){}
public synchronized static Single getInstance(){
if(single==null){
single=new Single();
}
return single;
}
}
方法加了synchronized 实现方式中,每次获取单例对象时都会加锁,这样就会带来性能损失。
改进版:
只在第一次的时候判断
class Single{
private static Single single; // 1
private Single(){}
public static Single getInstance(){
if(single==null){ // 2
//双重检查加锁,只有在第一次实例化时,才启用同步机制,提高了性能。
synchronized (Single.class) { // 3
if(single==null){ // 4
single=new Single(); // 5
}
}
}
return single;
}
}
问题又来了,指令重排序:
创建一个对象实例,可以分为三步:
- 分配对象内存
- 调用构造器方法
- 执行初始化 将对象引用赋值给变量。
虽然重排序并不影响单线程内的执行结果,但是在多线程的环境就带来一些问题。
上面错误双重检查锁定的示例代码中,如果线程 1 获取到锁进入创建对象实例,这个时候发生了指令重排序。当线程1 执行到 t3 时刻,线程 2 刚好进入,由于此时对象已经不为 Null,所以线程 2 可以自由访问该对象。然后该对象还未初始化,所以线程 2 访问时将会发生异常。
修改:
class Single{
private volatile static Single single; // 1
private Single(){}
public static Single getInstance(){
if(single==null){ // 2
//双重检查加锁,只有在第一次实例化时,才启用同步机制,提高了性能。
synchronized (Single.class) { // 3
if(single==null){ // 4
single=new Single(); // 5
}
}
}
return single;
}
}
volatile 作用:
正确的双重检查锁定模式需要需要使用 volatile。volatile主要包含两个功能。
- 保证可见性。使用 volatile 定义的变量,将会保证对所有线程的可见性。
- 禁止指令重排序优化。
由于 volatile 禁止对象创建时指令之间重排序,所以其他线程不会访问到一个未初始化的对象,从而保证安全性。
参考:https://juejin.im/post/5d54c2d251882542f27bdff6