目录
单例模式的概念
单例模式是多线程代码中一种经典的设计模式,设计模式类似于棋谱,下棋按照固定的套路来下,基本上棋力就不会太差。同样,设计模式就是把编程中的各种经典问题场景给你盘一盘,并且给你提供一些解决方案。遇到这个场景,代码就这么写,你的代码就不会写的很差。熟练掌握设计模式,不能提高你代码的上限,但是能保住你代码的下限。
设计模式有很多种,我们今天讲的是属于比较简单的单例模式。
顾名思义,单例模式指的就是单个实例,整个进程中的某个类,有且只有一个对象(不会new出来多个对象)。
那么如何保证这个类只有一个对象呢?靠程序员口头保证可不行。
我们需要让编译器来帮助我们做一个强制的检查。通过一些编码上的技巧,使编译器可以自动发现咱们代码中是否有多个实例,并且在尝试创建多个实例的时候,直接编译出错。
从根本上保证对象是唯一实例,这样的代码就称为单例模式。
单例模式有很多种不同的写法,我们这里主要介绍两种。
饿汉模式
下面就是一段饿汉模式的代码:
//单例
//饿汉模式,创建时机非常早
class Singleton{
private static Singleton instance = new Singleton();
public static Singleton getInstance(){
return instance;
}
private Singleton(){
}
}
public class ThreadDemo12 {
public static void main(String[] args) {
Singleton s1 = Singleton.getInstance();
Singleton s2 = Singleton.getInstance();
}
}
我们注意,static成员初始化时机是在类加载的时候,static修饰的其实是“类属性”,就是在“类对象”上的,每个类的类对象在JVM中只有一个,里面的静态成员,只有一份。所以对象的初始化也是只执行一次的。
后续需要使用这个类的实例,就可以通过getInstance来获取已经new好的这个,而不是重新new。
类之外的代码,尝试new的时候,势必就要调用构造方法,由于构造方法是私有的,无法调用,就会编译出错。
懒汉模式
懒汉模式,不是在程序启动的时候创建实例,而是在第一次使用的时候才去创建。(如果不使用了,就会把创建实例的代价就节省下来了)
下面是一段懒汉模式代码:
//懒汉模式 不是在程序启动的时候创建实例,而是在第一次使用的时候才去创建
class SingletonLazy{
private static volatile SingletonLazy instance = null;
public static SingletonLazy getInstance(){
Object locker = new Object();
if(instance == null){
instance = new SingletonLazy();
}
return instance;
}
private SingletonLazy(){
}
}
public class ThreadDemo13 {
public static void main(String[] args) {
SingletonLazy s1 = SingletonLazy.getInstance();
SingletonLazy s2 = SingletonLazy.getInstance();
System.out.println(s1);
System.out.println(s2);
}
}
添加if判断的目的就是,啥时候调用,就啥时候创建。如果不调用,就不创建了。
如果代码中存在多个单例类,使用饿汉模式,就会导致这些实例都是在程序启动的时候扎堆创建的,可能把程序启动时间拖慢。
如果是懒汉模式,啥时候首次调用,调用时机是分散的,化整为零,用户不太容易感知到“卡顿”。
那懒汉模式是否有缺点呢?
当然,懒汉模式是线程不安全的,而饿汉模式是线程安全的。(重点)
饿汉模式的创建时机是在java进程启动(比main调用还早的时机),后续线程执行getInstance的时候,意味着上述实例早都已经有了。每个线程的getInstance只做了一件事,就是读取上述静态变量的值。多个线程读取同一个变量,是线程安全的。
而懒汉模式的代码,在多线程的环境下,就可能产生问题。
当t1,t2两个线程创建实例的时候,就可能出现以下执行顺序。
t2切换回来之后,要沿着之前的位置继续往下执行,就会直接进入条件内部再次执行new操作。创建了两个对象,不符合单例模式了。
这种情况下,可以通过加锁的方式,来使创建实例的操作原子化,避免上述情况。
//懒汉模式 不是在程序启动的时候创建实例,而是在第一次使用的时候才去创建
class SingletonLazy{
private static volatile SingletonLazy instance = null;
public static SingletonLazy getInstance(){
Object locker = new Object();
synchronized (locker) {
if (instance == null) {
instance = new SingletonLazy();
}
}
return instance;
}
private SingletonLazy(){
}
}
public class ThreadDemo13 {
public static void main(String[] args) {
SingletonLazy s1 = SingletonLazy.getInstance();
SingletonLazy s2 = SingletonLazy.getInstance();
System.out.println(s1);
System.out.println(s2);
}
}
t1执行加锁之后,t2就会阻塞,直到t1释放锁(new完了),t2才能拿到锁,才能进行条件判定。t2的条件就会认为Instance非null,也就不会再new了。
当然,加锁本身也是有开销的。我们不一定每次创建实例都要加锁,那样也会使效率降低。
代码可以增加如下改进:
//懒汉模式 不是在程序启动的时候创建实例,而是在第一次使用的时候才去创建
class SingletonLazy{
private static volatile SingletonLazy instance = null;
public static SingletonLazy getInstance(){
Object locker = new Object();
if(instance == null) {
synchronized (locker) {
if (instance == null) {
instance = new SingletonLazy();
}
}
}
return instance;
}
private SingletonLazy(){
}
}
public class ThreadDemo13 {
public static void main(String[] args) {
SingletonLazy s1 = SingletonLazy.getInstance();
SingletonLazy s2 = SingletonLazy.getInstance();
System.out.println(s1);
System.out.println(s2);
}
}
最外面再加一层if的目的是:判定是否要加锁。如果是实例化之后,线程自然安全了,就无需加锁了。实例化之前,new之前,就应该要加锁。
第二层if的目的是:判定是否要创建对象。
两个if之间,synchronized回使该线程阻塞,阻塞过程中其他线程就可能会修改Instance的值。
以上,关于单例模式,希望对你有所帮助。