前引:单例模式是校招中最常考的设计模式之⼀.
啥是设计模式? (我感觉有点像做题的模板)
1)设计模式好⽐象棋中的"棋谱".红⽅当头炮,⿊⽅⻢来跳.针对红⽅的⼀些⾛法,⿊⽅应招的时候有⼀ 些固定的套路.按照套路来⾛局势就不会吃亏.
2)软件开发中也有很多常⻅的"问题场景".针对这些问题场景,⼤佬们总结出了⼀些固定的套路.按照这 个套路来实现代码,也不会吃亏.
1.饿汉
饿汉:顾名思义非常饥饿,想立马吃东西。在程序代码中就是立马创建对象。
特点:
1)创建对象快
2)只有一个实例对象(节约空间)
代码如下:
class HungrySingle{//instance 实例
private static HungrySingle instance = new HungrySingle();
//获取实例
public static HungrySingle getInstance(){
return instance;
}
}
public class Demo1 {
public static HungrySingle hungrySingle = null;
public static void main(String[] args) {
HungrySingle hungrySingle = HungrySingle.getInstance();
}
}
这个时候你可能在想这和我自己new一个对象有什么区别,还不如new一个对象来的简单。
解释:
//HungerySingle类中代码与上面一个一样
public class Demo1 {
public static HungrySingle hungrySingle = null;
public static HungrySingle hungrySingle1 = null;
public static void main(String[] args) {
HungrySingle hungrySingle = HungrySingle.getInstance();
HungrySingle hungrySingle1 = HungrySingle.getInstance();
System.out.println(hungrySingle1 == hungrySingle);//true
HungrySingle single2 = new HungrySingle();
HungrySingle single3 = new HungrySingle();
System.out.println(single2 == single3);//false
}
}
对比你会发现 hungrySingle = hungrySingle1 说明这二者是同一个,引用了同一内存,属于同一对象实例;single2 != single3,引用不同内存,属于不同对象实例。
2.懒汉(代码中细节较多)
懒汉:非常懒,只有在你万分火急的时候才会去做;在程序中就是在你调用的时候才创建对象。
特点:
1)在需要的时候创建对象
2)只有一个实例对象(节约空间)
class LazySingle {
private LazySingle instance = null;
public LazySingle getInstance(){
if(instance == null){
instance = new LazySingle();
}
return instance;
}
}
//注意:这里代码有bug
public class Demo2 {
private static LazySingle lazySingle = null;
public static void main(String[] args) {
LazySingle single = lazySingle.getInstance();
}
}
细节1:线程安全问题
解释:两个线程同时对同一个变量进行操作。与下面代码类似明明两个for循环5 0000次,count应该为10 0000,但如果你运行下面代码,你会发现结果都小于10 0000,这是为什么呢?
如图:
private static int count = 0;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() ->{
for (int i = 0;i < 50000;i++){
count++;
}
});
Thread t1 = new Thread(() ->{
for (int i = 0;i < 50000;i++){
count++;
}
});
t.start();
t1.start();
t1.join();
t.join();
System.out.println(count);//结果小于100000
}
注意:循环次数不能太少,否则可能会出现t1线程都循环完了,t2才开始,就会出现正确结果。
(当然这个也能解决,通过sleep,使两个线程有重叠就OK了)
但上面代码加锁就可以有正确结果了
public class Demo {
private static Object locker = new Object();
private static int count = 0;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() ->{
for (int i = 0;i < 50000;i++){
synchronized (locker){
count++;
}
}
});
Thread t1 = new Thread(() ->{
for (int i = 0;i < 50000;i++){
synchronized (locker){
count++;
}
}
});
t.start();
t1.start();
t1.join();
t.join();
System.out.println(count);
}
}
细节2:指令重排序问题(这个问题发生概率是非常小的)
解释:由于JVM,CPU指令重排序造成下面这种情况,volatile关键字修饰的变量会告诉CPU这个是容易变化的,CPU就不会对其进行优化,从而解决问题。
注:instance = lazySingle
细节3:两成if嵌套有神奇效果
如果在单线程下两层if是无意义的,多线程就不一样了
1)里面一层if是用来判断instance == null,为空,则创建对象;
2)外面一层if是用来减少加锁次,加锁/解锁是⼀件开销⽐较⾼的事情.⽽懒汉模式的线程不安全只是发⽣在⾸次创建实例的时候.因此后续使⽤的时候,不必再进⾏加锁了。
正确代码
//方法一
class LazySingle1 {
private static LazySingle instance = null;
private static Object locker = new Object();
//对方法加锁,在线成一调用,线程二不能调用,也就不会有指令重排序带来的问题了
public static synchronized LazySingle getInstance(){
if (instance == null) {
instance = new LazySingle();
}
return instance;
}
}
//方法二
class LazySingle {
private static volatile LazySingle instance = null;
private static Object locker = new Object();
public static LazySingle getInstance(){
if(instance == null) {
synchronized (locker) {
if (instance == null) {
instance = new LazySingle();
}
}
}
return instance;
}
}
public class Demo2 {
private static LazySingle lazySingle = null;
private static LazySingle lazySingle1 = null;
public static void main(String[] args) throws InterruptedException {
lazySingle = LazySingle.getInstance();
lazySingle1 = LazySingle1.getInstance();
}
}
看到这里就不要吝啬手中的赞,创作不易,你的点赞时对我最大的支持!