当前位置: 首页> 文旅> 美景 > 网络营销师待遇怎么样_网店运营实训报告总结_b站推广网站_b站软件推广大全

网络营销师待遇怎么样_网店运营实训报告总结_b站推广网站_b站软件推广大全

时间:2025/8/13 4:01:18来源:https://blog.csdn.net/weixin_74312694/article/details/147572878 浏览次数:0次
网络营销师待遇怎么样_网店运营实训报告总结_b站推广网站_b站软件推广大全

目录

1.单例模式

饿汉模式

懒汉模式

2.阻塞队列

生产者消费者模型的意义:

1,解耦合

2,削峰填谷

代码实现

3.定时器

实现步骤:

4.线程池

构造方法

拒绝策略

线程池数目设置

模拟实现线程池


1.单例模式

单例 => 单个实例(对象)

对于单例这个事情看起来是很简单的,只要保证写代码的时候,只给一个类new一次对象,不去new多次就可以,但是事实上在开发中并不能保证每个人都能完全做好这件事,所以让编译器来帮我门做监督,确保这个对象不会出现多个(如果出现直接报错),这样会更靠谱。

单例模式根据创建对象的时间分为两种实现形式,饿汉模式,懒汉模式

饿汉模式:比较急切,尽早创建实例化对象

懒汉模式:比较从容,在第一次使用的时候,再去创建实例化对象

饿汉模式

代码实现:

class SingleTon{private static SingleTon instance = new SingleTon();public static SingleTon getInstance(){return instance;}private SingleTon(){}
}

要特别注意:

  1. instance 用static修饰,确保instance是属于类的不属于某个方法,确保一个类只会有一个对象。

  2. 构造方法用private修饰,此时类外面的其他代码,就无法new出这个类的对象了

  3. 饿汉模式创建的时间比较早,是在类加载的时候就创建

懒汉模式

代码实现

class SingleTonLazy{//4号位置private static volatile SingleTonLazy instance = null;public static SingleTonLazy getInstance(){if(instance == null){  //3号位置synchronized (SingleTonLazy.class){  //2号位置if (instance == null){   //1号位置instance = new SingleTonLazy();}}}return instance;}private SingleTonLazy(){}
}

使用懒汉模式的时候需要特别关注线程安全。对比饿汉模式,此处添加了很多线程安全的条件。

饿汉模式中,获取实例对象调用的方法只需读取,不用修改。

懒汉模式中,获取实例对象调用的方法既有读取,又有修改,就可能存在问题。

分析上面懒汉中的关键条件: 1号位置:这里是在线程首次调用该方法时进行非空判断创建对象

2号位置:在多个线程都获取实例时,为保证new对象的时候只有一个线程能进行一次new对象操作从而进行上锁保护,其他线程如果也正在获取实例时,会出现锁竞争,进行阻塞等待,等正在创建对象的线程结束。

3号位置:在instance已经被new出来过后,还有很多地方需要获取实例对象,做一个非空判读,避免后续每一次都要进行上锁释放锁,上锁和释放锁也会消cpu耗资源,这样可以提高程序执行效率。

4号位置:主要是防止编译器的错误优化,禁止指令重排序。详细可以参考上一节博客volatile关键字的保证有序性线程入门4-CSDN博客

2.阻塞队列

阻塞队列是多线程代码比较常用的一种数据结构

阻塞队列是种特殊的队列

1.线程安全

2.带有阻塞的特性

a)如果队列为空,继续出队列,就会发生阻塞,阻塞到其他线程往队列里添加元素为止

b)如果队列为满,继续入队列,也会发生阻塞,阻塞到其他线程从队列中取走线程为止

阻塞队列最大的意义,就是可以用来实现”生产者消费者模型

生产者消费者模型的意义:

1,解耦合

两个模块,联系越紧密,耦合度越高,尤其是对于分布式系统来说,是更加有意义的。

在一个简单的分布式系统中

如果A 和 B 直接交互 (A把请求发给B,B把响应返回给A),彼此之间的耦合度是比较高的

a)如果B出现问题,很可能就把A影响到了

b)如果再添加一个C,就需要对A这把代码做出一些改动

相比之下,使用生产者消费者模型,就可以有效解决刚才的耦合问题

此时耦合度就被降低了,如果B出现问题了,就不会对A产生影响 (A只和队列交互,不知道B的存在)

如果后续新增一个C,此时A也不必进行任何修改,只需要让C从队列中获取数据即可

2,削峰填谷

在第一个结构中,一旦客户端这把发送的请求多了,每个A收到的请求,都会立即发给B,A抗多少访问量,B就和A完全一样,不同的服务器跑的业务不一样,虽然访问量一样,但是单个访问消耗的硬件资源是不一样的,可能A能承担这些并发量,但是B就不行然后就之间挂了(比如B操作数据库,数据库本身就是分布式系统中相对脆弱的环节)

当我们引入生产者消费者模型,上述问题也就能得到一些改善

A这边收到了较大的请求量,A会把对应的请求写入到队列中,B任然可以按照之前的节奏,来处理请求。

有了这样的机制之后,就可以保证在突发情况来临的时候,整个服务器任然可以正确执行。

代码实现

基于数组实现的环形阻塞队列

//模拟实现一个阻塞队列  元素用字符串代替
class MyBlockingQueue{private String[] queue = new String[1000];  //默认数组大小为1000
​//为避免内存可见性问题加上volatile关键字private volatile int size = 0;  //队列中当前存在的元素的个数private volatile int head = 0;  //指向队头,表示出队列的位置private volatile int tail = 0;  //指向队尾,表示入队列的位置
​public void put(String num) throws InterruptedException {synchronized(this){  //下面代码涉及到对数据的修改,多线程环境下可能会不安全,需要进行上锁while (size == queue.length){ //队列已经满了,无法插入this.wait();  //阻塞等待take操作结束后唤醒}//队列没满正常插入queue[tail] = num;tail++;if(tail == queue.length){  //tail已经超出最后一个位置了,需要进行修正tail = 0;}size++;  //个数增加this.notify();}
​}public String take() throws InterruptedException {synchronized (this){while (size == 0){//队列为空无法拿出数据this.wait(); //阻塞等待调用put方法,添加完元素,来唤醒take操作}String num = queue[head];head++;if(head == queue.length){  //超出范围,进行修正head = 0;}size--;this.notify(); //队列已经出了一个元素,有空位了,就可以唤醒put操作进行添加元素了return num;}}
}

put方法阻塞式入队列,take方法阻塞式出队列

注意代码中的等待唤醒方法

一个队列,要么是空,要么是满,take喝put只有一边能阻塞。

如果put阻塞了,其他线程继续调用put也都会阻塞,只有靠take唤醒

如果take阻塞了,其他线程继续调用take还是会阻塞,只有考put唤醒

关键要点:

判断是否需要阻塞等待有两种不同的实现判断方式:

考虑wait唤醒是通过notify 还剩 Interrupt 唤醒

notify唤醒:说明其他线程调用take此时队列已经不满了,可以继续添加元素

Interrupt唤醒:此时队列其实还是满的,继续添肯定会出问题

上面两段代码的区别主要是if 和 while

当while返回的时候,需要进一步确认一下,看当前队列是不是满着的。

而当if被唤醒返回的时候,不管何种唤醒方式,如果是Interrupt唤醒,也都不会再判断是否队列是满的,这样很有可能会出问题。

建议:使用wait等待的时候,往往都是用while作为条件判定的方式,目的就是为了让wait唤醒之后还能再确认一次,是否条件任然满足。

3.定时器

定时器作为一个日常开发常见的组件,他的作用是约定一个时间,时间到达之后,执行某个代码逻辑,定时器非常常见,尤其是在进行网络通信的时候。

实现步骤:

1.需要创建一个类,通过这类的对象来描述一个任务

代码实现:

class MyTimerTask implements Comparable<MyTimerTask> {private Runnable runnable;private long time;
​public MyTimerTask(Runnable r , long delay){this.runnable = r;this.time = System.currentTimeMillis()+delay;}
​public Runnable getRunnable(){return runnable;}
​public long getTime(){return time;}
​@Overridepublic int compareTo(MyTimerTask o) {return (int)(this.time-o.time);  //按照时间进行比较小根堆的方式}
}

2.需要一个数据结构能够将所有的任务都保存起来

这里我们选择使用优先级队列来保存数据,根据每个任务的执行时间建立小根堆。

这样每次堆顶元素一定是需要最早执行的,每次获取最顶元素就能完成按照时间先后顺序来执行任务。

并且注意:在使用优先级队列时需要实现一个比较每个任务时间的方法

我们在任务类中实现了Comparable接口,实现了compareTo方法。

3.Timer中需要有一个线程,循环扫描任务是都到达时间,时间到了就执行任务

代码实现

class MyTimer{private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();
​public void schedule(Runnable runnable,long delay){synchronized (this){queue.offer(new MyTimerTask(runnable,delay));this.notify();}
​}//构造扫描线程public MyTimer(){Thread t = new Thread(()->{while (true){try {synchronized (this){while (queue.isEmpty()) {this.wait();   //队列为空就一直等待}//队列不为空,取出堆顶元素MyTimerTask task = queue.peek();long curTime = System.currentTimeMillis();if (curTime >= task.getTime()) {task.getRunnable().run(); //执行任务queue.poll();  //释放堆顶元素} else {this.wait(task.getTime() - curTime);}}}catch (InterruptedException e){e.printStackTrace();}}});t.start();}
}

对于上面代码需要注意:

1.任务类,可比较问题 (实现指定比较方式)

2.线程安全问题 (加锁,阻塞等待被唤醒后需要的进一步判断再执行下一步)

3.忙等问题 (如果当前时间还没到要执行任务的时间,可以先释放锁进行指定时间的的等待)

4.线程池

线程池诞生的意义,是因为进程的创建/销毁。太重量了(比较慢)

虽然线程和进程相比,线程的创建和销毁比进程的创建销毁快不少,但是如果进一步提高创建销毁线程的频率,线程的开销也不能忽视。

提高线程创建/销毁效率的办法:

1.协程(轻量级线程)

相比于线程,把系统调度的过程给省略了,在当下是一种比较流行的并发编程手段,但是在Java圈子里,协程还不够流行。

Java的标准库中没有协程,但是有一些第三方库实现了协程。Java中实际开发也不怎么会使用这个方式更多的还是线程池。

2.线程池

这个虽然可能没协程好多少,但也不至于很慢。线程池主要是通过提前先把线程创建好,放在池子里,后续使用的时候之间从池子里取出来。

从线程池里取比创建新的线程效率高的原是:

从线程池里取:属于用户态操作

创建新的线程:需要用户态 + 内核态 相互配合完成。

(对于一段程序,如果是在系统内核中执行,此时就称为”内核态“,如果不是,则称为”用户态“)

构造方法

Java标准库中的线程池创建构造方法分析

corePoolSizemaximumPoolSize 分别别表示 ” 核心线程数 “ 和 ” 最大线程数 “,这个线程池里的线程数目是可以动态变化的。变化范围为[corePoolSize, maximumPoolSize]

keepAliveunit 分别表示 ” 多余 空闲线程结束前最多存货的时间 “ 和 ”单位表示(ms,s,min...)“

workQueue 表示 阻塞队列,用来存放线程池中的任务

threadFactory 表示 工厂模式的体现

handler 表示 线程池的拒绝策略,当往线程池里添加的任务达到上限了,继续添可能会出现什么效果。

拒绝策略

下面是不同的拒绝策略表示不同的含义:

1.直接抛出异常

2.新添加的任务,由添加任务的线程负责执行

3.丢弃任务队列中最老的任务

4.丢弃当前新加的任务

线程池数目设置

对于一个cpu的逻辑核心数是N,此时线程池需要设置线程的数目是多少合适?

答案是不确定的,没有准确的一个数,只要说出一个具体的数都是错的!

一个线程,执行的代码主要有两类: 1.cpu密集型:代码里主要的逻辑都是在进行 算术运算 / 逻辑判断

2.IO密集型:代码里主要进行的操作都是IO操作

假设一个线程的所有代码都是cpu密集型,这个时候,线程池的数量不应该超过N (设置N就是极限了),设置比N更大,这个时候,也没法提高效率了,cpu已经吃满了,此时更多的线程反而会增加调度的开销。

假设一个线程所有的代码都是IO密集型,这个时候不吃cpu,此时设置的核心数,就是可以超过N,一个核心可以通过调度的方式来并发执行。

结论:代码不同,线程池的线程数目设置就不同,无法知道一个代码,具体多少内容是cpu密集,多少是IO密集,因此只能通过实验的方式,对程序进行测试,找到最符合需求的线程数目。

模拟实现线程池
class MyThreadPool{private BlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(1000);
​public void submit(Runnable runnable) throws InterruptedException{queue.put(runnable);  //此处策略是拒绝等待策略}
​public MyThreadPool(int n){//创建n个线程,负责执行上述队列中的任务for(int i = 0; i < n; i++){Thread t = new Thread(()->{//让这个线程去队列中获取任务,并进行执行try {Runnable runnable = queue.take();runnable.run();} catch (InterruptedException e) {throw new RuntimeException(e);}});t.start();}}
}

测试方法:

public static void main(String[] args) throws InterruptedException {MyThreadPool myThreadPool = new MyThreadPool(4);for(int i =0; i < 1000; i++){int id = i;myThreadPool.submit(()->{System.out.println("正在执行"+id);});}
​
}

注意循环中我们定义了一个id,因为此处涉及到变量捕获,在匿名内部类中,变量捕获只能捕获final 或者 事实final 的变量,如果直接传i此时i一直在被修改,并不是事实final的变量。通过创建id,此处id并没有被修改,就被看作是一个事实final的变量,这样程序就不会报错。

关键字:网络营销师待遇怎么样_网店运营实训报告总结_b站推广网站_b站软件推广大全

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

责任编辑: