ForkJoinPool详解

news2024/12/26 14:20:10

一、归并排序

1、简介

先把一个庞大的数组进行递归分解,把拆分的数组排好序,之后把拆分排好序的数组进行有序的合并,必须住的问题就是,递归拆分的阈值,比如当数组长度拆分到10000时候就不拆了,不能无限制的拆分,如果栈帧入栈太多,而受栈大小的限制会发生栈溢出

归并排序的时间复杂度为O(nlogn),空间复杂度为O(n),其中n为数组的长度。

数组排序归并排序图解

2、单线程进行归并排序

public class ArraySortTest {
	
	public static void main(String[] args) {
		//生成两千万测试数组  用于归并排序
        int[] arrayToSortByMergeSort = buildRandomIntArray(20000000);
        long startTime = System.nanoTime();
        int[] mergeSort = mergeSort(arrayToSortByMergeSort, Runtime.getRuntime().availableProcessors());
        long duration = System.nanoTime()-startTime;
        System.out.println("单线程归并排序时间: "+(duration/(1000f*1000f))+"毫秒");
	}

	public static int[] mergeSort(int[] intArray, int threshold) {
		if (intArray.length < threshold) {
			// 小于阈值之后不再进行拆分,把小数组顺序整理好(这里先采用冒泡排序 冒泡的思路就是把数组中最大的值往后移)
			int temp;
			for (int i = 0; i < intArray.length - 1; i++) {
				// 判断是否发生交换
				boolean isSwap = false;
				for (int j = 0; j < intArray.length - 1 - i; j++) {
					if (intArray[j] > intArray[j+1]) {
						temp = intArray[j];
						intArray[j] = intArray[j+1];
						intArray[j + 1] = temp;
						isSwap = true;
					}
				}
				if (!isSwap) {
					// 如果没有发生交换 说明数组是有序的
					break;
				}
			}
			return intArray;
		}
		
		// 大于阈值 继续拆分
		int midpoint = intArray.length / 2;
		int[] leftArray = Arrays.copyOfRange(intArray, 0, midpoint);
		int[] rightArray = Arrays.copyOfRange(intArray, midpoint, intArray.length);
		// 递归进行拆分
		leftArray = mergeSort(leftArray, threshold);
		rightArray = mergeSort(rightArray, threshold);
		// 合并排序结果
		return merge(leftArray, rightArray);
	}

	/**
	 * 合并
	 * @param leftArray
	 * @param rightArray
	 * @return
	 */
	private static int[] merge(final int[] leftArray, final int[] rightArray) {
		// 新建一个新的数组用于存储返回结果集
		int[] mergeArray = new int[leftArray.length + rightArray.length];
		int mergeIndex = 0, leftIndex = 0, rightIndex = 0;
		while (leftIndex < leftArray.length && rightIndex < rightArray.length) {
			if (leftArray[leftIndex] <= rightArray[rightIndex]) {
				mergeArray[mergeIndex] = leftArray[leftIndex];
				leftIndex++;
			} else {
				mergeArray[mergeIndex] = rightArray[rightIndex];
				rightIndex++;
			}
			mergeIndex++;
		}
		// 两个数组中最后总会有一个数组剩下一个元素 把最后一个元素加到mergeArray中
		while (leftIndex < leftArray.length) {
			mergeArray[mergeIndex] = leftArray[leftIndex];
			leftIndex++;
			mergeIndex++;
		}
		while (rightIndex < rightArray.length) {
			mergeArray[mergeIndex] = rightArray[rightIndex];
			rightIndex++;
			mergeIndex++;
		}
		return mergeArray;
	}
	
	/**
	 * 随机生成指定size大小的数组
	 * @param size
	 * @return
	 */
	private 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;
	}
}

打印结果:单线程归并排序时间: 4081.0142毫秒 

3、使用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;
	}

	@Override
	protected 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);

		//调用任务,阻塞当前线程,直到所有子任务执行完成
		invokeAll(leftTask,rightTask);
		//提交任务
//		leftTask.fork();
//		rightTask.fork();
//		//合并结果
//		leftTask.join();
//		rightTask.join();

		// 合并排序结果
		arrayToSort = ArraySortTest.merge(leftTask.getSortedArray(), rightTask.getSortedArray());
	}

	public int[] getSortedArray() {
		return arrayToSort;
	}
	
	
	public static void main(String[] args) {
		int[] arrayToSortByMergeSort = ArraySortTest.buildRandomIntArray(20000000);
		//利用forkjoin排序
        MergeSortTask mergeSortTask = new MergeSortTask(arrayToSortByMergeSort, Runtime.getRuntime().availableProcessors());
        //构建forkjoin线程池
        ForkJoinPool forkJoinPool = new ForkJoinPool(Runtime.getRuntime().availableProcessors());
        long startTime = System.nanoTime();
        //执行排序任务
        forkJoinPool.invoke(mergeSortTask);
        long duration = System.nanoTime()-startTime;
        System.out.println("forkjoin排序时间: "+(duration/(1000f*1000f))+"毫秒");
	}
}

打印结果:forkjoin排序时间: 1176.6014毫秒 

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

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

二、Fork/Join介绍

1、介绍

Fork/Join是一个是一个并行计算的框架,主要就是用来支持分治任务模型的,这个计算框架里的 Fork 对应的是分治任务模型里的任务分解,Join 对应的是结果合并。它的核心思想是将一个大任务分成许多小任务,然后并行执行这些小任务,最终将它们的结果合并成一个大的结果。

2、应用场景

Fork/Join框架的应用场景包括以下几个方面:

  • 递归分解型任务

Fork/Join框架特别适用于递归分解型的任务,例如排序、归并、遍历等。这些任务通常可以将大的任务分解成若干个子任务,每个子任务可以独立执行,并且可以通过归并操作将子任务的结果合并成一个有序的结果。

  • 数组处理

Fork/Join框架还可以用于数组的处理,例如数组的排序、查找、统计等。在处理大型数组时,Fork/Join框架可以将数组分成若干个子数组,并行地处理每个子数组,最后将处理后的子数组合并成一个有序的大数组。

  • 并行化算法

Fork/Join框架还可以用于并行化算法的实现,例如并行化的图像处理算法、并行化的机器学习算法等。在这些算法中,可以将问题分解成若干个子问题,并行地解决每个子问题,然后将子问题的结果合并起来得到最终的解决方案。

  • 大数据处理

Fork/Join框架还可以用于大数据处理,例如大型日志文件的处理、大型数据库的查询等。在处理大数据时,可以将数据分成若干个分片,并行地处理每个分片,最后将处理后的分片合并成一个完整的结果。

3、使用

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的核心能力之一,提交任务有三种方式

返回值

方法

提交异步执行

void 

execute(ForkJoinTask task)

execute(Runnable task)

等待并获取结果

T

invoke(ForkJoinTask task)

提交执行获取Future结果

ForkJoinTask

submit(ForkJoinTask task)

submit(Callable task)

submit(Runnable task)

submit(Runnable task, T result)

和普通线程池之间的区别

  • 工作窃取算法

ForkJoinPool采用工作窃取算法来提高线程的利用率,而普通线程池则采用任务队列来管理任务。在工作窃取算法中,当一个线程完成自己的任务后,它可以从其它线程的队列中获取一个任务来执行,以此来提高线程的利用率。

  • 任务的分解和合并

ForkJoinPool可以将一个大任务分解为多个小任务,并行地执行这些小任务,最终将它们的结果合并起来得到最终结果。而普通线程池只能按照提交的任务顺序一个一个地执行任务。

  • 工作线程的数量

ForkJoinPool会根据当前系统的CPU核心数来自动设置工作线程的数量,以最大限度地发挥CPU的性能优势。而普通线程池需要手动设置线程池的大小,如果设置不合理,可能会导致线程过多或过少,从而影响程序的性能。

  • 任务类型

ForkJoinPool适用于执行大规模任务并行化,而普通线程池适用于执行一些短小的任务,如处理请求等。

 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);
    }
}

在上面的例子中,由于递归计算Fibonacci数列的任务数量呈指数级增长,当n较大时,就容易出现StackOverflowError错误。这个错误通常发生在递归过程中,由于递归过程中每次调用函数都会在栈中创建一个新的栈帧,当递归深度过大时,栈空间就会被耗尽,导致StackOverflowError错误。 

处理递归任务注意事项

对于一些递归深度较大的任务,使用Fork/Join框架可能会出现任务调度和内存消耗的问题。

当递归深度较大时,会产生大量的子任务,这些子任务可能被调度到不同的线程中执行,而线程的创建和销毁以及任务调度的开销都会占用大量的资源,从而导致性能下降。

此外,对于递归深度较大的任务,由于每个子任务所占用的栈空间较大,可能会导致内存消耗过大,从而引起内存溢出的问题。

因此,在使用Fork/Join框架处理递归任务时,需要根据实际情况来评估递归深度和任务粒度,以避免任务调度和内存消耗的问题。如果递归深度较大,可以尝试采用其他方法来优化算法,如使用迭代方式替代递归,或者限制递归深度来减少任务数量,以避免Fork/Join框架的缺点。

处理阻塞任务

在ForkJoinPool中使用阻塞型任务时需要注意以下几点:

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

4、工作原理

ThreadPoolExecutor是多个线程对应一个任务队列,ForkJoinPool是每个线程对应一个任务队列

// 入队方法
final void push(ForkJoinTask<?> task) {
    ForkJoinTask<?>[] a; ForkJoinPool p;
    int b = base, s = top, n;
    if ((a = array) != null) {    // ignore if queue removed
        int m = a.length - 1;     // fenced write for task visibility
        // 通过ForkJoinTask数组的计算下一个任务所存储的偏移量 然后添加到队列中
        U.putOrderedObject(a, ((m & s) << ASHIFT) + ABASE, task);
        // 队列指针加1
        U.putOrderedInt(this, QTOP, s + 1);
        if ((n = s - b) <= 1) {
            if ((p = pool) != null)
                p.signalWork(p.workQueues, this);
        }
        else if (n >= m)
            growArray();
    }
}
// 出队方法
final ForkJoinTask<?> pop() {
    ForkJoinTask<?>[] a; ForkJoinTask<?> t; int m;
    if ((a = array) != null && (m = a.length - 1) >= 0) {
        for (int s; (s = top - 1) - base >= 0;) {
            long j = ((m & s) << ASHIFT) + ABASE;
            if ((t = (ForkJoinTask<?>)U.getObject(a, j)) == null)
                break;
            // CAS方式把所在位置的任务置为空
            if (U.compareAndSwapObject(a, j, t, null)) {
                // s=top-1 这里可以就看出是后进先出的结构
                U.putOrderedInt(this, QTOP, s);
                // 把需要出对的任务返回
                return t;
            }
        }
    }
    return null;
}
// 其它线程窃取任务方法
final ForkJoinTask<?> poll() {
            ForkJoinTask<?>[] a; int b; ForkJoinTask<?> t;
            while ((b = base) - top < 0 && (a = array) != null) {
                int j = (((a.length - 1) & b) << ASHIFT) + ABASE;
                t = (ForkJoinTask<?>)U.getObjectVolatile(a, j);
                if (base == b) {
                    if (t != null) {
                        // 是通过base计算出队任务在数组中的偏移量 CAS置为空
                        if (U.compareAndSwapObject(a, j, t, null)) {
                            // 数组在底部base要加1出队
                            base = b + 1;
                            // 返回底部置为空的任务
                            return t;
                        }
                    }
                    else if (b + 1 == top) // now empty
                        break;
                }
            }
            return null;
        }

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

  • 任务切分:将大的任务分割成更小粒度的小任务,让更多的线程参与执行;
  • 任务窃取:通过任务窃取,充分地利用空闲线程,并减少竞争。

在使用ForkJoinPool时,需要特别注意任务的类型是否为纯函数计算类型,也就是这些任务不应该关心状态或者外界的变化,这样才是最安全的做法。如果是阻塞类型任务,那么你需要谨慎评估技术方案。虽然ForkJoinPool也能处理阻塞类型任务,但可能会带来复杂的管理成本。

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

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

相关文章

TPU编程竞赛系列 | 创客北京2023·算能AI+边缘计算专项赛开始啦!

为助力北京市高精尖产业发展&#xff0c;构建大中小企业相互依存、相互促进的企业发展生态&#xff0c;打造北京市有影响力的双创服务品牌赛事&#xff0c;“创客北京”大赛组委会联合算能举办AI边缘计算方向专项赛。 1.赛题任务 本赛题基于“AI边缘计算”方向&#xff0c;针对…

21、springboot的宽松绑定及属性处理类的构造注入

springboot的宽松绑定及属性处理类的构造注入 ★ 如何使用属性处理类所读取的属性 属性处理类最终变成了Spring容器中的一个Bean组件&#xff0c;因此接下来Spring即可将该Bean组件注入任意其他组件。 这种做法的好处是&#xff1a;可以将大量的配置信息封装一个对象——所以…

利用openTCS实现车辆调度系统(三)车辆适配器解读,封装自己的适配器

适配器的官方解释&#xff1a;openTCS 支持自定义车辆驱动程序的集成&#xff0c;这些驱动程序实现特定于车辆的通信协议&#xff0c;从而在内核和车辆之间进行调解。 由于其功能&#xff0c;车辆驾驶员也称为通信适配器。 openTCS适配器。欢迎随时沟通 1、源码下载 github下…

arcgis宗地或者地块四至权利人信息提取教程

ARCGIS怎样将图斑四邻的名称及方位加入其属性表 以前曾发表过一篇《 如何把相邻图斑的属性添加在某个字段中》的个人心得,有些会员提出了进一步的要求,不但要相邻图斑的名称,还要求有方位,下面讲一下自己的做法。 基本思路是:连接相邻图斑质心,根据连线的角度确定相邻图斑…

动态规划(二)

一、线性DP 1.1数字三角形 #include<iostream> #include<algorithm>using namespace std;const int N 510,INF 1e9;int n; int a[N][N]; int f[N][N];int main() {scanf("%d",&n);for(int i 1;i < n;i ){for(int j 1;j < i; j )scanf(&qu…

【2.3】Java微服务:sentinel服务哨兵

✅作者简介&#xff1a;大家好&#xff0c;我是 Meteors., 向往着更加简洁高效的代码写法与编程方式&#xff0c;持续分享Java技术内容。 &#x1f34e;个人主页&#xff1a;Meteors.的博客 &#x1f49e;当前专栏&#xff1a; Java微服务 ✨特色专栏&#xff1a; 知识分享 &am…

Spring 事务失效的八种场景

1. 抛出检查异常导致事务不能正确回滚 Service public class Service1 {Autowiredprivate AccountMapper accountMapper;Transactionalpublic void transfer(int from, int to, int amount) throws FileNotFoundException {int fromBalance accountMapper.findBalanceBy(from…

Butterfly 安装文档(一) 快速开始

安装 在你的Hexo根目录里面 git clone -b master https://github.com/jerryc127/hexo-theme-butterfly.git themes/butterfly 应用主题 修改 Hexo 根目录下的 _config.yml&#xff0c;把主题改为 butterfly theme: butterfly 安装插件 如果你没有 pug 以及 stylus 的渲染…

字符串查找匹配算法

概述 字符串匹配&#xff08;查找&#xff09;是字符串的一种基本操作&#xff1a;给定带匹配查询的文本串S和目标子串T&#xff0c;T也叫做模式串。在文本S中找到一个和模式T相符的子字符串&#xff0c;并返回该子字符串在文本中的位置。 暴力匹配 Brute Force Algorithm&a…

20天突破英语四级高频词汇——第②天

2&#xfeff;0天突破英语四级高频词汇~第2天加油(ง •_•)ง&#x1f4aa; &#x1f433;博主&#xff1a;命运之光 &#x1f308;专栏&#xff1a;英语四级高频词汇速记 &#x1f30c;博主的其他文章&#xff1a;点击进入博主的主页 目录 2&#xfeff;0天突破英语四级高…

LouvainMethod分布式运行的升级之路

1、背景介绍 Louvain是大规模图谱的谱聚类算法&#xff0c;引入模块度的概念分二阶段进行聚类&#xff0c;直到收敛为止。分布式的代码可以在如下网址进行下载。 GitHub - Sotera/spark-distributed-louvain-modularity: Spark / graphX implementation of the distri…

SQL server 与 MySQL count函数、以及sum、avg 是否包含 为null的值

sql server 与 mysql count 作用一样。 count 计算指定字段出现的个数&#xff0c; 不是计算 null的值 获取表的条数 count(n) n:常数 count(1),count&#xff08;0&#xff09;等 count(*) count(字段) 其中字段为null 不会统计在内。 avg(字段)、sum(字段) 跟count(字段)…

数字孪生技术的实用价值体现在哪?

随着科技的不断进步&#xff0c;数字孪生技术已成为引领未来发展的重要驱动力。数字孪生是将现实世界与数字世界紧密结合的技术&#xff0c;通过创建虚拟的物理模型&#xff0c;实时模拟和分析真实世界中的物体和过程&#xff0c;让数字孪生在各个领域都展现出了巨大的潜力&…

通用Mapper的四个常见注解

四个常见注解 1、Table 作用&#xff1a;建立实体类和数据库表之间的对应关系。 默认规则&#xff1a;实体类类名首字母小写作为表名&#xff0c;如 Employee -> employee 表 用法&#xff1a;在 Table 注解的 name 属性中指定目标数据库的表名&#xff1b; 案例&#…

vscode extension 怎么区分dev prod

开发模式注入环境变量 使用vsode 提供的api

【返回时间字段问题--消息转换器】

文章目录 前言一、第一种方式一、第二种方式&#xff08;推荐 ) 前言 一、第一种方式 1). 方式一 在属性上加上注解&#xff0c;对日期进行格式化 但这种方式&#xff0c;需要在每个时间属性上都要加上该注解&#xff0c;使用较麻烦&#xff0c;不能全局处理。 一、第二种方…

【数理知识】旋转矩阵的推导过程,基于向量的旋转来实现,同时解决欧式变换的非线性局限

序号内容1【数理知识】自由度 degree of freedom 及自由度的计算方法2【数理知识】刚体 rigid body 及刚体的运动3【数理知识】刚体基本运动&#xff0c;平动&#xff0c;转动4【数理知识】向量数乘&#xff0c;内积&#xff0c;外积&#xff0c;matlab代码实现5【数理知识】协…

WGS_1984_UTM、WGS_1984_Mercator坐标转化为经纬度坐标python

1、遥感影像的PROJECTION有哪些 遥感影像常见的投影类型有很多&#xff0c;具体选择哪种投影方式取决于数据的特性和使用需求。以下列举了一些常见的遥感影像投影类型&#xff1a; UTM (Universal Transverse Mercator) 投影&#xff1a;最常见的投影类型之一&#xff0c;将地…

[原创]从强化学习的本质推导到PPO

前言 这篇博客很久之前就想做了&#xff0c;一直在拖是因为觉得自己对知识点理解还没有足够的透彻。但是每当去复盘基本概念的时候又很难理清逻辑&#xff0c;所以觉得即便现在半吊子水平&#xff0c;但是也想通过博客记录一下自己肤浅的学习心得&#xff0c;权当是为自己巩固…

ParallelCollectionRDD [0] isEmpty at KyuubiSparkUtil.scala:48问题解决

ParallelCollectionRDD [0] isEmpty at KyuubiSparkUtil.scala:48问题解决 这个问题出现在使用Kyubi Spark Util处理ParallelCollectionRDD的过程中&#xff0c;具体是在KyubiSparkUtil.scala文件的第48行调用isEmpty方法时出现的。该问题可能是由以下几个原因引起的&#xff1…