多线程之旅:单例模式以及生产者消费者模型
今天小编来分享下什么是单例模式以及生产者消费者模型。
首先请出今天的主角:单例模式
单例模式
那什么又是单例模式呢?
单例模式是属于设计模式中的一种
那什么又是设计模式呢?
设计模式
是软件工程中一组最佳实践。
旨在解决软件设计中的重复出现的问题,通过标准化解决方案来提高软件的质量和效率。
常见的设计模式如下:
1.创建型模式
单例模式(Singleton)
工厂方法模式(Factory Method)
抽象工厂模式(Abstract Factory)
建造者模式(Builder)
原型模式(Prototype)
2.结构型模式
适配器模式(Adapter)
桥接模式(Bridge)
组合模式(Composite)
装饰模式(Decorator)
外观模式(Facade)
享元模式(Flyweight)
代理模式(Proxy)
3.行为型模式
责任链模式(Chain of Responsibility)
命令模式(Command)
解释器模式(Interpreter)
迭代器模式(Iterator)
中介者模式(Mediator)
备忘录模式(Memento)
观察者模式(Observer)
状态模式(State)
策略模式(Strategy)
模板方法模式(Template Method)
访问者模式(Visitor)
回过来,单例模式是什么呢?
单例模式
是保证一个类中只有一个实例,并提供全局访问点来访问这个类的唯一实例。
那么这个这个单例模式又分为两种方式进行实现
1.饿汉模式
类加载的同时,创建实例
代码如下:
class Singleton{
public static Singleton instance=new Singleton();
public static Singleton getInstance(){
return instance;
}
private Singleton(){
}
}保证其唯一性实例,所以设置了一个private的构造方法
使其在其它类中,无法实例出多个类。
2.懒汉模式
类加载时不创建实例,第一次使用时才创建实例。
代码如下:
class SingletonLazy {
private static Singleton instance = null;
private SingletonLazy() {}
public static Singleton getInstance() {
if (instance == null) {
instance = new SingletonLazy();
}
return instance;
}
}那么此时这个懒汉模式是会存在线程安全问题。
问题出在于if代码块这里
假设有两个线程,那么此时线程A执行到if()这里
此时由于CPU调度,由线程B执行,然后去实例化一个对象赋值给instance,
回到线程A的时候,也会实例化一个对象赋值给instance,此时违反了单例模式的规定
只运行存在一个实例。
所以显然我们就要进行优化下
那么如何进行优化呢?加锁!
线程安全版本:
class SingletonLazy2{
public volatile static Object locker2=new Object();
public static SingletonLazy2 instance=null;
public SingletonLazy2 getInstance(){
if(instance==null){
synchronized (locker2){
//这样导致了性能不是特别好if(instance==null){
//这样写线程不安全
instance=new SingletonLazy2();
}
}
}
return instance;
}
private SingletonLazy2(){
}
}代码中为什么还在外层套个判断语句呢?
这是因为,加锁操作也是会消耗资源,那么此时如若再次调用这个方法,还得加锁一次,所以造成一定性能浪费,不如在外面套个判断语句,进行提高点效率。
ok,这个单例模式就分享到这里。
接下来请出第二位主角——生成者-消费者模型
生成者-消费者模型
那什么又是生产者消费者模型呢?
其实它是一种经典的并发设计模式。
目的:这是主要去解决生产者和消费者之间的同步以及资源共享问题。
比如

此时服务器A向服务器B传输数据。
那么某一时刻,传给服务器A的流量突然增大,此时服务器A的流量也要流入到服务器B,
那么服务器B的硬件处理能力达到上限后,此时服务器B就会崩掉,对业务处理造成一定的影响。
所以为了解决这个问题,那么我们此时可以在两个服务器中间,设置一个容器,起到一个过度、缓冲作用。

此时,原本没有容器的时候,服务器A与服务器B的耦合度较高,加入容器后,耦合度就会下降。
在java中,起到这样一个作用的,有这样的一个接口:BolckingQueue
名字叫做阻塞队列。
阻塞队列
顾名思义,是起到一个阻塞作用,它的put方法和take()方法是会起到一个阻塞作用
当调用put方法时,队列为满,此时需要等到队列出一个元素
同理,当调用take方法时,队列为空,此时需要队列添加一个元素。
ok,那么接下来简单演示下是生产者和消费者模型是怎么样的
我们先创建两个线程,一个线程A作为生产者,一个线程作为消费者
代码如下:
public class Demo25 {
public static void main(String[] args) {
//生产者消费者模型
MyBlockingQueue deque=new MyBlockingQueue(1000);
Thread t1=new Thread(()->{
int i=1;
while (true){
try {
deque.put(""+i);
System.out.println("生产元素:"+i);
i++;
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
Thread t2=new Thread(()->{
while (true){
try {
Integer i=Integer.parseInt(deque.take());
System.out.println("消费元素:"+i);
// Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t1.start();
t2.start();
}
}ok,至于为什么会在t1生产者线程中加入休眠1秒钟,是为了更好的演示打印结果
因为,不加休眠,代码会快速打印生产元素
运行结果如下:

ok,这里值的注意的是,这个阻塞队列也同样存在offer、poll、peek方法
但这些个方法不带有阻塞特性。
那么接下来小编带着大家来如何简单实现一个阻塞队列。
简单模拟实现阻塞队列
阻塞队列
首先,我们的阻塞队列使用的是数组作为底层元素,队列具有循环属性
并且具有一个头指针、和尾指针,进行入队列,出队列操作。
初始化:
代码
class MyBlockingQueue{
//用数组来存储元素public String [] arr;
//代表的是头指针public int head=0;
//代表的是尾指针public int tail=0;
//代表的是,插入数据个数int size=0;
public static Object locker=new Object();
public MyBlockingQueue(int capacity){
arr=new String[capacity];
}
}ok,接下来讲讲这个put操作
put
这个put操作的思路较为简单
即添加一个元素,就tail++
然后检查下,tail是否是超出数组长度,超出了,那就回到起点
然后size也同时+1操作
代码如下:
public void put(String x) {
if(size==arr.length){
//暂不做处理
}
arr[tail]=x;
tail++;
if(tail>=arr.length){
tail=0;
}
}接下来讲讲这个take操作。
take
那么思路也是较为简单
从队头取出元素,
然后head++
检查一下,head操作是否超出数组长度,那么此时就要把head放回到初始位置
代码如下:
public String take(){
if(size==0){
暂不做处理
}
ret=arr[head];
head++;
if(head>=arr.length){
head=0;
}
size--;
return ret;
}那么此时值得注意的是,在put、take方法中分别存在着tail++、head++
此时在多线程环境中,会出现线程安全问题了,为了解决这个问题,所以分别在这两个方法进行加锁操作
put
多线程安全版本:
public void put(String x) throws InterruptedException {
//设计到++操作,线程不安全synchronized (this){
while (size==arr.length){
//满了,等待释放元素this.wait();
}
arr[tail]=x;
tail++;
if(tail>=arr.length){
tail=0;
}
size++;
//添加完元素要,通知下阻塞中的take方法this.notify();
}
}take
多线程安全版本:
public String take() throws InterruptedException {
String ret="";
synchronized (this){
while (size==0){
this.wait();
}
ret=arr[head];
head++;
if(head>=arr.length){
head=0;
}
size--;
//取出完元素,要通知下阻塞中put方法this.notify();
}
return ret;
}那么值得注意的是,两个方法中的this.wait()
它的进入条件均使用了while,这是因为
如若是使用了if语句进行判断,当其他线程调用interrupt方法时,会唤醒其等待,那么此时
如若是本来不需要take了,或者put操作时,那么会造成一定程序bug,所以使用while循环
即使是被唤醒了,也会检查下是否是符合条件。
ok,小编先分享到这里。
完!