【JUC系列-08】深入理解CyclicBarrier底层原理和基本使用

JUC系列整体栏目


内容链接地址
【一】深入理解JMM内存模型的底层实现原理https://zhenghuisheng.blog.csdn.net/article/details/132400429
【二】深入理解CAS底层原理和基本使用https://blog.csdn.net/zhenghuishengq/article/details/132478786
【三】熟练掌握Atomic原子系列基本使用https://blog.csdn.net/zhenghuishengq/article/details/132543379
【四】精通Synchronized底层的实现原理https://blog.csdn.net/zhenghuishengq/article/details/132740980
【五】通过源码分析AQS和ReentrantLock的底层原理https://blog.csdn.net/zhenghuishengq/article/details/132857564
【六】深入理解Semaphore底层原理和基本使用https://blog.csdn.net/zhenghuishengq/article/details/132908068
【七】深入理解CountDownLatch底层原理和基本使用https://blog.csdn.net/zhenghuishengq/article/details/133343440
【八】深入理解CyclicBarrier底层原理和基本使用https://blog.csdn.net/zhenghuishengq/article/details/133378623

深入理解CyclicBarrier的底层原理和基本使用

  • 一,深入理解CyclicBarrier的底层原理
    • 1,CyclicBarrier的基本使用
    • 2,CyclicBarrier的底层源码实现
      • 2.1,lock加锁操作
      • 2.2,条件队列入队操作
      • 2.3,同步状态器state设置为0
      • 2.4,条件队列Node结点阻塞
      • 2.5,signalAll满足屏障条件进入下一屏障
      • 2.6,条件队列结点出队
      • 2.7,条件队列结点入队同步队列
      • 2.8,unlock解锁
    • 3,总结

一,深入理解CyclicBarrier的底层原理

在前面两篇讲述了Semaphore和CountDownLatch两个并发工具类,都是通过CLH等待队列实现的,接下来讲解第三个常用的并发工具类:CyclicBarrier ,该类与前二者不同,除了使用CLH同步等待队列 外,还用了条件等待队列来实现的,接下来详细的描述一下该类的基本语法和底层的源码实现。

顾名思义,可以被称为循环屏障,屏障指的是可以让多个线程在满足某一个条件的时候,再全部的同时执行,有点类似于之前的内存屏障,循环指的是这个条件可以一直循环的使用
在这里插入图片描述

1,CyclicBarrier的基本使用

先举一个简单的例子,了解一下这个工具类是如何使用的。在此之前,先定义一个线程池,通过线程池工具类来管理线程

package com.zhs.study.util;import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.concurrent.*;/*** 线程池工具* @author zhenghuisheng* @date : 2023/9/27*/
public class ThreadPoolUtil {//日志级别(由高到低):fatal -> error -> warn -> info -> debug,低级别的会输出高级别的信息,高级别的不会输出低级别的信息private static final Logger log = LoggerFactory.getLogger(ThreadPoolUtil.class);//构建线程池public static ThreadPoolExecutor pool = null;//向线程池中添提交任务,将任务返回//判断核心线程数数量,阻塞队列,创建非核心线程数,拒绝策略public static <T> Future<?> submit(Runnable runnable) {//提交任务,并将任务返回Future<?> future = getThreadPool().submit(runnable);//将任务存储在hash表中return future;}/*** io密集型:最大核心线程数为2N,可以给cpu更好的轮换,*           核心线程数不超过2N即可,可以适当留点空间* cpu密集型:最大核心线程数为N或者N+1,N可以充分利用cpu资源,N加1是为了防止缺页造成cpu空闲,*           核心线程数不超过N+1即可* 使用线程池的时机:1,单个任务处理时间比较短 2,需要处理的任务数量很大*/public static synchronized ThreadPoolExecutor getThreadPool() {if (pool == null) {//获取当前机器的cpuint cpuNum = Runtime.getRuntime().availableProcessors();log.info("当前机器的cpu的个数为:" + cpuNum);int maximumPoolSize = cpuNum * 2 ;pool = new ThreadPoolExecutor(maximumPoolSize - 2,maximumPoolSize,5L,   //5sTimeUnit.SECONDS,new LinkedBlockingQueue<>(),  //链表无界队列Executors.defaultThreadFactory(), //默认的线程工厂new ThreadPoolExecutor.AbortPolicy());  //直接抛异常,默认异常}return pool;}
}

接下来再自定义一个线程任务类,内部定义具体的run方法的实现

package com.zhs.study.juc.aqs;
import java.util.concurrent.CyclicBarrier;/*** @author zhenghuisheng* @date : 2023/9/27*/
public class Task implements Runnable {CyclicBarrier cyclicBarrier;//通过构造方法传参,保证拿到的是同一个对象public Task(CyclicBarrier cyclicBarrier){this.cyclicBarrier = cyclicBarrier;}@Overridepublic void run() {try {System.out.println(Thread.currentThread().getName()+ "开始等待其他线程");cyclicBarrier.await();System.out.println(Thread.currentThread().getName() + "开始执行");//TODO 模拟业务处理Thread.sleep(5000);System.out.println(Thread.currentThread().getName() + "执行完毕");} catch (Exception e) {e.printStackTrace();}}
}

接下来定义一个main方法执行这个代码

/*** @author zhenghuisheng* @date : 2023/9/27*/
public class CyclicBarrierDemo {//创建一个线程池static ThreadPoolExecutor threadPool = ThreadPoolUtil.getThreadPool();public static void main(String[] args) {CyclicBarrier cyclicBarrier = new CyclicBarrier(5);//创建20个线程任务for (int i = 0; i < 20; i++) {//创建任务Task task = new Task(cyclicBarrier);//提交任务threadPool.submit(task);}}
}

查看执行结果如下

14:36:50.581 [main] INFO com.zhs.study.util.ThreadPoolUtil - 当前机器的cpu的个数为:4
pool-1-thread-1开始等待其他线程
pool-1-thread-2开始等待其他线程
pool-1-thread-3开始等待其他线程
pool-1-thread-4开始等待其他线程
pool-1-thread-5开始等待其他线程
pool-1-thread-5开始执行
pool-1-thread-1开始执行
pool-1-thread-6开始等待其他线程
pool-1-thread-2开始执行
pool-1-thread-4开始执行

主要就是当线程累加到5个之后,就会通过这个屏障执行以下的业务,如果没有达到5个,就会被阻塞着。与CountDownLatch的底层实现不同,后者是通过减法的方式实现业务,而循环屏障使用的是加法,并且循环屏障的参数是可以循环使用的,而CountDownLatch不能。

2,CyclicBarrier的底层源码实现

接下来研究一下CyclicBarrier 这个类,先查看一下这个类中的部分属性和构造方法。内部引入了ReentrantLock和trip条件队列对象,并且定义了一个重置内存屏障的对象,在构造方法中,除了引用一个正常的类加的数据之外,还引入了一个副本参数,用于循环使用

public class CyclicBarrier {//用于重置内存屏障private static class Generation {boolean broken = false;}//引入了ReentrantLock锁,因此有AQS的所有特性以及该锁的特性private final ReentrantLock lock = new ReentrantLock();//条件对象,用于构建条件等待队列private final Condition trip = lock.newCondition();//构造方法如下public CyclicBarrier(int parties, Runnable barrierAction) {if (parties <= 0) throw new IllegalArgumentException();this.parties = parties;		//外部传入参数的副本,用于循环使用this.count = parties;		//外部参数的个数,用于累计this.barrierCommand = barrierAction;	//线程任务,优先级更高}
}

在初步的熟悉了这个类之后,还是得通过这个 await 了解底层到底第如何实现的

cyclicBarrier.await();

接下来进入这个await方法内部,在源码中,一般真正干活的方法,都是以do开头的方法

public int await() throws InterruptedException, BrokenBarrierException {try {return dowait(false, 0L);	//真正干活的方法} catch (TimeoutException toe) {throw new Error(toe); // cannot happen}
}

在这个 dowait 方法中,这个方法内部逻辑是比较多的,如下图,接下来会一段一段的分析这里面的方法

在这里插入图片描述

2.1,lock加锁操作

首先在该方法中定义了一把ReentrantLock锁,并进行了一个加锁的操作。这个操作其实也不难理解,因为在条件等待队列中,需要加锁在能阻塞,就类似于使用wait方法时,必须在外层加synchronized关键字的

final ReentrantLock lock = this.lock;
lock.lock();	//独占锁

2.2,条件队列入队操作

如在CyclicBarrier构造方法中,会有一个count用于做具体的执行操作,因此在这会有一个自减的操作,如果这个count的自减操作的值不为0,那么会继续进入下面这个for循环的自旋操作,首先会有一个trip.await方法用于条件等待队列进行入队操作

int index = --count;			//自减操作
for (;;) {try {//trip是一个条件等待队列对象,调用的这个await是条件等待的入队和阻塞if (!timed) trip.await();	else if (nanos > 0L) nanos = trip.awaitNanos(nanos);} catch (InterruptedException ie) {if (g == generation && ! g.broken) {breakBarrier();throw ie;} else {Thread.currentThread().interrupt();}}if (g.broken) throw new BrokenBarrierException();if (g != generation) return index;if (timed && nanos <= 0L) {breakBarrier();throw new TimeoutException();}
}

接下来查看这个await方法的具体实现,里面首先会有一个addConditionWaiter 结点入队的操作

public final void await() throws InterruptedException {if (Thread.interrupted())throw new InterruptedException();Node node = addConditionWaiter();	//结点入队int savedState = fullyRelease(node);	//结点释放锁 int interruptMode = 0;while (!isOnSyncQueue(node)) {		//判断是不是同步等待队列结点LockSupport.park(this);			//不是则阻塞if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)break;}//被唤醒的结点尝试获取锁if (acquireQueued(node, savedState) && interruptMode != THROW_IE)interruptMode = REINTERRUPT;if (node.nextWaiter != null) // clean up if cancelledunlinkCancelledWaiters();if (interruptMode != 0)reportInterruptAfterWait(interruptMode);
}

这个Node对象是ConditionObject类下面声明的对象,在ConditionObject这个对象中,只对了Node结点的头指针和尾指针,因此组成这个条件等待的队列是一个由Node结点组成的单向链表,CLH同步等待队列中的Node结点和这个Condition条件等待队列的Node结点是同一个类的对象,只是实现两种队列的结构不一样。

public class ConditionObject implements Condition, java.io.Serializable {private static final long serialVersionUID = 1173984872572414699L;//定义头结点private transient Node firstWaiter;//定义尾结点private transient Node lastWaiter;...
}

在这里插入图片描述

结点入队的操作如下,首先会先修改Node结点的状态为-2条件等待状态,其次会判断这个单向链表是否存在,如果存在则直接将结点加入到单向链表的尾部,如果不存在则直接将结点作为头结点。

private Node addConditionWaiter() {Node t = lastWaiter;	//获取条件等待队列的尾结点// If lastWaiter is cancelled, clean out.if (t != null && t.waitStatus != Node.CONDITION) {unlinkCancelledWaiters();t = lastWaiter;			//如果链表存在,则直接将结点插入到尾结点中}//设置结点的waitStatus为-2,即为条件等待状态Node node = new Node(Thread.currentThread(), Node.CONDITION);if (t == null) firstWaiter = node;	//如果链表不存在,则创建链表,将头结点设置为当前结点else t.nextWaiter = node;			//如果链表存在,则直接将链表接入到队尾即可lastWaiter = node;	//将当前结点设置为尾结点return node;
}

2.3,同步状态器state设置为0

依旧是2.2的await方法中,会有一个 fullyRelease 方法,由于在一开始调用了lock方法,这个lock是一把独占锁,其内部也是通过CLH同步等待队列实现,因此也是通过修改state的值来让其他线程可以来抢锁,因此需要通过这个 fullyRelease 方法来实现修改状态的功能。

final int fullyRelease(Node node) {boolean failed = true;try {int savedState = getState();	//此时同步状态器的值为1if (release(savedState)) {	//释放锁failed = false;return savedState;} else {throw new IllegalMonitorStateException();}} finally {if (failed)node.waitStatus = Node.CANCELLED;}
}

主要是通过这个 release(savedState) 方法来进行释放锁,最终会调用tryRelease方法,将同步状态器中的state的值设置为0,并且将exclusive的值设置为null,主要从外面进来的线程(非队列中的阻塞线程)就可以去抢锁。

protected final boolean tryRelease(int releases) {//外部传参为1,因此 1-1=0int c = getState() - releases;if (Thread.currentThread() != getExclusiveOwnerThread())throw new IllegalMonitorStateException();boolean free = false;if (c == 0) {free = true;setExclusiveOwnerThread(null);	//exclusive设置为null}setState(c);	//设置为0return free;
}

2.4,条件队列Node结点阻塞

依旧是2.2的await方法中,会有一个判断当前结点是不是同步等待队列中的结点,很明显不是,因此会进入方法内部,就会有一个park方法阻塞的功能

while (!isOnSyncQueue(node)) {		//判断是不是同步等待队列结点LockSupport.park(this);			//不是则阻塞if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)break;
}

2.5,signalAll满足屏障条件进入下一屏障

如果此时的count值被减为0,那么就会跳过这个循环屏障,即可以执行这个循环屏障,并且会判断构造方法的参数中是否有这个线程任务,如果有则优先执行这个线程任务

int index = --count;			//自减操作
if (index == 0) {  // tripped	//如果此时为0boolean ranAction = false;	//设置一个标志位try {final Runnable command = barrierCommand;	//获取构造方法中的这个参数if (command != null)	//判断是否存在自定义的任务线程command.run();		//该线程优先级更高,可以先执行ranAction = true;		nextGeneration();		//进入下一个循环屏障return 0;} finally {if (!ranAction)breakBarrier();}
}

进入下一个循环屏障的nextGeneration方法的具体实现如下,里面会有一个signalAll

private void nextGeneration() {// signal completion of last generationtrip.signalAll();// set up next generationcount = parties;		//副本重置、复原generation = new Generation();
}

通过下图可以更加直观的分析流程,通过await将Node结点加入到队列,并让结点阻塞,那么可以直接通过这个signalAll方法将结点从同步等待队列中唤醒,但是唤醒之后结点的状态还是-2,因此需要解决park的唤醒,还是得加入到同步等待队列中,通过同步等待队列的唤醒机制,将状态改成-1,才能去抢锁,才能最终的释放锁和唤醒线程。

在这里插入图片描述

因此继续分析这个signalAll方法,其实现如下,具体的唤醒方法在doSignalAll中实现,并且首先唤醒的是头结点

public final void signalAll() {if (!isHeldExclusively())throw new IllegalMonitorStateException();Node first = firstWaiter;	//将头结点获取if (first != null)doSignalAll(first);
}

2.6,条件队列结点出队

里面首先会将条件队列的头结点和尾结点置为null,随后通过first结点执向头结点,随后将头结点的下一个结点也置为空,此时头结点出队。随后通过dowhile的方式,会将所有的结点遍历一遍,此时所有的结点出队

private void doSignalAll(Node first) {lastWaiter = firstWaiter = null;	//将头结点和尾结点全部置为null	do {Node next = first.nextWaiter;first.nextWaiter = null;	//将头结点的下一个结点也置为nulltransferForSignal(first);first = next;	} while (first != null);
}

2.7,条件队列结点入队同步队列

由于只有同步队列中才能去唤醒线程,因此只能将出队的队列加入到同步等待队列中,因此查看这个transferForSignal方法底层的具体实现。此时会先修改结点的状态,改成0,其次会有一个结点的enq入队操作,前面几篇都有写这个具体实现。

final boolean transferForSignal(Node node) {//将结点的-2条件状态改成0默认状态if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))return false;//随后结点入队操作Node p = enq(node);//获取结点的状态int ws = p.waitStatus;	//如果当前结点的前驱结点为-1,则唤醒if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))LockSupport.unpark(node.thread);	return true;
}

入队的操作依旧是那些,双向链表不存在则创建,存在则直接将结点加入,随后修改状态为-1可被唤醒状态,随后结点阻塞,详细可以看前面三篇文章

2.8,unlock解锁

在所有流程走完之后,会在finally里面有一个解锁的方法

lock.unlock();

此时结点已经从条件队列中入队到同步等待队列中,此时条件等待队列的结点都是处于阻塞的,并且状态都为-1,因此需要通过这个unlock方法,去对里面的对象进行唤醒和出队的功能,内部最终会调用这个unpark这个方法

LockSupport.unpark(s.thread);

在被唤醒之后,又会调用这个acquireQueued 进行一个获取锁的功能,这里的抢锁时之际通过cas获取锁的

acquireQueued()

获取锁之后,进入同步队列的结点出队。(同步队列的具体实现看前两篇,写烂了…)

setHead(node);
p.next = null; // help GC
failed = false;

通过unlock方法,可以不断的出队和唤醒下一个线程,这样就能将进入同步队列的条件队列结点给全部唤醒,这样就可以执行参数定义为n个线程了。

3,总结

在整个流程中,可以发现在刚进入是需要加lock获取锁,在await方法中,当结点进入条件队列之后有会释放锁,然后在条件队列结点进入同步队列时又会去抢锁,然后在执行完毕时又会释放锁,总共会有两次加锁和解锁的过程

第一次lock获取锁:配合await使用

第一次await释放锁:将state的值置为0,允许外部线程和同步队列线程结点抢锁

第二次获取锁:条件队列结点进入同步队列时,抢锁成功执行逻辑,失败进入同步队列阻塞

第二次unlock释放锁:同步队列执行完逻辑之后,需要唤醒同步队列中阻塞的结点

循环屏障是通过ReentrantLock和条件队列配合使用的,ReentrantLock中底层通过AQS实现,因此满足了同步队列和条件队列的同时使用。

整个流程可以总结如下:

首先可以在循环屏障中定义一个参数用于表示需要满足的条件,随后线程会调用这个await方法,先通过lock进行一个加锁操作,随后结点会进入条件等待队列,此时结点的状态为-2,在结点阻塞之后,会将同步状态器的state值改成0,锁就进行了释放,此时就会允许外部的线程进行一个抢锁的操作;

当满足这个循环屏障的条件的时候,此时就会进入下一个循环屏障,那么就需要将条件队列的结点进行一个出队的操作,由于唤醒线程只有在同步队列中实现,因此还要将结点加入到同步队列中,入队时又会有一个cas锁的操作,如果抢锁成功,则执行逻辑,如果抢锁失败,则加入到同步队列中并阻塞,当获取锁成功之后,需要结点出队并且唤醒同步队列中被阻塞的结点,因此需要调用最终的unlock方法

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/143520.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

RHCE---Web 服务器

文章目录 目录 文章目录 前言 一.Web服务器概述 网址及HTTP协议概述&#xff1a; HTTP协议请求过程&#xff1a; 二.搭建动态HTTP网页 动态网页概述&#xff1a; 搭建动态的HTTP协议网页&#xff1a; 总结 前言 通过上一个章节的学习了解了时间服务器以及远程连接服务器&a…

深信服安全GPT 2.0升级,开启安全运营“智能驾驶”旅程

9月22日&#xff0c;深信服对外展示安全GPT落地成果与2.0升级能力。来自各行业权威嘉宾代表&#xff1a;美的集团首席信息安全官&#xff08;CISO&#xff09;兼软件工程院院长、欧洲科学院院士&#xff08;MAE&#xff09;、IEEE Fellow、IET Fellow、ACM杰出科学家、AAIA Fel…

一个简单的工具,多种用途—J2L3x的优势

J2L3x 是一款流行的团队合作工具&#xff0c;许多组织和公司已经开始使用它来简化和提高沟通和协作的效率。J2L3x 的优势与其多种用途不无关系&#xff0c;下面将详细介绍。 1、实时通信与信息共享 J2L3x 通过实时通信功能&#xff0c;使团队成员随时随地都能保持联系。J2L3x…

向量数据库风起时,闭源「墨奇AI数据库」想成为第三种存在

AI大模型时代下,图片、视频、自然语言等多模态的非结构化数据量陡增,而大模型支持的token数有限,虽然可以在RLHF的配合下具备一定程度的“短期记忆”,但正是因为“长期记忆”的缺失,导致大模型经常会出现“一本正经地胡说八道”的情况。 区别于用来处理结构化数据的传统数…

【unity2023打包安卓工程】踩坑记录

这里写自定义目录标题 踩坑记录使用环境Unity的准备工作Windows10 SDKAndroidstudio第一个需要注意的地方第二个需要注意的地方第三个需要注意的地方第四个需要注意的地方第五个需要注意的地方第六个需要注意的 其他unity启动缓慢 更新更新一 2023.9.27 踩坑记录 踩了快一个星期…

在Vue中通过ElementUI构建前端页面【登录,注册】,在IEDA构建后端实现前后端分离

一.ElementUI组件入门 1.对于ElementUI的理解 是一套基于 Vue.js 的开源UI组件库&#xff0c;提供了丰富的可复用组件&#xff0c;可以帮助开发者快速构建美观、易用的前端界面 2.Element UI 的特点和优势 多样化的组件&#xff1a;Element UI 提供了众多常用的基础组件&#…

全网最牛,Jmeter接口自动化测试从0到1实施步骤(详细整理)

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 1、工具下载 JDK…

uniapp 微信小程序之隐私协议开发

uniapp 微信小程序之隐私协议开发 官网通知&#xff1a;https://developers.weixin.qq.com/miniprogram/dev/framework/user-privacy/PrivacyAuthorize.html 1、配置 __usePrivacyCheck__: true&#xff1b;位置 manifest.json : "mp-weixin":{"__usePrivacyCh…

数据结构:二叉树的基本概念

文章目录 1. 二叉树的定义2. 二叉树的特点3. 特殊二叉树斜树满二叉树完全二叉树 4. 二叉树的性质 1. 二叉树的定义 如果我们猜一个100以内的数字,该怎么猜才能理论最快呢? 第一种方式:从1,2一直猜到100, 反正数字都是100以内,总能猜到的 第二种方式:先猜50,如果比结果小,猜75…

Unity Urp无线延申的网格效果

无线延申的网格 该项目必须是再Urp项目 shader代码实现 Shader "Unlit/infTutorial1" {Properties{_Alpha ("Alpha", Range(0, 0.5)) 0.5}SubShader{Tags{"RenderPipeline""UniversalRenderPipeline""RenderType""…

VMware下的ubuntu虚拟机,实现虚拟机与本地硬盘间的文件互传

本次安装vmware tools工具&#xff0c;以实现二者间的文件互传。 1、打开VMware软件&#xff0c;运行Ubuntu系统虚拟机 安装过程需在ubuntu虚拟机启动的情况下&#xff0c;才能进行安装&#xff1b; 2、安装VMware Tools 在VM主菜单栏中&#xff0c;点击 “虚拟机&#xff0…

Linux ❀ 进程出现process information unavailable时的消除方法

[rootmaster ~]# jps 74963 -- process information unavailable 78678 Jps [rootmaster ~]# cd /tmp/hsperfdata_redhat/ # redhat为启动该java进程的用户ps -ef | grep $pid查找 [rootmaster hsperfdata_redhat]# ll total 32 -rw------- 1 redhat redhat 32768 Sep 27 15:…

使用日志分析工具了解网络情况

日志分析&#xff08;或日志文件分析&#xff09;是检查整个网络中生成的日志数据的过程&#xff0c;日志数据从各种来源生成&#xff0c;包括外围设备、工作站、服务器、应用程序以及其他硬件和软件组件&#xff0c;将它们收集到一个中心位置并进行分析&#xff0c;可以为了解…

深入解析JVM:双亲委派机制的原理与实践

双亲委派机制 引言概述流程工作原理&#xff1a; 优势自定义类加载器实际应用 主页传送门&#xff1a;&#x1f4c0; 传送 引言 在Java虚拟机&#xff08;JVM&#xff09;中&#xff0c;类加载是一个重要的概念&#xff0c;而双亲委派机制是类加载的核心之一。本文将深入研究双…

写代码生成流程图

我们在写文档&#xff0c;博客的时候&#xff0c;一般都会使用markdown语法&#xff0c;最常见的就是一些github开源项目的README。有时候会去画一些流程图&#xff0c;例如使用process.on或者xmind等第三方网站&#xff0c;然后截图插入到文档中。 今天我们介绍一种使用代码直…

分布式搜索引擎Elasticsearch

一、Elasticsearch介绍 1.Elasticsearch产生背景 大数据量的检索NoSql: not only sql,泛指非关系型的数据库Nginx的7层负载均衡和4层负载均衡2.Elasticsearch是什么 一个基于Lucene的分布式搜索和分析引擎,一个开源的高扩展的分布式全文检索引擎 Elasticsearch使用Java开发…

一次ES检索的性能优化经验记录

优化功能: 统一检索能力&#xff0c;为各服务所调用。 该接口并发压力大&#xff0c;压测效果不理想。 初步2k线程两台压测机预发环境压测结果两pod下为400qps左右&#xff0c;单pod 平均qps200&#xff0c;响应时间在五分钟之后达到了峰值&#xff0c;平响达到几十秒开外。 压…

跨境电商引流之Reddit营销,入门保姆级攻略

在当今竞争激烈的在线市场中&#xff0c;企业不断寻求新的方法来加强其数字营销工作。Reddit 是最受欢迎的社交媒体平台之一&#xff0c;为企业提供了巨大的潜力&#xff0c;可以通过引人入胜且相关的内容来接触目标受众。然而&#xff0c;将 Reddit 用于营销目的需要仔细考虑某…

企业专线成本高?贝锐蒲公英轻松实现财务系统远程访问

在办公及信息系统领域&#xff0c;许多企业纷纷采用金蝶等财务管理软件来提升运营效率。以某食品制造企业为例&#xff0c;该企业总部位于广州&#xff0c;并拥有湖北仙桃工厂、广州从化工厂和湖南平江工厂三大生产基地。为提高管理效率&#xff0c;该企业在广州总部局域网内部…

游戏社区-搭建的目的和意义是什么

在游戏社区中&#xff0c;用户的活跃度指标是至关重要的&#xff0c;因此在必要时&#xff0c;我们会进行指标转化&#xff0c;以丰富的内容形式来促进用户的活跃度&#xff1b;作为一个垂直社区&#xff0c;我们可以通过聚合和培养一批游戏KOL&#xff0c;建立用户之间的紧密联…