单例设计模式(Singleton Pattern)
是最简单且常见的设计模式之一,主要作用是提供一个全局访问且只实例化一次的对象,避免多实例对象的情况下引起逻辑性错误(实例化数量可控)
…
<!– more –>
概述
Java中,单例模式
主要分三种:懒汉式单例、饿汉式单例、登记式单例三种。
- 懒汉:非线程安全,需要用一定的风骚操作控制,装逼失败有可能导致看一周的海绵宝宝
- 饿汉:天生线程安全,
ClassLoad
的时候就已经实例化好,该操作过于风骚会造成资源浪费 - 单例注册表:Spring初始化Bean的时候,默认单例用的就是该方式
特点
- 私有构造方法,只能有一个实例。
- 私有静态引用指向自己实例,必须是自己在内部创建的唯一实例。
- 单例类给其它对象提供的都是自己创建的唯一实例
案例
- 在计算机系统中,内存、线程、CPU等使用情况都可以再
任务管理器
中看到,但始终只能打开一个任务管理器
,它在Windows操作系统中是具备唯一性的,因为弹多个框多次采集数据浪费性能不说,采集数据存在误差那就有点逗比了不是么… - 每台电脑只有一个
打印机后台处理程序
- 线程池的设计一般也是采用单例模式,方便对池中的线程进行控制
注意事项
- 实现方式种类较多,有的
非线程安全
方式的创建需要特别注意,且在使用的时候尽量根据场景选取较优的,线程安全了还需要去考虑性能问题。 - 不适用于变化的对象,如果同一类型的对象总是要在不同的用例场景发生变化,单例就会引起数据的错误,不能保存彼此的状态。
- 没有抽象层,扩展有困难。
- 职责过重,在一定程度上违背了
单一职责原则
。 - 使用时不能用反射模式创建单例,否则会实例化一个新的对象
解锁姿势
第一种:单一检查(懒汉)非线程安全
public class LazyLoadBalancer {
private static LazyLoadBalancer loadBalancer;
private List<String> servers = null;
private LazyLoadBalancer() {
servers = new ArrayList<>();
}
public void addServer(String server) {
servers.add(server);
}
public String getServer() {
Random random = new Random();
int i = random.nextInt(servers.size());
return servers.get(i);
}
public static LazyLoadBalancer getInstance() {
// 第一步:假设T1,T2两个线程同时进来且满足 loadBalancer == null
if (loadBalancer == null) {
// 第二步:那么 loadBalancer 即会被实例化2次
loadBalancer = new LazyLoadBalancer();
}
return loadBalancer;
}
public static void main(String[] args) {
LazyLoadBalancer balancer1 = LazyLoadBalancer.getInstance();
LazyLoadBalancer balancer2 = LazyLoadBalancer.getInstance();
System.out.println("hashCode:"+balancer1.hashCode());
System.out.println("hashCode:"+balancer2.hashCode());
balancer1.addServer("Server 1");
balancer2.addServer("Server 2");
IntStream.range(0, 5).forEach(i -> System.out.println("转发至:" + balancer1.getServer()));
}
}
日志
hashCode:460141958
hashCode:460141958
转发至:Server 2
转发至:Server 2
转发至:Server 2
转发至:Server 1
转发至:Server 2
分析: 在单线程环境一切正常,balancer1
和balancer2
两个对象的hashCode
一模一样,由此可以判断出堆栈中只有一份内容,不过该代码块中存在线程安全隐患
,因为缺乏竞争条件,多线程环境资源竞争的时候就显得不太乐观了,请看上文代码注释内容
第二种:无脑上锁(懒汉)线程安全,性能较差,第一种升级版
public synchronized static LazyLoadBalancer getInstance() {
if (loadBalancer == null) {
loadBalancer = new LazyLoadBalancer();
}
return loadBalancer;
}
分析: 毫无疑问,知道synchronized
关键字的都知道,同步方法
在锁没释放之前,其它线程都在排队候着呢,想不安全都不行啊,但在安全的同时,性能方面就显得短板了,我就初始化一次,你丫的每次来都上个锁,不累的吗(没关系,它是为了第三种做铺垫的)..
第三种:双重检查锁(DCL),完全就是前两种的结合体啊,有木有,只是将同步方法升级成了同步代码块
//划重点了 **volatile**
private volatile static LazyLoadBalancer loadBalancer;
public static LazyLoadBalancer getInstance() {
if (loadBalancer == null) {
synchronized (LazyLoadBalancer.class) {
if (loadBalancer == null) {
loadBalancer = new LazyLoadBalancer();
}
}
}
return loadBalancer;
}
1.假设new LazyLoadBalancer()
加载内容过多
2.因重排而导致loadBalancer
提前不为空
3.正好被其它线程观察到对象非空直接返回使用
mem = allocate(); //LazyLoadBalancer 分配内存
instance = mem; //注意当前实例已经不为空了
initByLoadBalancer(instance); //但是还有其它实例未初始化
存在问题: 首先我们一定要清楚,DCL
是不能保证线程安全
的,稍微了解过JVM的就清楚,对比C/C++
它始终缺少一个正式的内存模型
,所以为了提升性能,它还会做一次指令重排操作
,这个时候就会导致loadBalancer
提前不为空,正好被其它线程观察到对象非空直接返回使用(但实际还有部分内容没加载完成)
解决方案: 用volatile
修饰loadBalancer
,因为volatile
修饰的成员变量可以确保多个线程都能够顺序处理,它会屏蔽JVM指令重排带来的性能优化
。
volatile详解:http://blog.battcn.com/2017/10/18/java/thread/thread-volatile/
第四种:Demand Holder (懒汉)线程安全,推荐使用
private LazyLoadBalancer() {}
private static class LoadBalancerHolder {
//在JVM中 final 对象只会被实例化一次,无法修改
private final static LazyLoadBalancer INSTANCE = new LazyLoadBalancer();
}
public static LazyLoadBalancer getInstance() {
return LoadBalancerHolder.INSTANCE;
}
分析: 在Demand Holder
中,我们在LazyLoadBalancer
里增加一个静态(static)内部类,在该内部类中创建单例对象,再将
该单例对象通过getInstance()
方法返回给外部使用,由于静态单例对象没有作为LazyLoadBalancer
的成员变量直接实例化,类加载时并不会实例化LoadBalancerHolder
,因此既可以实现延迟加载,又可以保证线程安全,不影响系统性能(居家旅行必备良药啊)
第五种:枚举特性(懒汉)线程安全
enum Lazy {
INSTANCE;
private LazyLoadBalancer loadBalancer;
//枚举的特性,在JVM中只会被实例化一次
Lazy() {
loadBalancer = new LazyLoadBalancer();
}
public LazyLoadBalancer getInstance() {
return loadBalancer;
}
}
分析: 相比上一种,该方式同样是用到了JAVA特性:枚举类保证只有一个实例(即使使用反射机制也无法多次实例化一个枚举量)
第六种:饿汉单例(天生线程安全),
public class EagerLoadBalancer {
private final static EagerLoadBalancer INSTANCE = new EagerLoadBalancer();
private EagerLoadBalancer() {}
public static EagerLoadBalancer getInstance() {
return INSTANCE;
}
}
分析: 利用ClassLoad
机制,在加载时进行实例化,同时静态方法只在编译期间执行一次初始化,也就只有一个对象。使用的时候已被初始化完毕可以直接调用,但是相比懒汉模式
,它在使用的时候速度最快,但这玩意就像自己挖的坑哭着也得跳,你不用也得初始化一份在内存中占个坑…
– 说点什么
全文代码:https://gitee.com/battcn/design-pattern/tree/master/Chapter2/battcn-singleton
- 个人QQ:1837307557
- battcn开源群(适合新手):391619659
微信公众号:battcn
(欢迎调戏)