多线程篇六
什么是单例模式?
单例模式是最常见的 设计模式.
顾名思义,单例模式指的就是单个实例的模式.(针对某些类只能使用一个对象的场景【如MySQL、JDBC、DataSource】)
设计模式
设计模式是针对某些问题场景而产生的处理问题的方法.(就跟你想吃早饭,可以选择自己做或者出去买或者蹭别人或者别的解决方法一样)
tips
单例模式是线程安全的,能保证某个类在程序中只存在唯一一份实例而不会创建出多个实例.
单例模式又分为饿汗和懒汉两种.
饿汉模式
创建的比较早,类加载时就创建出了.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| class Singleton { private static Singleton instance = new Singleton(); private Singleton(){} public static Singleton getInstance() { return instance; } } public class TestSingleton { public static void main(String[] args) { Singleton.getInstance(); Singleton s = new Singleton(); } }
|
注意
1.将instance 设为静态成员,在Singleton类被加载的时候进行实例创建(类加载创建)
2.通过此方法获取new出来的实例,其他代码块后续想一直使用这个类(获取这个类唯一的实例),使用getInstance方法即可.
3.private Singleton() {} 是在设置私有构造方法,保证其它代码不能创建出新的对象.

懒汉模式
创建的比较迟,首次使用的时候才创建.
单线程版
1 2 3 4 5 6 7 8 9 10 11
| class SingletonLazy { private static SingletonLazy instance = null; private static SingletonLazy getInstance() { if(instance == null) { instance = new SingletonLazy(); } return instance; } private SingletonLazy() { } }
|
注意
1.首次使用instance的时候才真正创建实例.(不调用就不创建)
2.第一次调用getInstance时,instance引用为null,进入if创建出的实例可以持续调用的实例.
对比
1.懒汉模式比饿汉模式效率更高.
2.饿汉模式更具线程安全,饿汉模式getInstance只进行读取,懒汉模式对数据既会读取数据又会修改数据.
线程安全问题发生在首次创建实例时,如果多个线程同时调用getInstance方法对变量进行修改就可能导致线程安全问题.
怎么解决呢?synchronized!
多线程版
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| class Singleton { private static Object locker = new Object(); private static Singleton instance = null; private Singleton() {} public static Singleton getInstance() { synchronized(locker) { if(instance == null) { instance = new Singleton(); } } return instance; } }
|
问题来了😀.这么写后续每次调用getInstance都需要先加锁,但实际上懒汉模式线程安全问题只出现在new对象时,**一但对象new出来后续多线程调用getInstance只有读操作了,就不存在线程安全问题了.**【加锁就可能涉及到锁冲突一冲突就会引起阻塞和高性能无缘】
解决方案
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| class Singleton { private static Object locker = new Object(); private static Singleton instance = null; private Singleton() {} public static Singleton getInstance() { if (instance == null) { synchronized(locker) { if(instance == null) { instance = new Singleton(); } } } return instance; } }
|
在外层再加一层if判断(如果instance为null,即为首次调用->是否需要加锁,非null->后续会调用->不用加锁)
但是又有惊喜来了!指令重排序!
1
| instacnce = new Singleton();
|
这条语句执行有三个指令
1.申请一段内存空间
2.在内存上调用构造方法,创建出实例
3.把内存地址赋值给instance
前面给大家介绍过,这些指令正常情况下按顺序执行,但CPU 可以会自己进行优化打乱顺序.
怎么解决?
volatile关键字!(防止指令重排序)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| class Singleton { private static Object locker = new Object(); private static volatile Singleton instance = null; private Singleton() {} public static Singleton getInstance() { if (instance == null) { synchronized(locker) { if(instance == null) { instance = new Singleton(); } } } return instance; } }
|
阻塞队列
什么是阻塞队列
阻塞队列是一种特殊的队列,遵守”先进先出”原则.【典型的生产者消费者模型】
特性
1.队列满的时候继续入队会阻塞,直到有其他线程从队列中取走元素.
2.队列空时继续出队也会阻塞,直到有其他线程王队列中插入元素.
生产者消费者模型
分布式系统在实际开发中经常涉及,核心是分开工作发挥效果.服务器整个功能的实现是由每个服务器单独负责一部分工作实现的,通过各个服务器之间的网络通信完成整个功能.

注意
1.上述的阻塞队列是基于对应数据结构实现的服务器程序,被部署到单独的主机上.整个系统的结构更复杂.
2.引入阻塞队列在A发送请求到B接收是有开销损耗的.
解耦合
阻塞队列能使生产者和消费者解耦合.
高考完的暑假想赚点小钱,你和你的朋友开始摆摊卖冰汤圆,每个人都有明确的分工.(是的我是大馋丫头)小A负责采购材料,小B负责制作,小C负责配送,你负责宣传和看城管.顾客是“消费者”,不需要关注你们作为“生产者”谁做了冰汤圆.有吃就行.
削峰填谷
阻塞队列相当于一个缓冲区,平衡了生产者和消费者之间的处理能力.
618大抢购,一分钟之内可能会产生数百万订单,服务器在同一时刻收到大量的支付请求,直接处理服务器受不了会崩溃,(一个请求耗费的资源少但积累量变产生质变,任何一种硬件资源达到瓶颈服务器都会寄)
这时候就是阻塞队列大显身手的时候,将请求都放到一个阻塞队列中,然后再由消费者线程慢慢来处理每个支付请求.
代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| public class TestCustomerAndProducer { public static void main(String[] args) { BlockingQueue<Integer> blockingQueue = new LinkedBlockingQueue<Integer>(); Thread customer = new Thread(() -> { while(true) { try { int value = blockingQueue.take(); System.out.println("Consumption element: " + value); Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } } }, "customer"); Thread producer = new Thread(() -> { Random r = new Random(); while(true) { try { int num = r.nextInt(1000); System.out.println(" Production elements: " + num); blockingQueue.put(num); Thread.sleep(300); } catch (InterruptedException e) { e.printStackTrace(); } } }, "procedure"); customer.start(); producer.start(); } }
|
实现阻塞队列
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| import java.util.Random;
public class BlockingQueue { public int[] elems = new int[2000]; private volatile int size = 0; private volatile int head = 0; private volatile int tail = 0;
private Object locker = new Object();
public synchronized int getSize() { return size; } public void put(int value) throws InterruptedException{ synchronized(locker){ while(size >= elems.length) { locker.wait(); } elems[tail] = value; tail = (tail + 1) % elems.length; size++; locker.notify(); } } public int take() throws InterruptedException { int ret = 0; synchronized(locker) { while(size <= 0) { locker.wait(); } ret = elems[head]; head = (head + 1) % elems.length; size--; locker.notify(); } return ret; } }
|
注意
1.使用循环队列实现(注意理解头指针和尾指针的变化)
2.put和take使用的是同一把锁,若队列被put满之后又唤醒了另一个阻塞的put就会出bug,加while判断,如果队列一直是慢的就不再被唤醒,保证安全性.
标准库中的阻塞队列
在 Java 标准库中内置了阻塞队列. 如果我们需要在一些程序中使用阻塞队列, 直接使用标准库中的即可.
1.BlockingQueue 是一个接口. 真正实现的类是 LinkedBlockingQueue.
2.put 方法用于阻塞式的入队列, take 用于阻塞式的出队列.
3.BlockingQueue 也有 offer, poll, peek 等方法, 但是这些方法不带有阻塞特性.
1 2 3 4 5 6 7 8 9 10
| public class BlockingQueue { public static void main(String[] args) throws InterruptedException { BlockingQueue<String> queue = new LinkedBlockingQueue<>(); queue.put("abc"); String elem = queue.take(); System.out.println(elem); } }
|