• Java编程--单例模式(饿汉模式/懒汉模式)/阻塞队列


           前言 

            逆水行舟,不进则退!!!     


           目录

           单例模式

            饿汉模式:       

            懒汉模式:

           什么是阻塞队列

            什么是高内聚 低耦合

           阻塞队列的实现


           单例模式

            单例模式(Singleton Pattern)是一种常见的设计模式,主要应用于创建型模式。它确保一个类只有一个实例,并且自行负责实例化并向整个系统提供这个唯一实例。此外,这种模式属于创建型模式,通过这种方式创建的类在当前进程中只存在一个实例。

            在计算机系统中,诸如线程池、缓存、日志对象、对话框、打印机、显卡的驱动程序对象常被设计成单例,因为它们在整个应用程序中只需要一个实例。然而,尽管单例模式是应用最广的设计模式之一,也是程序员非常熟悉的一种设计模式,但它仍然存在一些需要注意的问题。

            饿汉模式:       
    1. //饿汉模式单例模式
    2. //此处保证Singleton这个类只能创建出一个实例
    3. class Singleton{
    4. //在此处,先把这个实例给创建出来
    5. private static Singleton instance = new Singleton();
    6. //获取这个唯一实例
    7. public static SingletongetInstance() {
    8. return instance;
    9. }
    10. //为了避免Singleton类不小心被复制出多份来,
    11. //把构造方法设为private,在类外面,就无法通过new的方式来创建这个Singleton实例了
    12. private Singleton() { }
    13. }
    14. public class ThreadDemo6 {
    15. public static void main (String[] args) {
    16. Singletons1 = Singleton.getInstance();
    17. Singletons2 = Singleton.getInstance();
    18. System.out.println(s1==s2);
    19. }
    20. }

            懒汉模式:
    1. //懒汉模式
    2. class SingletonLazy{
    3. // 饿汉 与 懒汉 的 根本区别在这里
    4. private static SingletonLazy instance = null;
    5. public static SingletonLazygetInstance() {
    6. if (instance == null) {
    7. instance = new SingletonLazy ();
    8. }
    9. return instance;
    10. }
    11. private SingletonLazy() {};
    12. }
    13. public class ThreadDemo7 {
    14. public static void main (String[] args) {
    15. SingletonLazys1 = SingletonLazy.getInstance();
    16. SingletonLazys2 = SingletonLazy.getInstance();
    17. System.out.println(s1==s2);
    18. }
    19. }

          

            懒汉模式中有一个new的操作,这里就可能造成线程安全问题,而饿汉模式中只有读的操作,所以是线程安全的。

             懒汉模式的线程不安全 是因为读操作、比较、写操作不是原子性的。解决的办法就是将这三个操作上锁,使其变成原子性的。

            在懒汉模式中,实例化对象这个步骤可分为下图中的三步:

            不过,也是有办法解决的:volatile,volatile有两个功能:1,解决内存可见性;2,禁止指令重排序。

            或者使用 synchronized 来解决指令重排序问题。

    1. //懒汉模式
    2. classSingletonLazy{
    3. private volatile static SingletonLazy instance = null;
    4. public static SingletonLazy getInstance () {
    5. if (instance == null ) { //外层这个if 是判断是否加锁
    6. synchronized (SingletonLazy.class) {
    7. if (instance == null) { // 内层这个if 是判断是否实例化对象
    8. instance = new SingletonLazy();
    9. }
    10. }
    11. }
    12. return instance;
    13. }
    14. private SingletonLazy () {};
    15. }
    16. public class ThreadDemo7 {
    17. public static void main (String[] args) {
    18. SingletonLazys1 = SingletonLazy.getInstance();
    19. SingletonLazys2 = SingletonLazy.getInstance();
    20. System.out.println(s1 == s2);
    21. }
    22. }

    针对上面代码中提一个问题:既然外层的if已经判断为null了, 又有加锁操作,是否可以将内存的判断删掉呢?

            答:假设有t1、t2两个线程,两个线程都执行到了外层的if语句,并且判断都为null,但是呢,t1线程比t2线程略微快那么一点儿,t1线程先申请到了锁,所以t2线程就只能线程阻塞了,等到t1线程执行完毕后,已经实例化了对象,此时t2线程再去执行,这时的内层if语句就会将t2线程拦下。所以说,两个判断语句,一个都不能少。

            

    这里还是有一个问题: 比如t1线程通过synchronized已经对实例化操作进行加锁了,其他线程又如何将执行到一半的t1线程给切换走呢?

            答:线程加锁,并不是说这个线程就一直占用cpu,只是其他线程执行被上锁的代码时会阻塞,但是线程的切换调度还是照常进行的,并不会因为加锁而改变。(意思就是,已经上锁了,你的就是你的,别人拿不走,但是你先别着急,我的这个任务优先级更高,急需执行,所以你就先阻塞一下)。然而问题就是出现在这里,我t1线程执行到一半了,instance已经是非null了,但是还没有构造出对象。结果阻塞了,然后其他线程刚好在t1阻塞这个期间执行到了第一个if语句,发现instance不为null,结果就直接返回了instance,这个就导致了instance引用指向的对象不完整,引发后续问题。


           什么是阻塞队列
            什么是高内聚 低耦合

                    答:高内聚,这是一个软件工程中的重要概念,它是判断软件设计优劣的标准之一。具体来说,高内聚是指模块内部的元素关联性非常强,以至于模块的单一性非常显著。理想情况下,一个模块应尽可能独立地完成某个特定的功能。

                    在面向对象的设计中,高内聚低耦合是一个重要的设计原则,其主要目标是增强程序模块的可重用性和移植性。如果一个模块的内部实现过于复杂,可能会影响其可重用性和移植性。例如,如果一个模块需要被各种场景引用,那么代码的质量可能会变得非常脆弱,这种情况下建议将该模块拆分为多个独立的模块。

                    总的来说,高内聚是强调模块功能的独立性和完整性,而低耦合则是强调模块之间的相互独立性,它们共同构成了软件设计的重要原则。

            阻塞队列是一种在多线程编程中经常使用的线程同步工具,它的主要功能是控制生产者和消费者之间的数据流量。阻塞队列的“阻塞”特性带来了几个显著的优点:

                    1. 内存消耗可控:当队列容量有限时,内存消耗也会受到限制,防止过度消耗系统资源。

                    2. 平衡生产者和消费者速度:阻塞功能使得生产者和消费者两端的能力得以平衡。当有任何一端速度过快时,阻塞队列便会把过快的速度限制下来。

                    3. 自动线程阻塞与唤醒:在队列中没有数据的情况下,消费者端的所有线程都会被自动阻塞(挂起),直到有数据放入队列。这样的机制减少了手动管理线程的复杂性。

                    4. 指定超时等待:在获取元素时,如果队列为空,线程可以等待特定的超时时间。如果在超时时间内有数据加入队列,线程将继续执行;否则,可以选择放弃等待或采取其他补救措施。

                    5. 缓冲区长度可调节:一般的队列只能是有限长度的缓冲区,一旦超出缓冲长度,就无法保留了。当阻塞队列满时,阻塞队列会通过阻塞保留住当前想要继续入队的任务。

            总体而言,阻塞队列通过其阻塞特性,不仅使线程间的通信更加高效,而且减少了程序员需要处理的并发问题,大大提高了程序的稳定性和可靠性。

    阻塞队列为生产者消费者模型 带来了两个好处:

            1)解耦合;

                    简单理解:就是现在有AB两个程序,其中任何一个程序崩了,都会导致另一个程序的崩溃,这种就是高耦合现象,处处受其他程序掣肘。

                    阻塞队列的出现就是来降低两个程序之间的耦合,将AB程序之间的信息交流通过阻塞队列来实现,这样的话,任何一个程序挂了,但是阻塞队列还好着,另一个程序还是可以从队里中拿到其的请求。

            2)削峰填谷

                    "阻塞队列的削峰填谷"是一个在多线程编程中常用的术语,主要用于描述阻塞队列在处理并发任务时的一种重要功能。

                    1. 削峰:这是指当并发任务的数量过多,以至于系统无法及时处理时,阻塞队列可以起到缓冲的作用,避免系统因为瞬时的大量任务而崩溃。阻塞队列相当于一个缓冲区,平衡了生产者和消费者的处理能力。

                    2. 填谷:当并发任务的数量较少时,阻塞队列还可以存储这些暂时没有处理的任务,等到后续有新的任务到来时,再一起处理。这样可以避免系统的空闲资源浪费,提高了系统的处理效率。


           阻塞队列的实现
    1. //自己实现阻塞队列
    2. //此处不考虑泛型, 直接使用 int 来表示
    3. class MyBlockingQueue {
    4. private int[] items = new int[1000];
    5. private int head = 0; // 头指针
    6. private int tail = 0; // 尾指针
    7. private int size = 0; // 记录阻塞队列中元素的个数
    8. //入队列
    9. public void put(int value) throws InterruptedException {
    10. synchronized(this) {
    11. //在这里进行循环等待,因为 wait 可能被打断。
    12. while (size == items.length) {
    13. //队列满了,不能继续插入
    14. this.wait();
    15. // 自动阻塞,等待出队列代码(消费者)的 唤醒
    16. }
    17. //数组实现循环队列的处理
    18. items[tail] = value;
    19. tail++;
    20. //求余数 是一种解决循环数组的方法
    21. //tail = tail % items.length;
    22. //也可以进行一个判断 来解决
    23. //相比求余数,判断语句的解决方式更容易看懂。 并且这种代码的效率可能更高。
    24. if (tail >= items.length) {
    25. tail = 0;
    26. }
    27. size++;
    28. // 当前队列不为空了,唤醒 正在阻塞等待 的出队列程序(消费者线程)
    29. this.notify();
    30. }
    31. }
    32. // 出队列
    33. public Integer take() throws InterruptedException {
    34. int result = 0;
    35. synchronized(this) {
    36. //这里也是循环等待,做同样的处理
    37. while (size == 0) {
    38. //队列为空, 无法取出数值
    39. // 自动阻塞,等待入队列程序(生产者线程)的唤醒
    40. this.wait();
    41. }
    42. result = items[head];
    43. head++;
    44. if (head >= items.length) {
    45. head = 0;
    46. }
    47. size--;
    48. // 此时队列中 不是满着的, 唤醒 入队列程序(生产者线程)
    49. this.notify();
    50. }
    51. return result;
    52. }
    53. //1, 先要保证线程安全
    54. }
    55. public class ThreadDemo9 {
    56. public static void main(String[] args) {
    57. MyBlockingQueue queue = new MyBlockingQueue();
    58. //消费者线程
    59. Thread customer = new Thread(() -> {
    60. while(true) {
    61. Integer result = null;
    62. try {
    63. result = queue.take();
    64. } catch (InterruptedException e) {
    65. e.printStackTrace();
    66. }
    67. System.out.println("消费元素:" + result);
    68. }
    69. });
    70. //生产者线程
    71. Thread producter = new Thread(() -> {
    72. int count = 0;
    73. while(true) {
    74. try {
    75. queue.put(count);
    76. } catch (InterruptedException e) {
    77. e.printStackTrace();
    78. }
    79. System.out.println("生产元素: " + count);
    80. count++;
    81. try {
    82. Thread.sleep(500);
    83. } catch (InterruptedException e) {
    84. e.printStackTrace();
    85. }
    86. }
    87. });
    88. customer.start();
    89. producter.start();
    90. }
    91. }


            我是专注学习的章鱼哥~

  • 相关阅读:
    分析脚手架、ref、props
    【算法】冒泡排序
    C#学习笔记(1)
    ffmpeg静态编译 —— 筑梦之路
    Ethereum Classic的难度计算|猿创征文
    分布式消息队列:Rabbitmq(2)
    u盘文件夹被隐藏怎么恢复正常?
    Java设计模式——单例模式(static修饰)
    springboot2自动加载sql文件
    【数据结构】串(二)—— 朴素模式匹配算法
  • 原文地址:https://blog.csdn.net/m0_63852716/article/details/134354677