单例模式,是设计模式中的一种。顾名思义,单例就是单个实例的意思,也就是说在代码中保证指定的类只有一个,如果创建了多个指定类的实例,直接让编译器报错。
单例模式的实现方式主要有两种
- 饿汉模式
public class Practice {//饿汉模式
private Practice() {//将构造方法设置成私有
}
static Practice p = new Practice();
public static Practice getPractice() {//通过一个方法将这个单独的实例传送出去
return p;
}
public static void main(String[] args) {
Practice p1 = Practice.getPractice();
Practice p2 = Practice.getPractice();
System.out.println((p1 == p2));
}
}
通过将一个类的构造方法设置成私有的,就可以保证在这个类外部无法通过new去创建这个类的实例,然后在这个类的里面new一个实例,通过一个方法传递出去。就保证了这个类外部只能获取这个类的一个实例,也就实现了单例。
这种饿汉模式的实现方式的特点就是在这个类被加载的时候就已经创建了一个实例。无论后序如何操作,只要严格遵守get方法,就永远只会得到一个实例。
- 懒汉模式
public class Practice {//饿汉模式
private Practice() {//将构造方法设置成私有
}
static Practice p = null;
public static Practice getPractice() {//通过一个方法将这个单独的实例传送出去
if(p==null){
p = new Practice();
}
return p;
}
public static void main(String[] args) {
Practice p1 = Practice.getPractice();
Practice p2 = Practice.getPractice();
System.out.println((p1 == p2));
}
}
懒汉模式和饿汉模式的差别主要体现在懒汉模式只有第一次调用get方法的时候才会实例化。只要我们不需要用到这个类,就不会实例化。这样的操作也叫做延时加载。
一般认为,懒汉模式比饿汉模式的效率更高。因为懒汉模式只要我们不去用它,就不会有实例化的开销,而饿汉模式只要类被加载,就会实例化,即使我们用不到它。
以上就是单例模式的两种实现方式,那么它们是否是线程安全的呢,多个线程调用get方法的时候是否会逻辑错误呢
对于饿汉模式来说,我们会发现get方法里面只做了一件事,就是读取p的值,并没有存在修改操作。当多个线程调用这个方法的时候,是在同时进行读取,所以没有什么影响,也就是线程安全的。
而对于懒汉模式来说,get方法里面做了四件事
- 读取p的值。
- 和null进行比较
- new一个实例赋值给p
- 返回到内存中。
我们可以发现,在new实例的时候发生了修改操作,当多个线程同时调用这个方法的时候,每个线程可能读取到的初始p值都为null,然后就都会new出一个实例,这就违反了单例模式的规则,所以是线程不安全的。
我们可以通过加锁操作来解决这个问题。
public class Practice {//饿汉模式
private Practice() {//将构造方法设置成私有
}
static Practice p = null;
public static Practice getPractice() {//通过一个方法将这个单独的实例传送出去
synchronized (p.class) {
if(p==null){
p = new Practice();
}
}
return p;
}
public static void main(String[] args) {
Practice p1 = Practice.getPractice();
Practice p2 = Practice.getPractice();
System.out.println((p1 == p2));
}
}
加锁之后,哪个线程先竞争到锁,哪个线程就会先执行锁里面包含的代码。初始情况下,某个线程竞争到锁之后,读取到的p的值为null,此时new出一个对象赋值给p。然后释放锁,第二个线程竞争到锁读取到p的值此时就不为null了,也就不会再new实例了。
虽然这个线程不安全问题解决了,但是仍然存在效率问题。一旦实例被创建好之后,那么线程再去读取的话会发现p已经不为null了,但是这样子加锁的话即使p不为null,仍然会去竞争锁,不能并发执行的代码增多了,也就降低了效率。
我们可以在外层再套一个if判断来解决
public class Practice {//饿汉模式
private Practice() {//将构造方法设置成私有
}
static Practice p = null;
public static Practice getPractice() {//通过一个方法将这个单独的实例传送出去
if (p==null) {
synchronized (p.class) {
if(p==null){
p = new Practice();
}
}
}
return p;
}
public static void main(String[] args) {
Practice p1 = Practice.getPractice();
Practice p2 = Practice.getPractice();
System.out.println((p1 == p2));
}
}
添加一个if判断之后,当某个线程运行的时候,会先判断一下p是否为null,也就是是否已经创建了一个实例。如果没有创建,才会竞争锁。
到这里,问题似乎已经全部解决了,但其实还存在一个线程不安全的问题。在实例化之前,多个线程同时调用这个方法的时候,两层if的读取操作可能都会被优化,第一次竞争到锁之后把p的值修改了,后面的线程中的读取操作如果被优化了,CPU就会从它的寄存器里面读取上次读取到的结果,也就是还是null,仍然不符合单例模式的规则。所以我们还应该在这个变量前面加上volatile关键字保证这个变量的内存可见性。
public class Practice {//饿汉模式
private Practice() {//将构造方法设置成私有
}
static volatile Practice p = null;
public static Practice getPractice() {//通过一个方法将这个单独的实例传送出去
if (p==null) {
synchronized (p.class) {
if(p==null){
p = new Practice();
}
}
}
return p;
}
public static void main(String[] args) {
Practice p1 = Practice.getPractice();
Practice p2 = Practice.getPractice();
System.out.println((p1 == p2));
}
}