文章目录
单例模式与阻塞队列
一. 单例模式
1.什么是单例模式
我们在java中说的单例是指一个程序中某个类,只能创建出一个实例,不能创建多个实例。所以单例模式可以总结为:保证某个类的程序中只存在唯一一份实例,而不会创建出多个实例。
这里可能有人会说了,多new几次,不就创建多个对象了吗?
java语法有办法禁止多次new,在java的单例模式中,借助java语法,可以保证一个类只能new一个实例,而不能多次new。
2.单例模式的具体实现方式
单例模式可以分为多种具体的实现方式,主要的模式有两种,分为“饿汉模式”和“懒汉模式”。
2.1饿汉模式
饿汉模式:在类加载同时创建实例。
从名字中的饿汉就可以看出这种模式是非常急迫的,和类的加载同时进行。
以大学生写作业为例:某位大学生现在有4科作业,以饿汉模式来说,就相当于这位大学生在某一时间段一次性写完着4科作业。
以生活中洗碗为例:以饿汉模式来说就是吃完饭后把所有用的碗都刷干净。
总的来说,饿汉模式就是急迫的把所有事情的一次性干完。
代码实例:
class Singleton{
private static Singleton instance = new Singleton();
public static Singleton getInstance(){
return instance;
}
private Singleton(){
}
}
public class ThreadDemo19 {
public static void main(String[] args) {
Singleton s = Singleton.getInstance();
Singleton s2 = Singleton.getInstance();
System.out.println(s == s2);
}
}
结合以上代码来看单例模式:
private static Singleton instance = new Singleton();
此处instance对象被static修饰,java中被static修饰的对象就是类对象,而在JVM中每个类的类对象只能有一个,所以instance对象就是唯一的实例。
private Singleton(){};
这里在Singleton类中写出私有的构造方法,保证在类外不能使用,就避免了多次new。
总的来说以上代码在类内部把实例创建好了,并且禁止了外部重新创建实例,这样就保证了单例的特性,同时上述代码在类加载的同时实例便被创建出来了,模式为饿汉模式。
2.2懒汉模式
与饿汉模式对应,懒汉模式为:类加载的时候不创建实例,在第一次使用的时候才创建实例。
以大学生写作业为例:某位大学生现在有4科作业,以懒汉模式来说,就相当于这位大学生在需要提交一科作业的时候再写这一科作业。
以生活中洗碗为例:以懒汉模式来说就是吃完饭后不刷碗,在下次吃饭时需要用到几个碗就刷几个碗。
总的来说懒汉模式就是需要做什么才做什么。
代码实例:
class SingletonLazy{
private static SingletonLazy instance = null;
public static SingletonLazy getInstance(){
if (instance == null) {
instance = new SingletonLazy();
}
return instance;
}
private SingletonLazy(){}
}
public class ThreadDemo20 {
public static void main(String[] args) {
SingletonLazy s = SingletonLazy.getInstance();
SingletonLazy s2 = SingletonLazy.getInstance();
}
以上代码保证单例特性的操作和饿汉模式一样就在赘述了。
private static SingletonLazy instance = null;
这里结果为null便保证了在类加载的时候实例不会被创建出来,实现懒汉模式的一个条件。
public static SingletonLazy getInstance(){
if (instance == null) {
if(instance == null){
instance = new SingletonLazy();
}
}
return instance;
}
在外部调用这个方法的时候如果没有实例便创建一个出来,保证了懒汉模式的另一个条件。
3. 单例模式的线程安全问题
3.1产生线程安全的原因
在具体实现方式中的两个代码实例线程是否安全?多个线程下调用getInstance()是否会出现问题?我们分开来看。
饿汉模式:
public static Singleton getInstance(){
return instance;
}
在饿汉模式中,该方法仅仅只进行了读操作,并没有修改,所以饿汉模式认为线程是安全的。
懒汉模式:
public static SingletonLazy getInstance(){
if (instance == null) {
instance = new SingletonLazy();
}
return instance;
}
懒汉模式下,只要多次调用getInstance()方法便实现了多次new的操作,着已经违反了单例模式的特性,所以多线程下单例模式可能无法保证创建对象的唯一性。
需要用到单例模式的对象如果多次创建可能会出现大问题,例如一个单例的对象需要吃200G的内存空间,在内存空间有限的情况下多次new可能会超出服务器内存空间大小导致崩溃。
3.2解决线程安全问题
两个字:加锁!
抛出问题:锁要加在哪里?
由于多线程代码很复杂,所有并不是说加了锁,就一定线程安全了,要具体情况具体分析,以上面代码为例:
错误的实例:
class SingletonLazy{
private static SingletonLazy instance = null;
public static SingletonLazy getInstance(){
if(instance == null){
synchronized(SingletonLazy.class){
instance = new SingletonLazy();
}
}
}
return instance;
}
private SingletonLazy(){}
}
以上错误实例,原因是因为判断和new不是一个原子操作。
修改后依然存在问题的实例:
class SingletonLazy{
private static SingletonLazy instance = null;
public static SingletonLazy getInstance(){
synchronized (SingletonLazy.class) {
if(instance == null){
instance = new SingletonLazy();
}
}
return instance;
}
private SingletonLazy(){}
}
这里的线程不安全只出在首次创建对象这里,一旦对象创建好了,后续调用getInstance()方法就是单纯的读操作了,就没有线程安全问题便不需要加锁,而上述代码实例任何时候调用getInstance()方法都会发生锁竞争。
双重if改善实例:
class SingletonLazy{
private static SingletonLazy instance = null;
public static SingletonLazy getInstance(){
if (instance == null) {
synchronized (SingletonLazy.class) {
if(instance == null){
instance = new SingletonLazy();
}
}
}
return instance;
}
private SingletonLazy(){}
}
上述代码使用了两个if(instance == null){ },仔细看两个if之间隔了一个synchronized,两个if的目的是不一样的。
外层的if是判定当下是否把instance实例创建出来了,当这个实例创建完了之后,其他竞争锁的线程就被里层的if挡住了也就不会继续创建其他实例。
volatile改善后最终实例:
class SingletonLazy{
private volatile static SingletonLazy instance = null;
public static SingletonLazy getInstance(){
if (instance == null) {
synchronized (SingletonLazy.class) {
if(instance == null){
instance = new SingletonLazy();
}
}
}
return instance;
}
private SingletonLazy(){}
}
乍眼一看这个实例和双重if的没有任何区别呀,仔细看对比
双重if实例:
private static SingletonLazy instance = null;
此处实例:
private volatile static SingletonLazy instance = null;
仅仅只多了一个volatile而已,但是不要小看这一个volatile,它的作用很大。
volatile避免了内存可见性,禁止了指令的重排序。
二.阻塞队列
1.什么是阻塞队列
我们先回顾一下什么是队列。
队列:一种先进先出的线性表。
那么阻塞队列是什么呢?
阻塞队列是一种特殊的队列,也遵循先进先出原则。
阻塞队列是一种线程安全的数据结构,并且有2个特性:
- 当队列为空的时候,尝试出队列就会阻塞等待,等待到队列不为空为止。
- 当队列是满的时候,尝试入队列也会阻塞等待,等待到队列不满为止。
简易的实例:
package thread;
import java.util.concurrent.BlockingDeque;
import java.util.concurrent.LinkedBlockingDeque;
public class ThreadDemo21 {
public static void main(String[] args) throws InterruptedException {
BlockingDeque<String> blockingDeque = new LinkedBlockingDeque<>();
blockingDeque.put("hello");
String res = blockingDeque.take();
System.out.println(res);
res = blockingDeque.take();
System.out.println(res);
}
}
下面我们解析一下以上代码:
BlockingDeque<String> blockingDeque = new LinkedBlockingDeque<>();
此处new出一个阻塞队列,阻塞队列类型为BlockingDeque
blockingDeque.put("hello");
进行入队列操作
String res = blockingDeque.take();
出队列操作,如果没有put直接take就会阻塞,以上代码中我们只put了一次,但是take了两次,所以在第二次take的时候代码就会进入阻塞状态运行结果如下图。
可以看到程序一直在运行状态。
2.生产者消费者模型
2.1什么是生产者消费者模型
生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。
生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取。
2.2阻塞队列在其中的作用
- 阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力,也可以叫做“削峰填谷”。
比如铁路12306国庆抢票时,服务器同一时刻可能会收到大量的支付请求,如果直接处理这些支付请求,服务器可能扛不住,这时候就可以把这些请求给放到阻塞队列里面,然后慢慢处理每一个支付请求,这就是削峰填谷,将高峰期阻塞,在低峰期处理。
- 阻塞队列能使生产者和消费者之间解耦。
比如五个服务器另外五个服务器之间是一对一的关系,如果此时两者挂一个,便不能完成通信流程,这样耦合性就比较高。但是加上阻塞队列就相当于,服务器之间互相都不认识,都只知道阻塞队列,五个服务器将请求放进阻塞队列,另外五个服务器从阻塞队列里面取出请求,这样任何一个服务器挂了都不会影响其他服务器。
2.3代码实例
class MyBlockingWueue{
private int[] items = new int[1000];
private int head =0;
private int tail = 0;
private int size = 0;
public void put(int value) throws InterruptedException {
synchronized (this) {
while (size == items.length){
this.wait();
}
items[tail] = value;
tail++;
if(tail >= items.length){
tail = 0;
}
size++;
this.notify();
}
}
public Integer take() throws InterruptedException {
int result;
synchronized (this) {
if (size == 0) {
this.wait();
}
result = items[head];
head++;
if (head >= items.length) {
head = 0;
}
size--;
this.notify();
}
return result;
}
}
public class ThreadDemo23 {
public static void main(String[] args) throws InterruptedException {
MyBlockingWueue queue = new MyBlockingWueue();
Thread customer = new Thread(()->{
while(true){
try {
int result = queue.take();
System.out.println("消费"+result);
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
customer.start();
Thread producer = new Thread(()->{
int count = 0;
while(true){
try {
System.out.println("生产"+count);
queue.put(count);
count++;
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
producer.start();
}
}
接下来我们解析一下以上代码:
class MyBlockingWueue{
private int[] items = new int[1000];
private int head =0;
private int tail = 0;
private int size = 0;
public void put(int value) throws InterruptedException {
synchronized (this) {
while (size == items.length){
this.wait();
}
items[tail] = value;
tail++;
if(tail >= items.length){
tail = 0;
}
size++;
this.notify();
}
}
public Integer take() throws InterruptedException {
int result;
synchronized (this) {
if (size == 0) {
this.wait();
}
result = items[head];
head++;
if (head >= items.length) {
head = 0;
}
size--;
this.notify();
}
return result;
}
}
这个实体类我们来模拟阻塞队列。
public void put(int value) throws InterruptedException {
synchronized (this) {
while (size == items.length){
this.wait();
}
items[tail] = value;
tail++;
if(tail >= items.length){
tail = 0;
}
size++;
this.notify();
}
}
这个方法实现阻塞队列入队操作,当size达到队列最大长度时,证明阻塞队列满了,使用wait()方法让入队线程进入等待状态,一直等到notify()方法,才证明阻塞队列有空位了继续入队。
public Integer take() throws InterruptedException {
int result;
synchronized (this) {
if (size == 0) {
this.wait();
}
result = items[head];
head++;
if (head >= items.length) {
head = 0;
}
size--;
this.notify();
}
return result;
}
这个方法实现阻塞队列出队操作,当size为0时,证明阻塞队列为空,这时调用wait()使出队线程进入等待状态,一直等到相应的nofity()证明阻塞队列不为空才继续出队。
Thread producer = new Thread(()->{
int count = 0;
while(true){
try {
System.out.println("生产"+count);
queue.put(count);
count++;
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
这里的线程producer模拟入队操作
Thread customer = new Thread(()->{
while(true){
try {
int result = queue.take();
System.out.println("消费"+result);
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
这里的customer操作模拟出队操作。
以上代码结合便构造出了一个完整的生产者消费者模型。
由运行结果可以看出先进行了1001次入队,这时阻塞队列满了入队线程进入wait()等待状态,这时出队线程进行一次后队列不在满并且同时调用了nofity()解除入队线程的等待状态,然后入队线程才进行了第1002次入队。