多线程篇六
如笔者理解有误欢迎交流指正~⭐
什么是单例模式?
单例模式是最常见的 设计模式.
顾名思义,单例模式指的就是单个实例的模式.(针对某些类只能使用一个对象的场景【如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);
}
}
未完待续🌟(●’◡’●)