单例模式大全:细说七种线程安全的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虚拟机、反射机制、网络传输等等,尽管这么多种单例写法看来有点华而不实,日常工作中也不一定会用到,但抱着学习心态去阅读,也能帮诸位将多方面的零散性知识串联起来,从而进一步巩固自己的知识体系。

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

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

相关文章
|
9天前
|
安全 Java 测试技术
Java并行流陷阱:为什么指定线程池可能是个坏主意
本文探讨了Java并行流的使用陷阱,尤其是指定线程池的问题。文章分析了并行流的设计思想,指出了指定线程池的弊端,并提供了使用CompletableFuture等替代方案。同时,介绍了Parallel Collector库在处理阻塞任务时的优势和特点。
|
5天前
|
安全 Java 开发者
深入解读JAVA多线程:wait()、notify()、notifyAll()的奥秘
在Java多线程编程中,`wait()`、`notify()`和`notifyAll()`方法是实现线程间通信和同步的关键机制。这些方法定义在`java.lang.Object`类中,每个Java对象都可以作为线程间通信的媒介。本文将详细解析这三个方法的使用方法和最佳实践,帮助开发者更高效地进行多线程编程。 示例代码展示了如何在同步方法中使用这些方法,确保线程安全和高效的通信。
25 9
|
8天前
|
存储 安全 Java
Java多线程编程的艺术:从基础到实践####
本文深入探讨了Java多线程编程的核心概念、应用场景及其实现方式,旨在帮助开发者理解并掌握多线程编程的基本技能。文章首先概述了多线程的重要性和常见挑战,随后详细介绍了Java中创建和管理线程的两种主要方式:继承Thread类与实现Runnable接口。通过实例代码,本文展示了如何正确启动、运行及同步线程,以及如何处理线程间的通信与协作问题。最后,文章总结了多线程编程的最佳实践,为读者在实际项目中应用多线程技术提供了宝贵的参考。 ####
|
5天前
|
监控 安全 Java
Java中的多线程编程:从入门到实践####
本文将深入浅出地探讨Java多线程编程的核心概念、应用场景及实践技巧。不同于传统的摘要形式,本文将以一个简短的代码示例作为开篇,直接展示多线程的魅力,随后再详细解析其背后的原理与实现方式,旨在帮助读者快速理解并掌握Java多线程编程的基本技能。 ```java // 简单的多线程示例:创建两个线程,分别打印不同的消息 public class SimpleMultithreading { public static void main(String[] args) { Thread thread1 = new Thread(() -> System.out.prin
|
8天前
|
Java
JAVA多线程通信:为何wait()与notify()如此重要?
在Java多线程编程中,`wait()` 和 `notify()/notifyAll()` 方法是实现线程间通信的核心机制。它们通过基于锁的方式,使线程在条件不满足时进入休眠状态,并在条件满足时被唤醒,从而确保数据一致性和同步。相比其他通信方式,如忙等待,这些方法更高效灵活。 示例代码展示了如何在生产者-消费者模型中使用这些方法实现线程间的协调和同步。
22 3
|
7天前
|
安全 Java
Java多线程集合类
本文介绍了Java中线程安全的问题及解决方案。通过示例代码展示了使用`CopyOnWriteArrayList`、`CopyOnWriteArraySet`和`ConcurrentHashMap`来解决多线程环境下集合操作的线程安全问题。这些类通过不同的机制确保了线程安全,提高了并发性能。
|
8天前
|
Java
java小知识—进程和线程
进程 进程是程序的一次执行过程,是系统运行的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。简单来说,一个进程就是一个执行中的程序,它在计算机中一个指令接着一个指令地执行着,同时,每个进程还占有某些系统资源如CPU时间,内存空间,文件,文件,输入输出设备的使用权等等。换句话说,当程序在执行时,将会被操作系统载入内存中。 线程 线程,与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中产生多个线程。与进程不同的是同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或是在各个线程之间做切换工作时,负担要比
18 1
|
8天前
|
Java UED
Java中的多线程编程基础与实践
【10月更文挑战第35天】在Java的世界中,多线程是提升应用性能和响应性的利器。本文将深入浅出地介绍如何在Java中创建和管理线程,以及如何利用同步机制确保数据一致性。我们将从简单的“Hello, World!”线程示例出发,逐步探索线程池的高效使用,并讨论常见的多线程问题。无论你是Java新手还是希望深化理解,这篇文章都将为你打开多线程的大门。
|
8天前
|
安全 Java 编译器
Java多线程编程的陷阱与最佳实践####
【10月更文挑战第29天】 本文深入探讨了Java多线程编程中的常见陷阱,如竞态条件、死锁、内存一致性错误等,并通过实例分析揭示了这些陷阱的成因。同时,文章也分享了一系列最佳实践,包括使用volatile关键字、原子类、线程安全集合以及并发框架(如java.util.concurrent包下的工具类),帮助开发者有效避免多线程编程中的问题,提升应用的稳定性和性能。 ####
36 1
|
12天前
|
设计模式 安全 Java
Java编程中的单例模式深入解析
【10月更文挑战第31天】在编程世界中,设计模式就像是建筑中的蓝图,它们定义了解决常见问题的最佳实践。本文将通过浅显易懂的语言带你深入了解Java中广泛应用的单例模式,并展示如何实现它。