往下学习单例模式吧
■ 章节目录
■ 前言
- 由于涉及到挺多关于线程这一块的知识,如果自己线程相关知识不是很清楚的话,可以去看看这篇文章噢👉:关于线程你想知道的都在这
■ 什么是单例模式?
单例模式(Singleton Pattern)是 Java中
最简单的设计模式之一
。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
这种模式涉及到一个单一的类,该类负责创建自己的对象
,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式
,可以直接访问,不需要实例化该类的对象。
注意
:
1、单例类只能有一个实例。
2、单例类必须自己创建自己的唯一实例。
3、单例类必须给所有其他对象提供这一实例。
再介绍
单例模式 | |
---|---|
意图 | 保证一个类仅有一个实例,并提供一个访问它的全局访问点。 |
主要解决 | 一个全局使用的类频繁地创建与销毁。 |
何时使用 | 当您想控制实例数目,节省系统资源的时候。 |
如何解决 | 判断系统是否已经有这个单例,如果有则返回,如果没有则创建。 |
关键代码 | 构造函数是私有的。 |
① 应用场景实例
1、
Windows的Task Manager(任务管理器)
就是很典型的单例模式(这个很熟悉吧),想想看,是不是呢,你能打开两个windows task manager吗? 不信你自己试试看哦~
2、windows的Recycle Bin(回收站)
也是典型的单例应用。在整个系统运行过程中,回收站一直维护着仅有的一个实例。
3、网站的计数器
,一般也是采用单例模式实现,否则难以同步。
4、应用程序的日志应用
,一般都是用单例模式实现,这一般是由于共享的日志文件一直处于打开状态,因为只能有一个实例去操作,否则内容不好追加。
5、数据库连接池
的设计一般也是采用单例模式,因为数据库连接是一种数据库资源。数据库软件系统中使用数据库连接池,主要是节省打开或者关闭数据库连接所引起的效率损耗,这种效率上的损耗还是非常昂贵的,因为何用单例模式来维护,就可以大大降低这种损耗。
6、多线程的线程池
的设计一般也是采用单例模式,这是由于线程池要方便对池中的线程进行控制。
7、J2EE中的ServletContext,ServletContextConfig等;在Spring中,每个Bean默认是单例;Spring中的ApplicationContext、数据库连接池等
② 模式优点
- 在单例模式中,活动的单例只有一个实例,对单例类的所有实例化得到的都是相同的一个实例。这样就 防止其它对象对自己的实例化,
确保所有的对象都访问一个实例
。 - 避免对
共享资源的多重占用
。(比如写文件操作)。 - 提供了对
唯一实例的受控访问
。 - 由于在系统内存中只存在一个对象,因此可以节约系统资源,当需要频繁创建和销毁的对象时单例模式无疑可以
提高系统的性能
。
③ 模式缺点
- 不适用于变化的对象,如果同一类型的对象总是要在不同的用例场景发生变化,单例就会引起数据的错误,不能保存彼此的状态。
- 由于单利模式中没有抽象层,因此单例类的扩展有很大的困难。
④ 注意事项
- 使用时
不能用反射模式创建单例
,否则会实例化一个新的对象 。 - 使用懒单例模式时注意
线程安全问题
。
■ 单例模式的五种实现方式
饿汉式
以空间换时间,没有实现延迟加载,
线程安全
懒汉式
以时间换空间,实现了延迟加载,
加了synchronized后线程安全
双重检测锁式
实现了延迟加载和线程安全,使用双重检查加锁的方法,使得性能不会有太大的影响,但会屏蔽掉虚拟机中一些必要的代码优化,所以运行效率并不是很高,
线程安全
静态内部类式
线程安全
,调用效率高。可以延时加载
使用枚举
线程安全
、调用效率高,不能延时加载
饿汉式代码实现
//线程1用来实例对象
class Thread1 implements Runnable{
@Override
public void run() {
for (int i=0;i<3;i++){
Singleton_Hungry instance1 = Singleton_Hungry.getInstance();
Singleton_Hungry instance2 = Singleton_Hungry.getInstance();
System.out.println("我是Thread1,他们地址返回"+(instance1 == instance2));
}
}
}
//线程2再用来实例对象
class Thread2 implements Runnable{
@Override
public void run() {
for (int i=0;i<3;i++){
Singleton_Hungry instance1 = Singleton_Hungry.getInstance();
Singleton_Hungry instance2 = Singleton_Hungry.getInstance();
System.out.println("我是Thread2,他们地址返回"+(instance1 == instance2));
}
}
}
public class HungryMan {
public static void main(String[] args) {
// 启动两个线程,线程1,2都实例Singleton_Hungry10次,实例Singleton_Hungry10次
Thread thread = new Thread(new Thread1());
Thread thread1 = new Thread(new Thread2());
thread.start();
thread1.start();
// 再测试单例模式
Singleton_Hungry instance1 = Singleton_Hungry.getInstance();
Singleton_Hungry instance2 = Singleton_Hungry.getInstance();
// 两者的地址相等,对外是同一个对象
System.out.println("我是main,他们地址返回"+(instance1 == instance2));
}
}
class Singleton_Hungry{
private static Singleton_Hungry instance = new Singleton_Hungry();
// 私有构造函数,使得在其它方法上不能被调用,必须通过内部方法进行实例化
private Singleton_Hungry(){
System.out.println("我是饿汉模式");
}
/* 提供一个对外方法,通过此方法可以获得该类的一个实例
这里不需要加synchronized,因为创建对象时,此类是在类加载器中加载的,具备天然的线程安全
而且不加synchronized可以提高效率
* */
public static /*synchronized*/ Singleton_Hungry getInstance(){
return instance;
}
}
打印结果:
我是饿汉模式
我是Thread1,他们地址返回true
我是Thread2,他们地址返回true
我是Thread2,他们地址返回true
我是Thread2,他们地址返回true
我是main,他们地址返回true
我是Thread1,他们地址返回true
我是Thread1,他们地址返回true
从结果可以看出,不管是在哪个位置,只要Singleton_Hungry被创建了,不管被创建几次,都是只有一个实例。
注意
:
- 这种方式的缺点是如果这个对象
没有被调用
,就会浪费内存
懒加载代码实现
暂时没用线程,线程在下面聊到
在这里插入代码片public class Lazy_load {
public static void main(String[] args) {
// 测试单例模式
Lazy_model instance1 = Lazy_model.getInstance();
Lazy_model instance2 = Lazy_model.getInstance();
// 两者的地址相等,对外是同一个对象
System.out.println("我是main,他们地址返回"+(instance1 == instance2));
}
}
class Lazy_model {
private static Lazy_model instance;
// 私有构造函数,使得在其它方法上不能被调用,必须通过内部方法进行实例化
private Lazy_model() {
System.out.println("我是懒加载模式");
}
/*先不加同步锁synchronized也可以达到效果,在本程序中
* */
public static /*synchronized*/ Lazy_model getInstance() {
if (instance==null){
instance = new Lazy_model();
}
return instance;
}
}
打印结果如下:
我是懒加载模式
我是main,他们地址返回true
对比饿汉模式,实际上只改了这块:
乍一看好像懒加载和饿汉其实一样,怎么才突出这个懒
字呢?来,我们对两个模式的代码进行修改,我都在其中增加了print()打印这个函数:
然后打印结果分别为:
饿汉:
我是饿汉模式
我被打印了
我被打印了
我是main,他们地址返回true
懒加载:
我被打印了
我是懒加载模式
我被打印了
我是main,他们地址返回true
我们看到在饿汉模式下,明明在return前执行的print()方法,居然在构造函数打印语句的后边,所以这就是懒加载
,只有真正要的时候
才会调用构造函数进行初始化,判断变量是否为null,为null在创建对象
。但这种方式的问题是多线程环境中线程不安全
,一个线程判断为null创建对象时,还没初始化时,另一个线程判断也为null,此时就会创建多个实例,例如下面这个代码👇:
// 线程3用来创建Lazy_model实例
class Thread3 extends Thread{
@Override
public void run() {
Lazy_model instance1 = Lazy_model.getInstance();
System.out.println("在线程中的地址:"+instance1.toString());
}
}
public class Lazy_load {
public static void main(String[] args) {
// 启动线程,同时对Lazy_model进行实例化
Thread t1 = new Thread3();
Thread t2 = new Thread3();
t1.start();
t2.start();
// 测试单例模式
Lazy_model instance1 = Lazy_model.getInstance();
Lazy_model instance2 = Lazy_model.getInstance();
// 两者的地址相等,对外是同一个对象
System.out.println("得到两个地址分别为:"+'\n'+instance1.toString() +'\n'+instance2.toString());
}
}
class Lazy_model {
private static Lazy_model instance;
// 私有构造函数,使得在其它方法上不能被调用,必须通过内部方法进行实例化
private Lazy_model() {
}
private static void print(){
System.out.println("我被打印了");
}
/*没加同步锁synchronized
* */
public static /*synchronized*/ Lazy_model getInstance() {
// print();
if (instance==null){
instance = new Lazy_model();
}
return instance;
}
}
我们看看打印结果:
在线程中的地址:Lazy_model@7510bd5c
得到两个地址分别为:
Lazy_model@4554617c
Lazy_model@4554617c
在线程中的地址:Lazy_model@7aa3aa20
可以看到,没加synchronized
时打印出来的地址不一样!!!
我们看看加了synchronized(同步锁)后的代码和结果:
在线程中的地址:Lazy_model@4554617c
得到两个地址分别为:
Lazy_model@4554617c
Lazy_model@4554617c
在线程中的地址:Lazy_model@4554617c
可以看到,地址都是一样的!!!所以要使用懒加载一定要加上synchronized
修饰!!
注意
:
- 使用被synchronized修饰的方法后,
多个线程同时访问这个方法时会阻塞,性能很低
。
所以要来介绍第三种方式…
双重检测锁代码实现
class Thread4 extends Thread{
@Override
public void run() {
DoubleCheckImpl doubleCheck = DoubleCheckImpl.getInstance();
// 打印实例地址
System.out.println(doubleCheck.toString());
}
}
public class DoubleCheck {
public static void main(String[] args) {
//启动了三个线程
Thread t1 = new Thread4();
Thread t2 = new Thread4();
Thread t3 = new Thread4();
t1.start();
t2.start();
t3.start();
}
}
class DoubleCheckImpl{
/* 添加了volatile关键字,被volatile关键字修饰的变量,如果值发生了变更,其他线程立马可见,避免出现脏读的现象
这里不加也可以,不过volatile变量通常能够减少同步的性能开销
* */
private volatile static DoubleCheckImpl instance;
private DoubleCheckImpl(){
}
// 公共可调用方法
public static DoubleCheckImpl getInstance(){
// 首先判断是否有创建实例,有可能会有线程同时通过这个instance==null
if (instance==null){
// 然后给下面这个方法上锁,将同步内容放到下边if下
synchronized (DoubleCheckImpl.class){
// 然后这里就变成了上边通行过来的两个线程在这排队,先是A线程通过
if (instance==null){
//A 线程里的instance没有实例,为null,故执行下边的实例方法
instance = new DoubleCheckImpl();
}
/*因为instance已经被A线程实例了,这时instance不是null了,
所以还排在上边的线程B再在判断instance是否为null时就是false了
* */
}
}
return instance;
}
}
打印结果:
DoubleCheckImpl@3d3e384b
DoubleCheckImpl@3d3e384b
DoubleCheckImpl@3d3e384b
三个地址一样!大家留意一下我注释的内容
,写的挺详细的应该嘻嘻^^
如果不进行双重检测会怎么样呢?
打印结果:
DoubleCheckImpl@3d6f32ba
DoubleCheckImpl@3c494859
DoubleCheckImpl@3c494859
果然,有线程一起通过了第一个if判断,然后就实例了两个出来…
注意
:
- 将对象声明为volatitle后,重排序在多线程环境中将会被禁止,所以使得线程的执行结果顺序不会改变,
不会出行线程访问到的是一个还未初始化的对象
(使用volatile的好处)。
静态内部类代码实现(也是懒加载的一种方式)
class Thread5 extends Thread{
@Override
public void run() {
TestInner testInner = TestInner.getInstance();
System.out.println("在线程中的地址:"+testInner.toString());
}
}
public class StaticInner {
public static void main(String[] args) {
// 有三个线程同时实例
Thread t1 = new Thread5();
Thread t2 = new Thread5();
Thread t3 = new Thread5();
t1.start();
t2.start();
t3.start();
// TestInner testInner = TestInner.getInstance();
// TestInner testInner1 = TestInner.getInstance();
// System.out.println(testInner==testInner1);
}
}
class TestInner{
// 声明静态内部类,类中有一个获取实例的属性testInner
private static class InstanceClass{
private static TestInner testInner = new TestInner();
}
private TestInner(){
}
// 测试是否为像懒加载模式那样延迟加载,发现下面代码先执行,所以是懒加载的一种
private static void print(){
System.out.println("我被打印了");
}
public static /*synchronized*/ TestInner getInstance(){
print();
// 返回静态内部类中的testInner属性
return InstanceClass.testInner;
}
}
打印结果:
我被打印了
我被打印了
我被打印了
在线程中的地址:TestInner@70d192f2
在线程中的地址:TestInner@70d192f2
在线程中的地址:TestInner@70d192f2
三个线程的地址都相同,说明没有实例化多个。
注意
:
- 只有真正调用getInstance(),才会加载静态内部类,所以说是
懒加载的一种方式
。 - 兼备了
并发高效调用
和延迟加载
的优势。 - 似乎静态内部类看起来
已经是最完美的方法
了,其实不是,可能还存在反射攻击或者反序列化攻击
。
枚举代码实现
class Thread6 extends Thread{
@Override
public void run() {
Singleton singleton1 = Singleton.INSTANCE;
System.out.println(singleton1);
}
}
public class EnumImpl {
public static void main(String[] args) {
// 启动三个线程
Thread t1 = new Thread6();
Thread t2 = new Thread6();
Thread t3 = new Thread6();
t1.start();
t2.start();
t3.start();
// Singleton singleton1 = Singleton.INSTANCE;
// Singleton singleton2 = Singleton.INSTANCE;
// System.out.println(singleton1==singleton2);
}
}
enum Singleton{
INSTANCE("实例",1);
private int code;
private String name;
public int getCode(){
return code;
}
public String getName(){
return name;
}
Singleton(String name, int code){
this.name = name;
this.code = code;
}
}
运行结果:
INSTANCE
INSTANCE
INSTANCE
♦ 总结
用一个表格来概括他们的优缺点吧👇:
优点 | 缺点 | |
---|---|---|
饿汉式 | 简单方便 ,线程安全 | 静态对象在类加载时就要生成,会降低应用的启动速度 ,存在反射攻击的风险 |
懒加载 | 延迟加载,可以提高应用的启动速度 | 线程不安全,多线程下需要加synchronized锁 ,降低性能 |
双重检测锁 | 懒加载,线程安全,效率高 | 代码比较复杂 |
静态内部类 | 实现简单,懒加载,线程安全 | 存在反射攻击的风险 |
枚举 | 线程安全,不用担心序列化和反射问题 | 枚举占用的内存会多一些 ,没有延迟加载 |
所以根据自己的需求,如是否需要延迟加载,是否需要提示性能,是否需要防范反射攻击等来选择适合的单例模式。
——————————————————————————————————————————————————————————————
一起加油吧❤!!!