单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
假定我们的设计中有A B C三个类,其关系如下:
public class A{
}
public class B{
public void print(){
A a =new A();
System.out.println(a);
}
}
public class C{
public void print(){
A a =new A();
System.out.println(a);
}
}
print方法的作用是打印出A类实例化后的地址
public class Main {
public static void main(String[] args){
new B().print();
new C().print();
}
}
这种情况下两个print方法的执行结果肯定是不同的,也就意味着这里创建了两个对象
输出结果
Test.A@5b2133b1
Test.A@33c7353a
如果我们在多线程模式中使用这种方式,那么对于性能的开销将会变得很大,因为每一个线程都会创建多个同样的实例,重复无数次操作。
那么如何保证该实例只被创建一次呢?方法如下:
//懒汉式创建a实例
public class A {
private static A a;//如果在此处直接创建实例,那么该方式被称为饿汉式
private A(){//声明构造函数为private的原因是为了保证A对象只能通过一种方法创建
}
public static A getInstance(){
if (null==a)a=new A();//如果采用饿汉式,此处不需要判断a实例是否为空
return a;
}
}
public class B{
public void print(){
A a =A.getInstance();
System.out.println(a);
}
}
public class C {
public void print(){
A a =A.getInstance();
System.out.println(a);
}
}
输出结果:
Test.A@5b2133b1
Test.A@5b2133b1
这种模式也仅是适合单线程模式,因为在多线程模式下我们并不知道哪个线程会先被启动,如果其中先启动的线程因为意外导致在创建实例之前被休眠,那么后面启动的线程又会创建一个实例a,等待最先启动的线程获取CPU控制权的时候又会创建一个实例a。如下所示
多线程模式下的正常情况:
package Test;
//懒汉式创建a实例
public class A {
private static A a;//如果在此处直接创建实例,那么该方式被称为饿汉式
private A(){//声明构造函数为private的原因是为了保证A对象只能通过一种方法创建
}
public static A getInstance(){
if (null==a){
//创建实例a
a=new A();
}
return a;
}
}
package Test;
public class B implements Runnable{
public void print(){
A a =A.getInstance();
System.out.println(a);
}
@Override
public void run() {
this.print();
}
}
package Test;
public class C implements Runnable{
public void print(){
A a =A.getInstance();
System.out.println(a);
}
@Override
public void run() {
this.print();
}
}
这种方式下,正常情况的输出肯定是一样的。但是当创建实例之前线程意外休眠的话,情况就不一样了
package Test;
//懒汉式创建a实例
public class A {
private static A a;//如果在此处直接创建实例,那么该方式被称为饿汉式
private A(){//声明构造函数为private的原因是为了保证A对象只能通过一种方法创建
}
public static A getInstance(){
if (null==a){
//模拟意外休眠情况
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//创建实例a
a=new A();
}
return a;
}
}
输出:
Test.A@2c0bb663
Test.A@74c02126
假定线程1先启动,那么按正常情况下线程1会创建唯一的实例a,之后的线程也将不用再次创建实例。但是如果线程1在判断实例a是否为空之后创建实例之前意外休眠,那么随后启动的线程2又会创建一个实例a(因为线程1并没有创建实例,所以a依旧为空)。但当CPU控制权再次回到线程1手里的时候,线程1又会创建一个实例a(因为线程1休眠的时候已经在if内,只是还没有创建实例),所以输出结果并不相同。
解决方法1:在声明getInstance方法的时候增加synchronizedg关键字,使线程同步。但是采用这种方式依旧存在一个问题,那就是每次调用getInstance方式的时候都会让线程形成一个队列。
package Test;
//懒汉式创建a实例
public class A {
private static A a;//如果在此处直接创建实例,那么该方式被称为饿汉式
private A(){//声明构造函数为private的原因是为了保证A对象只能通过一种方法创建
}
public synchronized static A getInstance(){//增加同步关键字,实现线程同步
if (null==a){
//模拟意外休眠情况
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//创建实例a
a=new A();
}
return a;
}
}
解决方式2:彻底的解决方式,采用双锁模式,因为多线程下产生多个实例的原因是先启动的线程意外休眠,如果同步getInstance方法会造成线程堵塞形成队列,那么如果直接同步实例化部分呢?如下所示
package Test;
//懒汉式创建a实例
public class A {
private static A a;//如果在此处直接创建实例,那么该方式被称为饿汉式
private static Object clock=new Object();//创建一个锁
private A(){//声明构造函数为private的原因是为了保证A对象只能通过一种方法创建
}
public static A getInstance(){//增加同步关键字,实现线程同步
if (null==a){
//模拟意外休眠情况
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//使线程在创建实例的时候同步,避免在每次调用getInstance都要同步
synchronized (clock){
if (a==null) a=new A();//形成双锁,此处必须再次进行一次判断,因为因为线程1休眠了,线程2已经创建了一个实例,等待线程1唤醒后又会再次创建一个实例,所以会出现两个实例;
}
}
return a;
}
}
Test.A@480a0d08
Test.A@480a0d08
如果在同步后没有再次进行判断,那么 结果如何呢?
package Test;
//懒汉式创建a实例
public class A {
private static A a;//如果在此处直接创建实例,那么该方式被称为饿汉式
private static Object clock=new Object();//创建一个锁
private A(){//声明构造函数为private的原因是为了保证A对象只能通过一种方法创建
}
public static A getInstance(){//增加同步关键字,实现线程同步
if (null==a){
//模拟意外休眠情况
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//使线程在创建实例的时候同步,避免在每次调用getInstance都要同步
synchronized (clock){
a=new A();
}
}
return a;
}
}
Test.A@480a0d08
Test.A@244306f9