1. 简介
顾名思义,单例即单一的实例,确切地讲就是指在某个系统中只存在一个实例,同时提供集中、统一的访问接口,以使系统行为保持协调一致。singleton一词在逻辑学中指“有且仅有一个元素的集合”,这非常恰当地概括了单例的概念,也就是“一个类仅有一个实例”
2. 饿汉模式
初始阶段就主动进行实例化,并时刻保持一种渴求的状态,无论此单例是否有人使用
饿汉模式没有线程安全性问题
class Sun{
private static final Sun sun=new Sun();
private Sun() {//不能省略 默认的Sun有一个public的构造函数
}
public static Sun getInstance() {
return sun;
}
}
public class SingletonDemo {
public static void main(String[] args) {
Sun sun1=Sun.getInstance();
Sun sun2=Sun.getInstance();
System.out.println(sun1==sun2);//true
}
}
- Sun类的构造函数重写成私有的,保证其他的类不能通过构造函数来创建Sun的对象
- static关键字保证单例对象与类同在,在类加载的时候就初始化了
- final关键字保证Sun对象是一个常量
3. 懒汉模式
初始阶段就不进行实例化,当有人使用时才初始化
1. 懒汉模式版本1
class Sun{
private static Sun sun;
private Sun() {
}
public static Sun getInstance() {
if(sun==null)//1
sun=new Sun();
return sun;
}
}
public class SingletonDemo {
public static void main(String[] args) throws InterruptedException {
for(int i=0;i<5;i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName()+" "+Sun.getInstance());
},"thread"+i).start();;
}
Thread.sleep(3000);
}
}
- 上面的代码在外部调用时才创建Sun对象并返回
- 但是代码中存在问题:在多线程情况下,如果有多个线程同时达到代码1处,那么就会创建多个实例对象,违背了单一原则,比如上面的main方法执行的结果如下:
从运行结果就可以看出,5个线程获得的对象并不完全一样,因此上述版本存在线程安全问题
2. 懒汉模式版本2
为了解决线程安全问题,我们可以枷锁,代码如下:
package designpattern;
class Sun{
private static Sun sun;
private Sun() {
}
public static synchronized Sun getInstance() {
if(sun==null)
sun=new Sun();
return sun;
}
}
public class SingletonDemo {
public static void main(String[] args) throws InterruptedException {
for(int i=0;i<5;i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName()+" "+Sun.getInstance());
},"thread"+i).start();;
}
Thread.sleep(3000);
}
}
从运行结果来看,现在对象唯一了,但是上面的代码效率不高,线程只要进入方法就进行加锁,可能有的线程来的比较晚,来的时候对象已经创建了,对于这类线程其实是不需要进行枷锁的
3. 懒汉模式版本3
package designpattern;
class Sun{
private static Sun sun;
private Sun() {
}
public static Sun getInstance() {
if(sun==null) {
synchronized (Sun.class) {
if(sun==null) {
sun=new Sun();
}
}
}
return sun;
}
}
public class SingletonDemo {
public static void main(String[] args) throws InterruptedException {
for(int i=0;i<5;i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName()+" "+Sun.getInstance());
},"thread"+i).start();;
}
Thread.sleep(1000);
}
}
if(sun==null) {//Thread1 Thread2 Thread3 Thread4 代码1
//Thread1 Thread2 Thread3 Thread4
synchronized (Sun.class) { //代码2
//Thread1
if(sun==null) { //代码3
//Thread1
sun=new Sun();
}
对这段代码的解释:
- 假设有4个线程同时到达了代码1处,这4个线程都可以进入第一个if块内
- 在代码2处由于是加锁,因此只有1个线程可以进入,假设线程1进入了同步代码块
- 线程1进入了同步代码块,进行代码3处的判断,对象为空,条件成立,因此线程1创建对象,执行结束,退出代码块
- 后面的线程2 3 4 也相继进入同步代码块中,但是由于第二个if条件不成立,因此不能创建对象
- 如果后面还有线程5 6 7等来到代码1处,对于这些后来的线程而言,第一个if条件就不成立,因此无法进入
这种方式称为懒加载模式的“双检锁
如果去掉第一个if: 和懒汉模式版本2的代码本质上一样,只要线程来了就要进行抢锁操作,耗费时间
如果去掉第二个if:当有多个线程通过了第一个if语句后,这些线程都会相继进入synchronized代码块中,没有第二个if就会创建多个实例对象
总的来说,第一个if保证线程效率,第二个if保证线程安全