代码案例
1.单例模式:这是一种设计模式。单例模式对应的场景 有些时候希望有的对象,在整个程序中只有一个实例,只能new一次
a)饿汉模式
这个代码的执行时机 是在Singleton类被jvm加载的时候
Singleton会在JVM第一次使用的时候被加载
b)懒汉模式
c)饿汉模式代码中 线程是安全的
d) 懒汉模式代码中线程是不安全的,
主要是在首次new对象的时候,才出现问题,一旦把对象new好之后,后续再调用getinstance 就没事了(假设有两个线程 假如两个线程都判断if里面的条件 而其中一个线程的判断会在另一个线程的判定之前,这时候instance还是null!但这时就会new出两个对象 就不是单例了)
d)如何解决
1.加锁
将代码加一个synchronize
但是加锁是一个成本比较高的操作,加锁会引起程序阻塞等待
加锁的基本原理:应该是 非必要,不加锁,不能无脑加锁
2.我们加入一个双重if
3.于是我们再进行优化 给instance加一个volatile
(这里可能存在内存可见性的风险,触不触发是未知的 加上volatile是一个更稳妥的做法)而加上volatile还有另一个用途:避免此处赋值操作的指令从排序。
而我们的最终版本就是如此
2.阻塞队列
a) 队列分为很多种:普通队列,优先级队列,阻塞队列(带有阻塞功能,常用于后端开发)
b)这个案例来详细讲讲阻塞队列:阻塞队列有两种状态:
1.当队列满的时候,继续入队列,就会出现阻塞,阻塞到其他线程从队列中取走元素为止
2.当队列空的时候,继续出队列,也会出现阻塞状态,阻塞到其他线程往队列中添加元素即可
c)首先我们来了解两个概念
1.解耦合:就是降低模块之间的耦合 耦合:就是两个系统关联很紧密 一个系统出现问题,另一个系统会受到较大的影响 如果在他们中间引入一个阻塞队列就能很好的解决上述的问题 ,而这也有缺陷:会使效率降低。
2.削峰填谷:一个机器的硬件资源是有限的,服务器每次处理一个请求,都要消耗一定的硬件资源 而一个分布式系统中,经常会出现,有的机器承受压力更大,有的机器承受压力更小,当两个系统如图所示
此时,如果A接收到一个请求,B也就需要立即处理这个请求,如果A能承受足够的压力 B能够承受的压力小的话,此时B可能就会崩溃
而假如我们添加一个阻塞队列,如果所示
当外界的请求暴涨时,A的请求多了 A就会往请求中写入更多的数据,但是B仍然可以按照既定的节奏来处理,B就不至于崩溃 这时候队列就起到了一个缓冲的作用、(削峰)
而峰值很多时候只是暂时的,当峰值消退的时候,A收到的请求少了,B还是按照既定的节奏来处理,B也不至于太空闲 (填谷)
d)java有三种阻塞队列
第一行是基于链表的,第二行是基于堆实现的,第三行是基于数组的
Array这个版本速度更快,但前提是要知道有多少个元素,如果不知道有多少元素,使用Linked更合适
a)对于BlockingQueue来说 offer poll 不带有阻塞功能,put和take带有阻塞功能
b)基于阻塞队列写一个生产者消费者模型
package Thread;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
public class no4 {
public static void main(String[] args) {
//负责生产元素
BlockingQueue<Integer> queue=new LinkedBlockingQueue<>();
Thread t1=new Thread(()->{
int count =0;
while (true){
try {
queue.put(count);
System.out.println("生产元素"+count);
count++;
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
//负责消费元素
Thread t2=new Thread(()->{
while (true){
try {
Integer n =queue.take();
System.out.println("消费元素"+n);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t1.start();
t2.start();
}
}
d)这是java提供现有的阻塞队列 我们自己来编写代码实现阻塞队列
1.首先我们先写出入队列的代码
2. 然后我们写出出队列的代码
3.我们现在写出了基础代码 因此我们需要考虑线程安全问题
a)先给put和take进行加锁保证线程安全
b)除了线程安全问题 我们还需要考虑内存可见性问题 因此,我们还需加上volatile
4.实现阻塞
a)当队列为满时 进行put会产生阻塞
take的notify唤醒put的wait
b)当队列为空是 ,进行take会产生阻塞
put的notify唤醒take的wait。
c)但是 以上代码仍然有概率数据会覆盖 因此 我们把if改为while进行再一次的判断
因此 我们完整的代码如图所示
package Thread;
class MyBlockingQueue {
private String[] items = new String[1000];
volatile private int head = 0;
volatile private int tail = 0;
//指向队列的尾部的下一次元素,队列的有效元素范围[head,tail)
volatile private int size = 0;
private Object locker=new Object();
//元素个数
//入队列
public void put(String elem) throws InterruptedException {
synchronized (locker) {
while (size >= items.length) {
locker.wait();
//队列满了
//return;
}
items[tail] = elem;
tail++;
if (tail >= items.length) {
tail = 0;
}
size++;
locker.notify();
//用来唤醒队列为空的阻塞情况
}
}
//出队列
public String take() throws InterruptedException {
synchronized (locker){
if (size == 0) {
//return null;
locker.wait();
}
String elem = items[head];
head++;
if (head >= items.length) {
head = 0;
}
size--;
locker.notify();
//使用这个notify来唤醒队列满的阻塞情况
return elem;
}
}
}
public class no20 {
public static void main(String[] args) throws InterruptedException {
MyBlockingQueue queue=new MyBlockingQueue();
// queue.put("aaa");
// queue.put("bbb");
// queue.put("ccc");
// String elem=queue.take();
//创建两个线程代表生产者消费者
Thread t1=new Thread(()->{
int count=0;
while(true){
try {
queue.put(count+" ");
System.out.println("生产元素"+count);
count++;
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread t2=new Thread(()->{
while (true){
try {
String count=queue.take();
System.out.println("消费元素"+count);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t1.start();
t2.start();
}
}