14、线程池ForkJoinPool实战及其工作原理分析

1. 由一道算法题引发的思考

算法题:如何充分利用多核CPU的性能,快速对一个2千万大小的数组进行排序?
1)首先这是一道排序的算法题,而且是需要使用高效的排序算法对2千万大小的数组进行排序,可以考虑使用快速排序或者归并排序。
2)可以使用多线程并行排序算法来充分利用多核CPU的性能。

2. 基于归并排序算法实现

快速对一个大小为2千万的数组进行排序,可以使用高效的归并排序算法来实现。

2.1 什么是归并排序

**归并排序(Merge Sort)是一种基于分治思想的排序算法。**归并排序的基本思想是将一个大数组分成两个相等大小的子数组,对每个子数组分别进行排序,然后将两个子数组合并成一个有序的大数组。因为常常使用递归实现(由先拆分后合并的性质决定的),所以我们称其为归并排序。
归并排序的步骤包括以下几个方面:

  • 将数组分成两个子数组
  • 对每个子数组进行排序
  • 合并两个有序的子数组
    归并排序的时间复杂度为O(nlogn),空间复杂度为O(n),其中n为数组的长度。
分治思想是将一个规模为N的问题分解为K个规模较小的子问题,这些子问题相互独立且与原问题性质相同。求出子问题的解,就可得到原问题的解。
分治思想的步骤如下:
undefined.分解:将要解决的问题划分成若干规模较小的同类问题;
undefined.求解:当子问题划分得足够小时,用较简单的方法解决;
undefined.合并:按原问题的要求,将子问题的解逐层合并构成原问题的解。
计算机十大经典算法中的归并排序、快速排序、二分查找都是基于分治思想实现的算法
分治任务模型图如下:

在这里插入图片描述

2.2 归并排序动图演示

归并排序演示

2.3 使用归并排序实现上面的算法题

单线程实现归并排序
单线程归并算法的实现,它的基本思路是将序列分成两个部分,分别进行递归排序,然后将排序好的子序列合并起来。

public class MergeSort {private final int[] arrayToSort;//要排序的数组private final int threshold;//拆分的阈值,低于此阈值就不再进行拆分public MergeSort(int[] arrayToSort, int threshold) {this.arrayToSort = arrayToSort;this.threshold = threshold;}/*** 排序* @return*/public int[] sequentialSort(){return sequentialSort(arrayToSort,threshold);}private int[] sequentialSort(int[] arrayToSort, int threshold) {//拆分后的数组长度小于阈值,直接进行排序if(arrayToSort.length<threshold){Arrays.sort(arrayToSort);}int midpoint = arrayToSort.length / 2;//对数组进行拆分int[] leftArray = Arrays.copyOfRange(arrayToSort, 0, midpoint);int[] rightArray = Arrays.copyOfRange(arrayToSort, midpoint, arrayToSort.length);//递归调用leftArray = sequentialSort(leftArray, threshold);rightArray = sequentialSort(rightArray,threshold);//合并排序结果return merge(leftArray,rightArray);}private static int[] merge(final int[] leftArray, final int[] rightArray) {int[] mergedArray = new int[leftArray.length + rightArray.length];int mergedArrayPos = 0;int leftArrayPos = 0;int rightArrayPos = 0;while (leftArrayPos < leftArray.length & rightArrayPos < rightArray.length){if (leftArray[leftArrayPos]<=rightArray[rightArrayPos]){mergedArray[mergedArrayPos] = leftArray[leftArrayPos];leftArrayPos++;}else {mergedArray[mergedArrayPos] = rightArray[rightArrayPos];rightArrayPos++;}mergedArrayPos++;}while (leftArrayPos < leftArray.length){mergedArray[mergedArrayPos] = leftArray[leftArrayPos];leftArrayPos++;mergedArrayPos++;}while (rightArrayPos < rightArray.length){mergedArray[mergedArrayPos] = rightArray[rightArrayPos];rightArrayPos++;mergedArrayPos++;}return mergedArray;}
}

Fork/Join并行归并排序
并行归并排序是一种利用多线程实现的归并排序算法。它的基本思路是将数据分成若干部分,然后在不同线程上对这些部分进行归并排序,最后将排好序的部分合并成有序数组。在多核CPU上,这种算法也能够有效提高排序速度。
可以使用Java的Fork/Join框架来实现归并排序的并行化

public class MergeSortTask extends RecursiveAction {private final int threshold; //拆分的阈值,低于此阈值就不再进行拆分private int[] arrayToSort; //要排序的数组public MergeSortTask(final int[] arrayToSort, final int threshold) {this.arrayToSort = arrayToSort;this.threshold = threshold;}@Overrideprotected void compute() {//拆分后的数组长度小于阈值,直接进行排序if (arrayToSort.length <= threshold) {// 调用jdk提供的排序方法Arrays.sort(arrayToSort);return;}// 对数组进行拆分int midpoint = arrayToSort.length / 2;int[] leftArray = Arrays.copyOfRange(arrayToSort, 0, midpoint);int[] rightArray = Arrays.copyOfRange(arrayToSort, midpoint, arrayToSort.length);MergeSortTask leftTask = new MergeSortTask(leftArray, threshold);MergeSortTask rightTask = new MergeSortTask(rightArray, threshold);//提交任务leftTask.fork();rightTask.fork();//阻塞当前线程,直到获取任务的执行结果leftTask.join();rightTask.join();// 合并排序结果arrayToSort = MergeSort.merge(leftTask.getSortedArray(), rightTask.getSortedArray());}public int[] getSortedArray() {return arrayToSort;}
}

在这个示例中,我们使用Fork/Join框架实现了归并排序算法,并通过递归调用实现了并行化。使用Fork/Join框架实现归并排序算法的关键在于将排序任务分解成小的任务,使用Fork/Join框架将这些小任务提交给线程池中的不同线程并行执行,并在最后将排序后的结果进行合并。这样可以充分利用多核CPU的并行处理能力,提高程序的执行效率。
测试结果对比
测试代码

public class Utils {/*** 随机生成数组* @param size 数组的大小* @return*/public static int[] buildRandomIntArray(final int size) {int[] arrayToCalculateSumOf = new int[size];Random generator = new Random();for (int i = 0; i < arrayToCalculateSumOf.length; i++) {arrayToCalculateSumOf[i] = generator.nextInt(100000000);}return arrayToCalculateSumOf;}
}
public class ArrayToSortMain {public static void main(String[] args) {//生成测试数组  用于归并排序int[] arrayToSortByMergeSort = Utils.buildRandomIntArray(20000000);//生成测试数组  用于forkjoin排序int[] arrayToSortByForkJoin = Arrays.copyOf(arrayToSortByMergeSort, arrayToSortByMergeSort.length);//获取处理器数量int processors = Runtime.getRuntime().availableProcessors();MergeSort mergeSort = new MergeSort(arrayToSortByMergeSort, processors);long startTime = System.nanoTime();// 归并排序mergeSort.mergeSort();long duration = System.nanoTime()-startTime;System.out.println("单线程归并排序时间: "+(duration/(1000f*1000f))+"毫秒");//利用forkjoin排序MergeSortTask mergeSortTask = new MergeSortTask(arrayToSortByForkJoin, processors);//构建forkjoin线程池ForkJoinPool forkJoinPool = new ForkJoinPool(processors);startTime = System.nanoTime();//执行排序任务forkJoinPool.invoke(mergeSortTask);duration = System.nanoTime()-startTime;System.out.println("forkjoin排序时间: "+(duration/(1000f*1000f))+"毫秒");}
}

根据测试结果可以看出,数组越大,利用Fork/Join框架实现的并行化归并排序比单线程归并排序的效率更高:在这里插入图片描述

2.4 并行实现归并排序的优化和注意事项

在实际应用中,我们需要考虑数据分布的均匀性、内存使用情况、线程切换开销等因素,以充分利用多核CPU并保证算法的正确性和效率。
以下是并行实现归并排序的一些优化和注意事项:

  • 任务的大小:任务大小的选择会影响并行算法的效率和负载均衡,如果任务太小,会造成任务划分和合并的开销过大;如果任务太大,会导致任务无法充分利用多核CPU并行处理能力。因此,在实际应用中需要根据数据量、CPU核心数等因素选择合适的任务大小。
  • 负载均衡:并行算法需要保证负载均衡,即各个线程执行的任务大小和时间应该尽可能相等,否则会导致某些线程负载过重,而其他线程负载过轻的情况。在归并排序中,可以通过递归调用实现负载均衡,但是需要注意递归的层数不能太深,否则会导致任务划分和合并的开销过大。
  • 数据分布:数据分布的均匀性也会影响并行算法的效率和负载均衡。在归并排序中,如果数据分布不均匀,会导致某些线程处理的数据量过大,而其他线程处理的数据量过小的情况。因此,在实际应用中需要考虑数据的分布情况,尽可能将数据分成大小相等的子数组。
  • 内存使用:并行算法需要考虑内存的使用情况,特别是在处理大规模数据时,内存的使用情况会对算法的执行效率产生重要影响。在归并排序中,可以通过对数据进行原地归并实现内存的节约,但是需要注意归并的实现方式,以避免数据的覆盖和不稳定排序等问题。
  • 线程切换:线程切换是并行算法的一个重要开销,需要尽量减少线程的切换次数,以提高算法的执行效率。在归并排序中,可以通过设置线程池的大小和调整任务大小等方式控制线程的数量和切换开销,以实现算法的最优性能。

3. Fork/Join框架介绍

3.1 什么是Fork/Join

Fork/Join是一个是一个并行计算的框架,主要就是用来支持分治任务模型的,这个计算框架里的 Fork 对应的是分治任务模型里的任务分解,Join 对应的是结果合并。它的核心思想是将一个大任务分成许多小任务,然后并行执行这些小任务,最终将它们的结果合并成一个大的结果。它适用于可以采用分治策略的计算密集型任务,例如大规模数组的排序、图形的渲染、复杂算法的求解等。在这里插入图片描述

3.2 应用场景

1.并行计算:
ForkJoinPool 提供了一种方便的方式来执行大规模的计算任务,并充分利用多核处理器的性能优势。通过将大任务分解成小任务,并通过工作窃取算法实现任务的并行执行,可以提高计算效率。
2.递归任务处理:
ForkJoinPool 特别适用于递归式的任务分解和执行。它可以将一个大任务递归地分解成许多小任务,并通过工作窃取算法动态地将这些小任务分配给工作线程执行。
3.并行流操作:
Java 8 引入了 Stream API,用于对集合进行函数式编程风格的操作。ForkJoinPool 通常用于执行并行流操作中的并行计算部分,例如对流中的元素进行过滤、映射、聚合等操作。
4.高性能任务执行:
ForkJoinPool 提供了一种高性能的任务执行机制,通过对任务进行动态调度和线程池管理,可以有效地利用系统资源,并在多核处理器上实现任务的并行执行。
总的来说,ForkJoinPool 类在 Java 中具有广泛的应用场景,特别适用于大规模的并行计算任务和递归式的任务处理。它通过工作窃取算法和任务分割合并机制,提供了一种高效的并行计算方式,可以显著提高计算效率和性能。

3.3 Fork/Join使用

Fork/Join框架的主要组成部分是ForkJoinPool、ForkJoinTask。ForkJoinPool是一个线程池,它用于管理ForkJoin任务的执行。ForkJoinTask是一个抽象类,用于表示可以被分割成更小部分的任务。
ForkJoinPool
ForkJoinPool是Fork/Join框架中的线程池类,它用于管理Fork/Join任务的线程。ForkJoinPool类包括一些重要的方法,例如submit()、invoke()、shutdown()、awaitTermination()等,用于提交任务、执行任务、关闭线程池和等待任务的执行结果。ForkJoinPool类中还包括一些参数,例如线程池的大小、工作线程的优先级、任务队列的容量等,可以根据具体的应用场景进行设置。
构造器在这里插入图片描述
ForkJoinPool中有四个核心参数,用于控制线程池的并行数、工作线程的创建、异常处理和模式指定等。各参数解释如下:

  • int parallelism:指定并行级别(parallelism level)。ForkJoinPool将根据这个设定,决定工作线程的数量。如果未设置的话,将使用Runtime.getRuntime().availableProcessors()来设置并行级别;
  • ForkJoinWorkerThreadFactory factory:ForkJoinPool在创建线程时,会通过factory来创建。注意,这里需要实现的是ForkJoinWorkerThreadFactory,而不是ThreadFactory。如果你不指定factory,那么将由默认的DefaultForkJoinWorkerThreadFactory负责线程的创建工作;
  • UncaughtExceptionHandler handler:指定异常处理器,当任务在运行中出错时,将由设定的处理器处理;
  • boolean asyncMode:设置队列的工作模式。当asyncMode为true时,将使用先进先出队列,而为false时则使用后进先出的模式。
//获取处理器数量
int processors = Runtime.getRuntime().availableProcessors();
//构建forkjoin线程池
ForkJoinPool forkJoinPool = new ForkJoinPool(processors);

任务提交方式
任务提交是ForkJoinPool的核心能力之一,提交任务有三种方式:

返回值方法
提交异步执行voidexecute(ForkJoinTask<?> task) execute(Runnable task)
等待并获取结果Tinvoke(ForkJoinTask task)
提交执行获取Future结果ForkJoinTasksubmit(ForkJoinTask task) submit(Callable task) submit(Runnable task) submit(Runnable task, T result)

ForkJoinTask
ForkJoinTask是Fork/Join框架中的抽象类,它定义了执行任务的基本接口。用户可以通过继承ForkJoinTask类来实现自己的任务类,并重写其中的compute()方法来定义任务的执行逻辑。通常情况下我们不需要直接继承ForkJoinTask类,而只需要继承它的子类,Fork/Join框架提供了以下三个子类:

  • RecursiveAction:用于递归执行但不需要返回结果的任务。
  • RecursiveTask :用于递归执行需要返回结果的任务。
  • CountedCompleter :在任务完成执行后会触发执行一个自定义的钩子函数

调用方法
ForkJoinTask 最核心的是 fork() 方法和 join() 方法,承载着主要的任务协调作用,一个用于任务提交,一个用于结果获取。

  • fork()——提交任务
    fork()方法用于向当前任务所运行的线程池中提交任务。如果当前线程是ForkJoinWorkerThread类型,将会放入该线程的工作队列,否则放入common线程池的工作队列中。
  • join()——获取任务执行结果
    join()方法用于获取任务的执行结果。调用join()时,将阻塞当前线程直到对应的子任务完成运行并返回结果。
    处理递归任务
    计算斐波那契数列
    斐波那契数列指的是这样一个数列:1,1,2,3,5,8,13,21,34,55,89… 这个数列从第3项开始,每一项都等于前两项之和。
public class Fibonacci extends RecursiveTask<Integer> {final int n;Fibonacci(int n) {this.n = n;}/*** 重写RecursiveTask的compute()方法* @return*/protected Integer compute() {if (n <= 1)return n;Fibonacci f1 = new Fibonacci(n - 1);//提交任务f1.fork();Fibonacci f2 = new Fibonacci(n - 2);//合并结果return f2.compute() + f1.join();}public static void main(String[] args) {//构建forkjoin线程池ForkJoinPool pool = new ForkJoinPool();Fibonacci task = new Fibonacci(10);//提交任务并一直阻塞直到任务 执行完成返回合并结果。int result = pool.invoke(task);System.out.println(result);}
}

思考:如果n为100000,执行上面的代码会发生什么问题?在这里插入图片描述
在上面的例子中,由于递归计算Fibonacci数列的任务数量呈指数级增长,当n较大时,就容易出现StackOverflowError错误。这个错误通常发生在递归过程中,由于递归过程中每次调用函数都会在栈中创建一个新的栈帧,当递归深度过大时,栈空间就会被耗尽,导致StackOverflowError错误。
栈溢出如何解决
我们可以使用迭代的方式计算Fibonacci数列,以避免递归过程中占用大量的栈空间。下面是一个使用迭代方式计算Fibonacci数列的例子:

public class Fibonacci {public static void main(String[] args) {int n = 100000;long[] fib = new long[n + 1];fib[0] = 0;fib[1] = 1;for (int i = 2; i <= n; i++) {fib[i] = fib[i - 1] + fib[i - 2];}System.out.println(fib[n]);}
}

处理递归任务注意事项

对于一些递归深度较大的任务,使用Fork/Join框架可能会出现任务调度和内存消耗的问题。
当递归深度较大时,会产生大量的子任务,这些子任务可能被调度到不同的线程中执行,而线程的创建和销毁以及任务调度的开销都会占用大量的资源,从而导致性能下降。
此外,对于递归深度较大的任务,由于每个子任务所占用的栈空间较大,可能会导致内存消耗过大,从而引起内存溢出的问题。
因此,在使用Fork/Join框架处理递归任务时,需要根据实际情况来评估递归深度和任务粒度,以避免任务调度和内存消耗的问题。如果递归深度较大,可以尝试采用其他方法来优化算法,如使用迭代方式替代递归,或者限制递归深度来减少任务数量,以避免Fork/Join框架的缺点。

处理阻塞任务
在ForkJoinPool中使用阻塞型任务时需要注意以下几点:
1.防止线程饥饿:当一个线程在执行一个阻塞型任务时,它将会一直等待任务完成,这时如果没有其他线程可以窃取任务,那么该线程将一直被阻塞,直到任务完成为止。为了避免这种情况,应该避免在ForkJoinPool中提交大量的阻塞型任务。
2.使用特定的线程池:为了最大程度地利用ForkJoinPool的性能,可以使用专门的线程池来处理阻塞型任务,这些线程不会被ForkJoinPool的窃取机制所影响。例如,可以使用ThreadPoolExecutor来创建一个线程池,然后将这个线程池作为ForkJoinPool的执行器,这样就可以使用ThreadPoolExecutor来处理阻塞型任务,而使用ForkJoinPool来处理非阻塞型任务。
3.不要阻塞工作线程:如果在ForkJoinPool中使用阻塞型任务,那么需要确保这些任务不会阻塞工作线程,否则会导致整个线程池的性能下降。为了避免这种情况,可以将阻塞型任务提交到一个专门的线程池中,或者使用CompletableFuture等异步编程工具来处理阻塞型任务。
下面是一个使用阻塞型任务的例子,这个例子展示了如何使用CompletableFuture来处理阻塞型任务:

public class BlockingTaskDemo {public static void main(String[] args) {//构建一个forkjoin线程池ForkJoinPool pool = new ForkJoinPool();//创建一个异步任务,并将其提交到ForkJoinPool中执行CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {try {// 模拟一个耗时的任务TimeUnit.SECONDS.sleep(5);return "Hello, world!";} catch (InterruptedException e) {e.printStackTrace();return null;}}, pool);try {// 等待任务完成,并获取结果String result = future.get();System.out.println(result);} catch (InterruptedException e) {e.printStackTrace();} catch (ExecutionException e) {e.printStackTrace();} finally {//关闭ForkJoinPool,释放资源pool.shutdown();}}
}

在这个例子中,我们使用了CompletableFuture来处理阻塞型任务,因为它可以避免阻塞ForkJoinPool中的工作线程。另外,我们也可以使用专门的线程池来处理阻塞型任务,例如ThreadPoolExecutor等。不管是哪种方式,都需要避免在ForkJoinPool中提交大量的阻塞型任务,以免影响整个线程池的性能。

3.4 ForkJoinPool工作原理

ForkJoinPool 内部有多个任务队列,当我们通过 ForkJoinPool 的 invoke() 或者 submit() 方法提交任务时,ForkJoinPool 根据一定的路由规则把任务提交到一个任务队列中,如果任务在执行过程中会创建出子任务,那么子任务会提交到工作线程对应的任务队列中。
如果工作线程对应的任务队列空了,是不是就没活儿干了呢?不是的,ForkJoinPool 支持一种叫做“任务窃取”的机制,如果工作线程空闲了,那它可以“窃取”其他工作任务队列里的任务。如此一来,所有的工作线程都不会闲下来了。
核心设计:

  • ForkJoinPool的任务会被内部存储了一个WorkQueue数组,提交给ForkJoinPool的任务会被分配到指定的WorkQueue上执行
  • 每个WorkQueue内部维护了一个ForkJoinTask数组用来存储待执行的任务,以及一个独立的ForkJoinWorkerThread用来真正执行任务

工作线程ForkJoinWorkerThread
ForkJoinWorkerThread是ForkJoinPool中的一个专门用于执行任务的线程。当一个ForkJoinWorkerThread被创建时,它会自动注册一个WorkQueue到ForkJoinPool中。这个WorkQueue是该线程专门用于存储自己的任务的队列,只能出现在WorkQueues[]的奇数位。
ForkJoinWorkerThread工作线程启动后就会扫描偷取任务执行,另外当其在 ForkJoinTask#join() 等待返回结果时如果被 ForkJoinPool 线程池发现其任务队列为空或者已经将当前任务执行完毕,也会通过工作窃取算法从其他任务队列中获取任务分配到其任务队列中并执行。在这里插入图片描述
工作队列WorkQueue
WorkQueue是一个双端队列,用于存储工作线程自己的任务。每个工作线程都会维护一个本地的WorkQueue,并且优先执行本地队列中的任务。当本地队列中的任务执行完毕后,工作线程会尝试从其他线程的WorkQueue中窃取任务。在这里插入图片描述
工作窃取
ForkJoinPool与ThreadPoolExecutor有个很大的不同之处在于,ForkJoinPool引入了工作窃取设计,它是其性能保证的关键之一。工作窃取,就是允许空闲线程从繁忙线程的双端队列中窃取任务。默认情况下,工作线程从它自己的双端队列的头部获取任务。但是,当自己的任务为空时,线程会从其他繁忙线程双端队列的尾部中获取任务。这种方法,最大限度地减少了线程竞争任务的可能性。
在这里插入图片描述

通过工作窃取,Fork/Join框架可以实现任务的自动负载均衡,以充分利用多核CPU的计算能力,同时也可以避免线程的饥饿和延迟问题

3.5 ForkJoinPool执行流程

在这里插入图片描述

3.6 总结

Fork/Join是一种基于分治思想的模型,在并发处理计算型任务时有着显著的优势。其效率的提升主要得益于两个方面:

  • 任务切分:将大的任务分割成更小粒度的小任务,让更多的线程参与执行;
  • 任务窃取:通过任务窃取,充分地利用空闲线程,并减少竞争。
    在使用ForkJoinPool时,需要特别注意任务的类型是否为纯函数计算类型,也就是这些任务不应该关心状态或者外界的变化,这样才是最安全的做法。如果是阻塞类型任务,那么你需要谨慎评估技术方案。虽然ForkJoinPool也能处理阻塞类型任务,但可能会带来复杂的管理成本。

和普通线程池之间的区别

  • 工作窃取算法
    ForkJoinPool采用工作窃取算法来提高线程的利用率,而普通线程池则采用任务队列来管理任务。在工作窃取算法中,当一个线程完成自己的任务后,它可以从其它线程的队列中获取一个任务来执行,以此来提高线程的利用率。
  • 任务的分解和合并
    ForkJoinPool可以将一个大任务分解为多个小任务,并行地执行这些小任务,最终将它们的结果合并起来得到最终结果。而普通线程池只能按照提交的任务顺序一个一个地执行任务。
  • 工作线程的数量
    ForkJoinPool会根据当前系统的CPU核心数来自动设置工作线程的数量,以最大限度地发挥CPU的性能优势。而普通线程池需要手动设置线程池的大小,如果设置不合理,可能会导致线程过多或过少,从而影响程序的性能。
  • 任务类型
    ForkJoinPool适用于执行大规模任务并行化,而普通线程池适用于执行一些短小的任务,如处理请求等。

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

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

安卓13禁止待机 永不休眠 android13永不休眠

总纲 android13 rom 开发总纲说明 文章目录 1.前言2.问题分析3.代码分析4.代码修改5.彩蛋1.前言 设置 =》显示 =》屏幕超时 =》 永不。 我们通过修改系统待机时间配置,来达到设置屏幕超时的配置。像网上好多文章都只写了在哪里改,改什么东西,但是实际上并未写明为什么要改那…

Redis的数据类型常用命令

目录 前言 String字符串 常见命令 set get mget mset setnx incr incrby decr decyby append Hash哈希 常见命令 hset hget hexists hdel hkeys hvals hgetall hmget hlen hsetnx List 列表 常见命令 lpush lrange lpushx rpush rpushhx lpop…

Mybtais高级结果映射-多对一表映射

前言 从前我们只进行单表的sql操作&#xff0c;但是如果涉及多张表的操作&#xff0c;原先的映射关系就不太适用了&#xff0c;因此这里将会介绍Mybatis的高级结果映射技巧 准备工作 准备两张数据库表&#xff0c;一个员工表&#xff0c;一个部门表&#xff08;我们使用oracle的…

一份转型大模型产品经理指南

作为一个产品经理&#xff0c;你可能已经熟悉了一些常见的AI技术和应用&#xff0c;比如机器学习、深度学习、自然语言处理、计算机视觉等。 但是&#xff0c;你是否了解什么是大模型&#xff1f;大模型又有什么特点和优势&#xff1f;为什么大模型会成为AI领域的一个重要趋势…

SQL语言入门

一、SQL语言入门&#xff1a; 数据库管理人员&#xff08;DBA&#xff09;通过数据库管理系统&#xff08;DBMS&#xff09;可以对数据库&#xff08;DB&#xff09;中的数据进行操作 SQL是一种非过程化语言&#xff0c;只需提出“做什么”&#xff0c;而不需要指明“怎么做”…

如何打造一个圈子社交系统?社交圈子论坛系统源码该如何实现

要打造一个圈子社交系统&#xff0c;并实现其源码&#xff0c;需要经历一系列详细的步骤&#xff0c;包括需求分析、系统设计、技术选型、开发、测试以及部署等。以下是一个全面的指南&#xff1a; 一、需求分析 明确目标用户&#xff1a;确定你的社交圈子面向哪类用户群体&a…

将U盘作为启动项报错:Verifying shim SBAT data failed: Security Policy Violation

问题描述 今天给新电脑装双系统的时候&#xff0c;将U盘设为启动项&#xff0c;总是报错&#xff0c;起初以为是启动盘做的有问题&#xff0c;报错如下&#xff1a; Verifying shim SBAT data failed: Security Policy Violation Something has gone seriously wrong: SBAT s…

栈的深度解析:顺序栈与链栈的实现

引言 栈是一种重要的线性数据结构&#xff0c;遵循“后进先出”&#xff08;LIFO&#xff09;的原则。栈的应用非常广泛&#xff0c;如表达式求值、括号匹配、递归实现等。在本文中&#xff0c;我们将深入探讨栈的概念&#xff0c;并通过顺序栈和链栈两种实现方式进行对比分析…

路径报错问题

项目场景&#xff1a; 假设这是我的项目结构&#xff0c;我现在需要在aa.js文件中引入并使用aa.geojson文件&#xff0c; 问题&#xff1a; 当我引入路径是const filePath ../geo/aa.geojson&#xff1b;的时候&#xff0c;系统报错 "aa.geojson is not Found",找不…

[000-002-01].第29节:MySQL执行流程

1、MySQL的查询流程&#xff1a; 客户端请求进入到数据库服务器后&#xff0c;先进行查询缓存&#xff0c;如果命中&#xff0c;那么就返回结果&#xff1b;如果没命中&#xff0c;进入到解析器&#xff0c;进行词法解析和语法解析&#xff0c;生成解析树&#xff1b;然后进入到…

企业图纸文档管理系统推荐 三大企业图纸文档管理软件详细介绍

在现代企业的设计和生产过程中&#xff0c;图纸文档的管理是至关重要的一环。 无论是建筑、制造业&#xff0c;还是技术研发领域&#xff0c;图纸文档的正确存储、分享与管理能够极大提升工作效率&#xff0c;避免误操作或信息丢失。 接下来&#xff0c;小编将为大家推荐三款优…

采购管理系统SRM助力电子元器件制造企业构建高效的供应商管理体系

在当今快速迭代的电子元器件制造行业中&#xff0c;构建一套高效、透明的供应商管理体系对于提升企业竞争力、降低运营成本、确保供应链稳定性至关重要。采购管理系统(SRM&#xff0c;Supplier Relationship Management)作为这一领域的得力助手&#xff0c;正引领着电子元器件制…

远程连接服务器时出现“这可能是由于CredSSP加密数据库修正”的错误提示的解决办法

现象&#xff1a; 当远程连接服务器时&#xff0c;有时候会出现以下提示&#xff0c;从而导致无法成功连接服务&#xff0c;如下所述&#xff1a; 原因&#xff1a; 远程桌面使用的是“凭据安全支持提供程序协议 (CredSSP) ”&#xff0c;这个协议在未修补的版本中是存在漏…

scrapy 爬取微博(四)【最新超详细解析】: 设计篇

一、功能设计 开始开发之前我们先对本文的scrapy微博爬虫工程进行一个功能的设计&#xff0c;包含的功能模块如下&#xff1a; 功能模块具体描述微博文章爬取根据关键词、时间范围等参数爬取微博文章&#xff0c;获取用户名、ID、微博mid、微博内容、点赞、转发、评论等数据微…

《深度学习》卷积神经网络 使用最优模型、调整学习率 用法解析及案例实现

目录 一、使用最优模型 1、什么是最优模型 2、如实使用最优模型 1&#xff09;读取参数方法 2&#xff09;调用完整模型方法 3&#xff09;实例 完整代码&#xff1a; 打印结果&#xff1a; 二、调整学习率 1、什么是调整学习率 2、目的 3、调整学习率的方法 1&am…

C++ 语言课程笔记

C 语言课程笔记 C语言程序设计第四版——谭浩强著&#xff0c;此书中的代码题大部分已经在本文中展示&#xff0c;以及南开大学 C 语言上机题库 100 题的作答&#xff0c;如果有作答不正确的地方或者可优化的地方&#xff0c;欢迎指正&#xff0c;谢谢&#xff01; 001 屏幕输出…

DAMODEL丹摩智算平台实践CogVideoX

文章目录 前言 一、平台账号注册并登录 二、部署CogVideoX &#xff08;一&#xff09;简介 &#xff08;二&#xff09;部署 1. 创建实例 2. 配置环境和依赖 3.预制模型与配置文件 三、开始运行 总结 前言 该文章主要记录DAMODEL丹摩智算平台实践过程与心得体会&…

【YashanDB知识库】客户端字符集与数据库字符集兼容问题

本文转自YashanDB官网&#xff0c;具体内容请见https://www.yashandb.com/newsinfo/7352675.html?templateId1718516 问题现象 客户端yasql配置字符集为GBK&#xff0c;服务端yasdb配置字符集为UTF8&#xff0c;之后执行语句&#xff1a; 会发现&#xff1a; 期望是两个都…

FAT32取证分析

前言&#xff1a; 在正常工作中经常会有数据恢复或者取证分析的场景&#xff0c;数据是否能被恢复&#xff0c;主要还是看数据是否被覆盖&#xff0c;正常情况下文件虽然被删除&#xff0c;只是修对应的标志位&#xff0c;文件本身数据并不会被破坏&#xff0c;所以我们就可以…

【Java】1.初识Java

文章目录 1. 使用记事本创建.Java程序2. 使用IDEA创建第一个Java程序3. 标识符4. 关键字 1. 使用记事本创建.Java程序 先创建了HelloWorld.java这个文件。然后用Sublime Text记事本打开&#xff0c;输入以下代码。 winr&#xff0c;cmd输入D:切换到D盘&#xff0c;然后输入cd …