单例模式大全:细说七种线程安全的Java单例实现,及数种打破单例的手段!

简介: 设计模式,这是编程中的灵魂,用好不同的设计模式,能使你的代码更优雅/健壮、维护性更强、灵活性更高,而众多设计模式中最出名、最广为人知的就是Singleton Pattern单例模式。通过单例模式,我们就可以避免由于多个实例的创建和销毁带来的额外开销,本文就来一起聊聊单例模式。

引言

设计模式,这是编程中的灵魂,用好不同的设计模式,能使你的代码更优雅/健壮、维护性更强、灵活性更高,而众多设计模式中最出名、最广为人知的就是Singleton Pattern单例模式。单例模式是一种创建型设计模式,它确保一个类只会有一个实例,并提供一个全局共用的访问点来获取这个实例。

在很多场景下,比如配置管理类、线程池、各种Dao/Service对象、数据库连接池等,我们其实只需要一个实例就够了,毕竟这些实例都可以共用,每次使用时创建一个实例,反而会带来初始化损坏与额外的内存资源浪费。通过单例模式,我们就可以避免由于多个实例的创建和销毁带来的额外开销,本文就来一起聊聊单例模式。

一、全解单例模式

单例模式精髓可以用一句话概述:私有化构造函数,公开化获取实例方法,因为将构造函数私有化了,意味着外部就无法通过new关键字,来创建对应的对象实例,这时只能通过提供的getInstance()方法获得对象实例。

单例单例,这就说明全局只有一个对象实例,任意时刻、任意位置调用getInstance()方法,拿到的都是同一个对象,这就叫做单例模式。而getInstance()方法获得的对象什么时候被创建出来呢?根据创建时机的不同,就能分为不同的单例模式,如著名的饿汉式、懒汉式,下面一起来展开聊聊。

1.1、饿汉式单例

饿汉式单例,特性如同其名称,就跟一个从未吃饱饭的饿汉子一样,见到饭的第一眼就是干饱为止。将这个思想放到单例模式里,饿汉式单例是指:类加载的时候就立即初始化单例对象,带来的优势很明显,因为在类加载的时候就创建好了对象,这个时候还没有用户线程出现,所以这个单例对象绝对是线程安全的。

可缺点同样很明显,因为类加载时就创建了对象,如果后续很长时间没有线程来获取该对象,就会导致这个对象一直占用着内存,造成一定程度上的内存浪费,这也可以理解成一种另类的内存泄露场景。好了,那饿汉式单例怎么实现呢?代码如下:

public class HungerSingleton {
   
   
    // 静态的实例对象,类加载阶段时就会被初始化
    private static final HungerSingleton instance = new HungerSingleton();

    /*
    * 私有化构造函数
    * */
    private HungerSingleton() {
   
   }

    /*
    * 公开化的获取实例方法
    * */
    public static HungerSingleton getInstance() {
   
   
        return instance;
    }
}

正如上述代码所示,因为构造函数变为了private关键字修饰,代表外部不可能再通过new创建出实例,只能通过getInstance()方法获取,而学习过之前《JVM类加载机制》的小伙伴应该明白,被static关键字修饰的instance成员,就会在该阶段进行初始化。

当然,饿汉式除开上述这种写法外,还有另一种写法:

public class HungerSingleton {
   
   

    private static final HungerSingleton instance;

    // 类加载阶段时会初始化静态代码块创建单例对象
    static {
   
   
         instance = new HungerSingleton();
    }

    private HungerSingleton() {
   
   }

    public static HungerSingleton getInstance() {
   
   
        return instance;
    }
}

这种方式的原理也一样,静态代码块和静态变量的初始化时机相同,都会在类加载的时候触发,所以这里不过多废话。

1.2、 懒汉式单例

为了解决饿汉式单例在类加载阶段被提前创建导致的内存浪费问题,就产生了与之相反的懒汉式单例,而懒汉式单例则是懒加载思想的落地,懒加载思想的核心是:只有真正用的时候才会创建对象,对应的写法如下:

public class LazySingleton {
   
   

    // 静态的实例对象
    private static LazySingleton instance;

    /*
    * 私有化构造函数
    * */
    private LazySingleton() {
   
   }

    /*
    * 公开化的获取实例方法
    * */
    public static LazySingleton getInstance() {
   
   
        if (instance == null) {
   
   
            instance = new LazySingleton();
        }
        return instance;
    }
}

这种写法是经典的懒加载思想,最开始先声明一个对象,但并不会立马将其初始化,只有真正有线程来调用getInstance()方法获取实例时,再判断一下instance是否为空,如果为空才创建一个对象返回,如果不为空则获取已创建好的实例返回。

这种模式对内存最友好,只有真正需要用到时才会在内存创建对象,而不会提前创建占用内存。可是,懒汉式同样有着致命缺点,即在多线程环境下,会存在线程安全问题,为啥呢?我们来分析一下。

如果此时出现T1、T2两条线程同时调用getInstance()方法,那就有可能同时执行if (instance == null)这行判空的代码,因为两条线程在并行执行,那么T1、T2看到的instance变量都为null,这时T1、T2就会各自new一个实例对象,此时就出现了两个对象,从而违反了单例模式的特性。

Java中如何保证线程安全?经验老道的小伙伴,下意识就会回答出synchronized,所以想要解决懒汉式加载带来的线程安全问题,只需要在getInstance()方法上加个关键字即可:

public static synchronized LazySingleton getInstance() {
   
   
    if (instance == null) {
   
   
        instance = new LazySingleton();
    }
    return instance;
}

通过synchronized可以有效保证多线程环境下的安全性,可这种方式显然会影响性能,因为将synchronized关键字加在方法上,会导致锁的颗粒度变大,每次线程调用getInstance()方法前,都需要先获取到锁才能执行,代表所有线程都会被串行化,性能将会变得尤为低效。

1.3、双重锁单例(DCL)

如果直接将synchronized关键字加在方法上,会导致getInstance()方法性能特别差,怎么办?

大家先想想,我们为什么要加synchronized关键字?为了解决首次初始化单例对象时,多条线程同时调用、并行执行判空代码,从而创建出多个对象实例的问题,正因如此,业界提出了一种名为双重锁单例的实现方式,如下:

public class DCLSingleton {
   
   

    // 静态的实例对象
    private static volatile DCLSingleton instance;

    /*
    * 私有化构造函数
    * */
    private DCLSingleton() {
   
   }

    /*
    * 公开化的获取实例方法
    * */
    public static DCLSingleton getInstance() {
   
   
        if (instance == null) {
   
   
            synchronized (DCLSingleton.class) {
   
   
                if (instance == null) {
   
   
                    instance = new DCLSingleton();
                }
            }
        }
        return instance;
    }
}

因为最初的懒汉式单例写法,只有在首次初始化单例对象时才需要做空乏控制,也只有这个时候才存在线程安全问题。为此,上述的双重锁单例模式中,如果是第一次调用getInstance()方法,这时第一个if判空会成立,然后才会获取锁去创建单例对象。

而后续调用getInstance()方法的线程,因为instance已经不为空了,所以就不需要再获取锁创建对象,第一个判断不会再成立,那么会直接获取已有对象返回,这样既能保证线程安全,又能保证性能可观。

大家会看到,instance成员还会被volatile关键字修饰,同时synchronized关键字里面还有个if判断,这对许多未学习过并发编程的小伙伴来说有点不理解,怎么办?我们先来看下双重锁单例在多线程环境下的执行流程,如下(该图是以前的流程图):

1.png

1.3.1、为什么需要第二个if判空存在?

大家可以简单看下前面图中标注的流程,先来解释下为什么synchronized里还要加个if判断?因为synchronized是阻塞式锁。还是用之前的例子来说明,T1、T2两条线程一起调用getInstance()方法,那么会同时执行第一个if (instance == null)的判空代码,这时T1、T2都会满足条件,然后执行if里面的代码。

执行if里面的代码时,因为是synchronized关键字保护的临界资源,在执行前则需要获取到指定的class类锁,假设T1竞争锁成功,那么T2会陷入阻塞状态;T1继续往下执行,继续执行第二个if (instance == null)判空动作,因为T1最先来,所以条件肯定成立,所以T1会执行new指令创建一个对象并返回。

T1执行完成后会释放对应的锁资源,这时阻塞的T2线程会被唤醒,注意看,之前T2被阻塞在synchronized这处代码,因为T1已经释放锁了,所以T2肯定能拿到锁,继续往下执行,如果里面没有第二个if判空动作,那么T2必定会再创建出一个对象,从而打破单例模式的特性,而第二个if的作用就是这个:为了防止抢锁失败陷入阻塞的线程,被再次唤醒后再次创建一个对象实例

1.3.2、为什么需要加volatile关键字?

好,接着来看第二个疑惑,为什么instance需要用volatile关键字修饰呢?想要说清楚原因,这就牵扯到了之前聊过的《Java内存模型-JMM机制》,首先明确一点,Java中执行new关键字创建一个对象时,并非一步到位的,而是会分成三步执行:

// 1.分配对象内存空间
memory = allocate();

// 2.初始化对象
instance(memory); 

// 3.设置instance指向刚分配的内存地址,此时instance != null
instance = memory;

上面是new指令执行时对应的伪代码,这段代码放在单线程环境下执行没有任何问题,可放在多线程环境里,就会存在一定隐患,为什么呢?因为CPU也好,操作系统也罢,都是从单线程发展而来,早期为了尽可能的保证CPU指令按流水线形式执行,编译器、处理器通常会在不破坏单线程语义的前提下,对指令进行重排优化。

经过指令重排优化后,上述伪代码就有可能从原本的1-2-3这个顺序,变为1-3-2这个顺序,毕竟步骤2、步骤3之间不存在数据依赖关系,无论重排前还是重排后,在单线程的语义中并没有改变,即在单线程环境下的执行结果都是一致的,为此,这种重排优化也是允许的。

也正是这种重排优化,在多线程环境会引发意料之外的问题,毕竟指令重排只会保证串行语义的执行一致性,并不关心多线程间的语义一致性。因此,当T1线程执行完instance = new DCLSingleton()代码后就会释放锁,而这时这行代码对应的指令,已经被重排成了1-3-2顺序,就有可能出现“instance已经指向某个内存地址,但对应内存处还未初始化对象”,接着T1就释放了锁。

于是T2被唤醒后,拿到锁发现instance已经指向某个地址,这时会直接拿着instance去进行操作,但实际上instance未必完成了初始化,所以T2拿着instance去操作时就会出错,如空指针异常。

综上所述,为了解决指令重排带来的问题,我们在这里使用volatile修饰instance变量,从而禁止new这行代码对应的指令与其他指令发生重排序。好了,到这里也讲明白了DCL双重锁单例,这种单例写法在诸多框架的源码中都可以看见,例如大名鼎鼎的Spring框架。

同时,双重锁单例也是理解并发编程、JVM路上必经的一道坎,因为它涵盖了JMM、JVM、synchronized等多方面的知识,理解透了这个案例,能让你对并发、虚拟机的认知进一步加深。不过话说回来,虽然这种方式能提升性能,但对并发编程接触较少的小伙伴不太友好,因为很难理解为什么要这么写~

1.4、枚举式单例

除开前面通过类实现单例模式外,如果不考虑实用性,其实枚举才是最适合单例的模式,因为它足够简单,既不会有性能问题,也不用考虑线程安全、内存浪费、单例被破坏(后面细说)等问题:

public enum EnumSingleton {
   
   
    /*
    * 定义枚举1
    * */
    INSTANCE;

    /*
    * 获取单例对象
    * */
    public EnumSingleton getInstance() {
   
   
        return INSTANCE;
    }
}

其实枚举的底层还是一个class类,感兴趣的小伙伴通过javap反编译看下,不过这种单例的意义不大,毕竟实际开发场景中,做成单例模式的对象,一定具备业务或技术价值,而单纯的枚举类,只能用来表示某个常量,不具备实际的价值。因此,尽管枚举能很轻易的实现一个完美的单例对象,可它中看不中用~

1.5、容器式单例

好了,下面介绍一种另类的单例模式,也是大家日常开发接触最多的一种,即容器单例模式,是不是有点耳熟?没错,SpringIOC容器,其中存放的每个Bean默认就是单例的,而IOC本质上就是一个大的容器,实现如下:

@Slf4j
public class ContainerSingleton {
   
   

    // 单例容器
    private static Map<String, Object> singletonMap = new HashMap<>();

    /*
    * 私有化构造函数
    * */
    private ContainerSingleton() {
   
   }

    /*
    * 公开化的获取实例方法
    * */
    public static synchronized Object getInstance(String className) {
   
   
        Object instance = null;
        // 如果容器里没有对应的单例对象,则初始化放进去一个
        if (!singletonMap.containsKey(className)) {
   
   
            try {
   
   
                instance = Class.forName(className).newInstance();
                singletonMap.put(className, instance);
                return instance;
            } catch (InstantiationException | IllegalAccessException | ClassNotFoundException e) {
   
   
                log.error("Load Singleton Object Failed : {}", className);
            }
        }
        // 如果容器中有,则直接从容器中获取并返回
        return singletonMap.get(className);
    }
}

这其实就是一个单例对象管理容器,首先定义一个大的Map保存所有单例对象,当线程试图获取某个单例对象时,需要传入目标对象的全限定名,而后会在容器里匹配,如果容器里没有对应的单例对象,就会通过反射机制创建一个实例放进去,接着返回创建好的单例对象。反之,如果容器中已经存在对应的单例对象,则直接返回即可。

当然,上面为了线程安全,又直接在getInstance()方法上加了synchronized关键字,而这个方法还是一个static静态方法,为此,这个锁的颗粒度比较粗,并发冲突特别高,能否优化呢?答案是当然可以,大家可以自行尝试一下,如何降低锁的颗粒度,或者通过其他手段来避免并发冲突。

1.6、静态内部类

前面的双重锁单例模式,已经竭尽所能将锁的颗粒度压缩至最小了,可是不管怎么说,终归还是用到了锁机制,而用到锁就会产生线程竞争与性能问题,那能不能不用锁呢?答案是可以,一开始的饿汉式加载,显然就没有用到锁,只不过带来了内存资源浪费罢了。

既然如此,有没有不用锁,同时不会浪费内存的方式?没错,就是鱼和熊掌都想要!答案还是有,那就是通过静态内部类的方式:

public class StaticInnerClassSingleton {
   
   

    /*
    * 私有化构造函数
    * */
    private StaticInnerClassSingleton() {
   
   }

    /*
    * 公开化的获取实例方法
    * */
    public static StaticInnerClassSingleton getInstance() {
   
   
        return LazyHandler.INSTANCE;
    }

    /*
    * 静态内部类
    * */
    private static class LazyHandler {
   
   
        private static final StaticInnerClassSingleton INSTANCE = new StaticInnerClassSingleton();
    }
}

静态内部类其实利用了Java类加载的一种特性,静态内部类在主类加载时并不会被加载,只有当调用getInstance()方法用到内部类时,才会被加载进行初始化

1.7、CAS自旋单例

到目前为止,前面已经叙述了六种不同的单例实现方式,可上面的方式,多少都有直接或间接的了synchronized的保障线程安全,为什么这么说呢?比如前面的饿汉式、枚举、静态内部类,本质都是借助类加载的时候来初始化单例对象,即依赖于ClassLoader的线程安全机制。

如果对类加载机制比较熟悉的小伙伴应该知道,ClassLoader的线程安全机制依赖于synchronized实现,如下:

002.png

在类加载的源码中,第一行代码就是synchronized关键字,因此,除非被重写loadClass()方法,否则默认在类加载过程中都会依赖synchronized保障线程安全。那么如果不依赖于synchronized,又该如何实现一个线程安全的单例呢?

其实在《并发编程专栏》中提到过一种保证线程安全的无锁机制,即CAS结合自旋的乐观锁策略。

这种无锁机制,当出现多条线程同时更新同一个变量时,也能保证只会有一个线程能成功更新变量的值。至于更新失败的线程也不会被挂起,而是被告知这次更新失败,可以再次尝试更新,对应实现的单例代码如下:

public class CasSingleton {
   
   

    /*
    * 原子引用类型
    * */
    private static final AtomicReference<CasSingleton> INSTANCE = 
                            new AtomicReference<CasSingleton>();

    /*
    * 私有化构造函数
    * */
    private CasSingleton() {
   
   }

    /*
    * 公开化获取单例方法
    * */
    public static CasSingleton getInstance() {
   
   
        // 开启自旋
        for (; ;) {
   
   
            // 获取原子引用中的单例对象
            CasSingleton singleton = INSTANCE.get();
            // 如果单例对象已经被创建,则直接返回获取到的单例对象
            if (null != singleton) {
   
   
                return singleton;
            }

            // 如果原子引用中的单例对象还未被初始化,则创建一个对象放进去
            singleton = new CasSingleton();
            if (INSTANCE.compareAndSet(null, singleton)) {
   
   
                return singleton;
            }
        }
    }
}

这里用到了JUC包中提供的原子引用类,想要理解这种单例模式,最主要的是要理解getInstance()方法开头的自旋逻辑,来看T1、T2线程同时调用该方法的执行逻辑:

  • T1、T2线程同时调用getInstance()方法,并执行内部逻辑;
  • T1、T2线程同时开启死循环,都先从原子引用器中读取单例对象;
  • T1、T2都会发现单例对象未初始化,于是各自new个对象并尝试CAS更新;
  • ④假设T1CAS先成功,那么T1会返回true,然后进入if并返回创建的单例对象;
  • T2线程CAS失败,得到的结果为false,本次循环结束,继续下次循环;
  • T2线程再次会回到循环的一开始,这时从原子引用器里拿到的对象不为空,则直接返回。

通过这种CAS+自旋机制,尽管这里没有用到锁,但也依旧能够保证线程安全,所以这是一种完全不依赖于锁机制实现的单例模式,而INSTANCE.compareAndSet(null,singleton)这一步操作是如何保证原子性的?大家对底层原理感兴趣可参考之前的《JUC-原子引用器实现原理》

二、破坏单例模式的手段

至此,我们已经讲述了七种实现单例的方式,经过不断推敲与演进,既考虑到了内存资源,又考虑到了线程安全,还考虑到了性能问题,终于逐步找到了最完美的单例实现,可静态内部类这种方式真的完美吗?实则不然,这种方式,包括前面的多种方式,其实都存在安全隐患,如果不把安全性考虑进去,那么设计出的单例模式将有可能被破坏!

2.1、通过反射机制破坏单例

在前面给出的大多数单例写法中,都是将构造函数加上private关键字修饰,从而避免外部直接new的方式来创建对象,但这只能防住正规手段的创建方案,而Java中还有一种暴力的途径,即强大的反射机制!如下:

public static void main(String[] args) {
   
   
    try {
   
   
        Class<?> clazz = StaticInnerClassSingleton.class;
        Constructor<?> constructor = clazz.getDeclaredConstructor(null);
        constructor.setAccessible(true);
        Object instance1 = constructor.newInstance();
        Object instance2 = constructor.newInstance();
        System.out.println(instance1 == instance2);
    } catch (Exception e) {
   
   
        e.printStackTrace();
    }
}

// 输出结果:false

这是基于之前的静态内部类的单例写法,使用反射技术暴力破除单例的例子,通过反射机制提供的setAccessible(true)方法,可以强制越过访问修饰符的访问权限控制,尽管使用了private修饰构造函数,反射机制也能直接将权限置为true,从而能正常调用构造函数,并且能通过constructor.newInstance()多次调用了构造函数创建不同的实例对象。

因为调用了两次constructor.newInstance()构造函数,所以拿到了instance1、instance2两个实例,从上述代码的输出结果来看,==比较的是引用地址,false表示这两并非同一个对象,这就从侧面印证了反射机制的确能成功破坏前面的单例写法。

那么,我们又该如何防止反射呢?很简单,在构造方法中加一个判断即可:

/*
* 私有化构造函数
* */
private StaticInnerClassSingleton() {
   
   
    if (LazyHandler.INSTANCE != null) {
   
   
        throw new RuntimeException("请不要使用非法手段挑战我的底线……");
    }
}

因为构造函数里主动引用了LazyHandler.INSTANCE,所以会被动触发静态内部类的加载,此时就会先初始化一个对象实例。这时,当通过反射机制调用构造函数时,就会发现单例对象已经不为空了,最终抛出异常阻止反射机制继续创建对象。

但对于懒加载式的单例写法而言,因为单例对象并不会随着类加载时初始化,构造函数里又该怎么写呢?以双重锁单例来说明:

private DCLSingleton() {
   
   
    if (instance != null) {
   
   
        throw new RuntimeException("请不要使用非法手段挑战我的底线……");
    }
    instance = this;
}

毕竟反射最终都会调用到构造函数,所以这里先判断单例对象是否为空,如果不为空,继续抛出异常阻止创建对象。如果为空,说明单例对象还未初始化,那么则将创建的当前对象this,赋值给单例对象,这样也能保证实例的全局唯一性,只不过这里要考虑到反射与getInstance()方法一起调用的并发冲突问题。

2.2、通过反序列化机制破坏单例

前面通过在私有化构造函数里加判断,解决了反射机制破坏单例模式,可除开反射机制外,还有另一种不走寻常路的手段,即序列化与反序列化机制。

首先我们在静态内部类单例上实现Serializable接口,然后把这个先获取一个单例对象,接着先序列化,再将其反序列化出来,最后对比一下:

public class StaticInnerClassSingleton implements Serializable {
   
   

    private static final long serialVersionUID = 1L;

    /*
    * 私有化构造函数
    * */
    private StaticInnerClassSingleton() {
   
   
        if (LazyHandler.INSTANCE != null) {
   
   
            throw new RuntimeException("请不要使用非法手段挑战我的底线……");
        }
    }

    /*
    * 公开化的获取实例方法
    * */
    public static StaticInnerClassSingleton getInstance() {
   
   
        return LazyHandler.INSTANCE;
    }

    /*
    * 静态内部类
    * */
    private static class LazyHandler {
   
   
        private static final StaticInnerClassSingleton INSTANCE = new StaticInnerClassSingleton();
    }

    public static void main(String[] args) {
   
   
        StaticInnerClassSingleton instance1 = StaticInnerClassSingleton.getInstance();
        StaticInnerClassSingleton instance2 = null;

        FileOutputStream fos = null;
        try {
   
   
            fos = new FileOutputStream("SerializeSingleton.obj");
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            oos.writeObject(instance1);
            oos.flush();
            oos.close();

            FileInputStream fis = new FileInputStream("SerializeSingleton.obj");
            ObjectInputStream ois = new ObjectInputStream(fis);
            instance2 = (StaticInnerClassSingleton) ois.readObject();
            ois.close();

            System.out.println(instance2);
            System.out.println(instance1);
            System.out.println(instance2 == instance1);

        } catch (Exception e) {
   
   
            e.printStackTrace();
        }
    }
}

重点看代码里的main()方法即可,里面的逻辑就是我们说的,先对得到的instance1单例对象进行序列化,接着对序列化后得到的文件进行反序列化,从而得到另一个实例instance2,这时来看运行结果:

输出结果:
com.zhuzi.demo.StaticInnerClassSingleton@3f91beef
com.zhuzi.demo.StaticInnerClassSingleton@38af3868
false

结果很感人,完全两个不同的地址,并且==对比的结果也是false,这意味啥?意味着单例模式又被序列化机制打破了,这又该怎么整?其实在之前关于序列化的文章里提到过:Serializable序列化会打破单例模式,解决的方案也很简单,手动重写一下readResolve()方法即可:

/*
* 手动重写readResolve()方法,防止反序列化时打破单例模式
* */
private Object readResolve() {
   
   
    return LazyHandler.INSTANCE;
}

这时再次运行前面的main()方法,就会发现反序列化出来的对象也是同一个啦:

com.zhuzi.demo.StaticInnerClassSingleton@38af3868
com.zhuzi.demo.StaticInnerClassSingleton@38af3868
true

关于具体原因,在前面给出的序列化文章的链接里面有提到,这里就不再反复说明了。

三、单例模式总结

叨叨絮絮,前面讲述了多种单例模式的实现方式,也提到了两种常见的破坏单例模式的技术,以及如何防止单例模式被打破的手段,本文的话题虽然很古老,但相信诸位认真看下来也一定会有许多收获。

想要彻底搞懂本文中提到的多种单例写法,这需要你具备扎实的基础功底,知识面覆盖了设计模式、并发编程、Java虚拟机、反射机制、网络传输等等,尽管这么多种单例写法看来有点华而不实,日常工作中也不一定会用到,但抱着学习心态去阅读,也能帮诸位将多方面的零散性知识串联起来,从而进一步巩固自己的知识体系。

最后,这种以点串线、以线连面的学习思想,各位一定也要将其掌握,很多知识从初学者角度看,或者从使用者角度去看,会发现都是零零散散分布的,可是当你真正掌握后,就能用一个点串起一条线,从而带出整个面。这样搭建出的知识体系,既不容易忘,而且有助于在面试过程中做到侃侃而谈~

所有文章已开始陆续同步至公众号:竹子爱熊猫,想在微信上便捷阅读的小伙伴可搜索关注~

相关文章
|
4天前
|
安全 Java UED
Java中的多线程编程:从基础到实践
本文深入探讨了Java中的多线程编程,包括线程的创建、生命周期管理以及同步机制。通过实例展示了如何使用Thread类和Runnable接口来创建线程,讨论了线程安全问题及解决策略,如使用synchronized关键字和ReentrantLock类。文章还涵盖了线程间通信的方式,包括wait()、notify()和notifyAll()方法,以及如何避免死锁。此外,还介绍了高级并发工具如CountDownLatch和CyclicBarrier的使用方法。通过综合运用这些技术,可以有效提高多线程程序的性能和可靠性。
|
4天前
|
缓存 Java UED
Java中的多线程编程:从基础到实践
【10月更文挑战第13天】 Java作为一门跨平台的编程语言,其强大的多线程能力一直是其核心优势之一。本文将从最基础的概念讲起,逐步深入探讨Java多线程的实现方式及其应用场景,通过实例讲解帮助读者更好地理解和应用这一技术。
19 3
|
8天前
|
Java 调度 UED
深入理解Java中的多线程与并发机制
本文将详细探讨Java中多线程的概念、实现方式及并发机制,包括线程的生命周期、同步与锁机制以及高级并发工具。通过实例代码演示,帮助读者理解如何在Java中有效地处理多线程和并发问题,提高程序的性能和响应能力。
|
5天前
|
缓存 安全 Java
使用 Java 内存模型解决多线程中的数据竞争问题
【10月更文挑战第11天】在 Java 多线程编程中,数据竞争是一个常见问题。通过使用 `synchronized` 关键字、`volatile` 关键字、原子类、显式锁、避免共享可变数据、合理设计数据结构、遵循线程安全原则和使用线程池等方法,可以有效解决数据竞争问题,确保程序的正确性和稳定性。
13 2
|
7天前
|
存储 安全 Java
Java-如何保证线程安全?
【10月更文挑战第10天】
|
8天前
|
Java
|
8天前
|
Java
【编程进阶知识】揭秘Java多线程:并发与顺序编程的奥秘
本文介绍了Java多线程编程的基础,通过对比顺序执行和并发执行的方式,展示了如何使用`run`方法和`start`方法来控制线程的执行模式。文章通过具体示例详细解析了两者的异同及应用场景,帮助读者更好地理解和运用多线程技术。
21 1
|
2天前
|
缓存 算法 Java
|
5天前
|
设计模式 SQL 安全
【编程进阶知识】Java单例模式深度解析:饿汉式与懒汉式实现技巧
本文深入解析了Java单例模式中的饿汉式和懒汉式实现方法,包括它们的特点、实现代码和适用场景。通过静态常量、枚举类、静态代码块等方式实现饿汉式,通过非线程安全、同步方法、同步代码块、双重检查锁定和静态内部类等方式实现懒汉式。文章还对比了各种实现方式的优缺点,帮助读者在实际项目中做出更好的设计决策。
22 0
|
7天前
|
Java 应用服务中间件 测试技术
Java21虚拟线程:我的锁去哪儿了?
【10月更文挑战第8天】
19 0