多线程篇六
如笔者理解有误欢迎交流指正~⭐
什么是单例模式?
单例模式是最常见的 设计模式.
顾名思义,单例模式指的就是单个实例的模式.(针对某些类只能使用一个对象的场景【如MySQL、JDBC、DataSource】)
设计模式
设计模式是针对某些问题场景而产生的处理问题的方法.(就跟你想吃早饭,可以选择自己做或者出去买或者蹭别人或者别的解决方法一样)
tips
单例模式是线程安全的,能保证某个类在程序中只存在唯一一份实例而不会创建出多个实例.
单例模式又分为饿汗和懒汉两种.
饿汉模式
创建的比较早,类加载时就创建出了.
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() {} 是在设置私有构造方法,保证其它代码不能创建出新的对象.
懒汉模式
创建的比较迟,首次使用的时候才创建.
单线程版
class SingletonLazy {//先将引用初始化为null 不立即创建实例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!
多线程版
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只有读操作了,就不存在线程安全问题了.【加锁就可能涉及到锁冲突一冲突就会引起阻塞和高性能无缘】
解决方案
class Singleton {private static Object locker = new Object();private static Singleton instance = null;private Singleton() {}public static Singleton getInstance() {if (instance == null) {//第一个if判定的是是否加锁(保证执行效率)synchronized(locker) {if(instance == null) {//第二个if判定的是是否要创建对象(保障线程安全)instance = new Singleton();}}}return instance;}
}
在外层再加一层if判断(如果instance为null,即为首次调用->是否需要加锁,非null->后续会调用->不用加锁)
但是又有惊喜来了!指令重排序!
instacnce = new Singleton();
这条语句执行有三个指令
1.申请一段内存空间
2.在内存上调用构造方法,创建出实例
3.把内存地址赋值给instance
前面给大家介绍过,这些指令正常情况下按顺序执行,但CPU 可以会自己进行优化打乱顺序.
怎么解决?
volatile关键字!(防止指令重排序)
class Singleton {private static Object locker = new Object();private static volatile Singleton instance = null;private Singleton() {}public static Singleton getInstance() {if (instance == null) {//第一个if判定的是是否加锁(保证执行效率)synchronized(locker) {if(instance == null) {//第二个if判定的是是否要创建对象(保障线程安全)instance = new Singleton();}}}return instance;}
}
阻塞队列
什么是阻塞队列
阻塞队列是一种特殊的队列,遵守”先进先出”原则.【典型的生产者消费者模型】
特性
1.队列满的时候继续入队会阻塞,直到有其他线程从队列中取走元素.
2.队列空时继续出队也会阻塞,直到有其他线程王队列中插入元素.
生产者消费者模型
分布式系统在实际开发中经常涉及,核心是分开工作发挥效果.服务器整个功能的实现是由每个服务器单独负责一部分工作实现的,通过各个服务器之间的网络通信完成整个功能.
注意
1.上述的阻塞队列是基于对应数据结构实现的服务器程序,被部署到单独的主机上.整个系统的结构更复杂.
2.引入阻塞队列在A发送请求到B接收是有开销损耗的.
解耦合
阻塞队列能使生产者和消费者解耦合.
高考完的暑假想赚点小钱,你和你的朋友开始摆摊卖冰汤圆,每个人都有明确的分工.(是的我是大馋丫头)小A负责采购材料,小B负责制作,小C负责配送,你负责宣传和看城管.顾客是“消费者”,不需要关注你们作为“生产者”谁做了冰汤圆.有吃就行.
削峰填谷
阻塞队列相当于一个缓冲区,平衡了生产者和消费者之间的处理能力.
618大抢购,一分钟之内可能会产生数百万订单,服务器在同一时刻收到大量的支付请求,直接处理服务器受不了会崩溃,(一个请求耗费的资源少但积累量变产生质变,任何一种硬件资源达到瓶颈服务器都会寄)
这时候就是阻塞队列大显身手的时候,将请求都放到一个阻塞队列中,然后再由消费者线程慢慢来处理每个支付请求.
代码实现
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();}
}
实现阻塞队列
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 等方法, 但是这些方法不带有阻塞特性.
public class BlockingQueue {public static void main(String[] args) throws InterruptedException {BlockingQueue<String> queue = new LinkedBlockingQueue<>();//入队列queue.put("abc");//出队列.如果没有put直接take,会阻塞.String elem = queue.take();System.out.println(elem);}
}
未完待续🌟(●’◡’●)