Java 并行数据处理和性能分析

并行流

并行流是一个把元素分成多个块的流,每个块用不同的线程处理。可以自动分区,让所有的处理器都忙起来。

假设要写一个方法,接受一个数量n做参数,计算1-n的和。可以这样实现:

  public long sequentialSum(long n) {
    return Stream.iterate(1L, i -> i + 1)
        .limit(n)
        .reduce(0L, Long::sum);
  }

也许可以使用parallel方法,简单地使用并行计算,提高程序性能:

  public long sequentialSum(long n) {
    return Stream.iterate(1L, i -> i + 1)
        .limit(n)
        .parallel()
        .reduce(0L, Long::sum);
  }

这样,流可能在内部被分成多个块,导致reduction操作可以在不同的块上互不依赖地并行地各自工作。最后,reduction操作组合每个子流的并行reductions的返回值,返回的结果就是整个流的结果。见下面的示意图

实际上,调用parallel方法,流自身不会有任何变化。在内部,设置一个布尔类型的标记,标明你想在并行模式执行操作,接下来的操作都是并行的。

类似地,你也可以使用sequential方法,把并行流转成串行的。你也许认为可以组合这两个方法:

    stream.parallel()
      .filter(...)
      .sequential()
      .map(...)
      .parallel()
      .reduce();

但是,最后一次调用parallel或者sequential才会全局地影响管道。上面的例子,管道将被并行地执行。

配置并行流使用的线程池

并行流内部使用ForkJoinPool。默认地,线程数量等于处理器数量(Runtime.getRuntime().availableProcessors())。但是,可以修改系统属性java.util.concurrent.ForkJoinPool.common.parallelism,配置线程数量。

这是全局配置,所以,除非你认为对性能有帮助,否则不要修改。

测量流的性能

我们声称并行加法应该比串行的或者自己的迭代方法快。我们可以使用JMH测量一下。这是一个工具,使用基于注解的方法,可以为JVM程序增加

可靠的microbenchmarks。如果使用maven,可以这样引入:

    <dependency>
      <groupId>org.openjdk.jmh</groupId>
      <artifactId>jmh-core</artifactId>
      <version>1.21</version>
    </dependency>
    <dependency>
      <groupId>org.openjdk.jmh</groupId>
      <artifactId>jmh-generator-annprocess</artifactId>
      <version>1.21</version>
    </dependency>

第一个库是核心实现,第二个包含一个注解处理器,帮助生成JAR文件,通过它可以方便地运行你的benchmark。maven配置里还应该有下面的plugin:

      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-shade-plugin</artifactId>
        <executions>
          <execution>
            <phase>package</phase>
            <goals>
              <goal>shade</goal>
            </goals>
            <configuration>
              <finalName>benchmarks</finalName>
              <transformers>
                <transformer
                    implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                  <mainClass>org.openjdk.jmh.Main</mainClass>
                </transformer>
              </transformers>
            </configuration>
          </execution>
        </executions>
      </plugin>

程序代码如下

import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Level;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.TearDown;

import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;

//测量平均时间
@BenchmarkMode(Mode.AverageTime)
//以毫秒为单位,打印benchmark结果
@OutputTimeUnit(TimeUnit.MILLISECONDS)
//执行两次,增加可靠性。堆空间是4Gb
@Fork(value = 2, jvmArgs = {"-Xms4G", "-Xmx4G"})
@State(Scope.Benchmark)
public class ParallelStreamBenchmark {
  private static final long N = 10_000_000L;

  @Benchmark
  public long sequentialSum() {
    return Stream.iterate(1L, i -> i + 1).limit(N)
        .reduce(0L, Long::sum);
  }

  //每次执行benchmark后,执行GC
  @TearDown(Level.Invocation)
  public void tearDown() {
    System.gc();
  }
}

使用大内存,和每次迭代以后试着GC都是为了尽量减少GC的影响。尽管如此,结果应该再加一些盐。很多因素会影响执行时间,比如你的机器有多少核。

默认地,JMH一般先执行5次热身迭代,这样可以让HotSpot优化代码,然后再执行5次迭代用来计算最终的结果。你可以使用-w和-i命令行参数修改这些配置。

在我的机器上,使用JDK 1.8.0_121, Java HotSpot™ 64-Bit Server VM,执行结果是

Benchmark Mode Cnt Score Error Units

ParallelStreamBenchmark.sequentialSum avgt 10 83.565 ± 1.841 ms/op

你应该期望,使用经典的for循环的迭代版本运行得更快,因为它在更低层(level)工作,而且,更重要的是,它不需要执行原始类型的装箱和拆箱操作。我们测试一下这个方法:

  @Benchmark
  public long iterativeSum() {
    long result = 0;
    for (long i = 1L; i <= N; i++) {
      result += i;
    }
    return result;
  }

执行结果是

Benchmark Mode Cnt Score Error Units

ParallelStreamBenchmark.iterativeSum avgt 10 6.877 ± 0.068 ms/op

证实了我们的期望:迭代版本比串行流快了10倍。让我们使用并行流试一试:

Benchmark Mode Cnt Score Error Units

ParallelStreamBenchmark.parallelSum avgt 10 110.157 ± 1.882 ms/op

非常令人失望:并行版本的求和一点都没有发挥多核的优势,比串行版还要慢。为什么会这样?有两个问题混在一起:

迭代生成了装箱对象,它们在做加法前,必须拆箱成数字

迭代很难划分独立的块来并行地执行

第二点是特别有趣的,不是所有的流都是适合并行处理的。特别是,迭代的流就很难,这是因为,函数的输入依赖上一个函数的结果。见下图:

这意味着,reduction过程并没有像第一张图里所表示的那样执行。reduction开始的时候,还没有整个数字列表,所以没法分块。把流标记为并行的,反而增加了在不同线程上执行的求和要被串行处理的负担。

使用更专业的方法

LongStream.rangeClosed方法使用的是原始long类型,所以不用装箱和拆箱。而且,它生产的数的范围,可以很容易地分成不依赖的块。比如,范围1-20可以被分成1-5、6-10、11-15和16-20。

  @Benchmark
  public long rangedSum() {
    return LongStream.rangeClosed(1, N)
        .reduce(0L, Long::sum);
  }

输出是

Benchmark Mode Cnt Score Error Units

ParallelStreamBenchmark.rangedSum avgt 10 7.660 ± 1.643 ms/op

可以看出来,比并行流快了很多,仅比经典的for循环慢了一点。LongStream支持并行:

  @Benchmark
  public long parallelRangedSum() {
    return LongStream.rangeClosed(1, N)
        .parallel()
        .reduce(0L, Long::sum);
  }

输出是

Benchmark Mode Cnt Score Error Units

ParallelStreamBenchmark.parallelRangedSum avgt 10 4.790 ± 5.142 ms/op

可以发现,并行生效了。甚至比for循环还快了1/3。

正确使用并行流

滥用并行流产生错误的主要原因是使用了改变共享状态的算法。下面是一个通过改变共享的累加器来实现前n个自然数求和的例子:

  public long sideEffectSum(long n) {
    Accumulator accumulator = new Accumulator();
    LongStream.rangeClosed(1, n).forEach(accumulator::add);
    return accumulator.total;
  }

  public class Accumulator {
    public long total = 0;
    public void add(long value) {
      total += value;
    }
  }

这种代码很常见,特别对熟悉命令式编程范式的开发者而言。当你迭代数字列表时,经常这样做:初始化一个累加器,遍历元素,使用累加器相加。

这代码有什么错?它是串行的,失去了并行性。让我们试着使用并行流:

  public long sideEffectParallelSum(long n) {
    Accumulator accumulator = new Accumulator();
    LongStream.rangeClosed(1, n).parallel().forEach(accumulator::add);
    return accumulator.total;
  }

多执行几次,你会发现,每次返回的结果都不一样,而且都不是正确的50000005000000。这是因为多线程累加的时候,total += value并不是原子操作。那么怎样才能写出并行情况下,正确的代码呢?

如果有怀疑,就做测试

注意装箱问题。Java提供的原始类型流(IntStream、LongStream和DoubleStream)可以避免类似的问题,尽量使用他们

有些操作使用并行流性能更差。尤其是像limit和findFirst这种依赖元素顺序的操作,使用并行是非常昂贵的。比如,findAny就比findFirst性能好,因为它跟顺序无关。调用unordered方法,可以把一个有顺序的流变成无顺序的流。比如,如果你需要流的N个元素,而你对前M个感兴趣,在一个无顺序的流上调用limit比有顺序的高效

如果数据量不大,不要选择并行流

要考虑流的底层数据结构的可分解程度。比如,ArrayList比LinkedList分解起来更高效,因为不遍历就可以分割。使用range工厂增加的原始类型流也很容易分割。可以通过实现自己的Spliterator分割流

流的特征,以及中间操作如何修改流的元素,会改变分解过程的性能。比如,一个SIZED流可以被分解成两个相等的部分,并且每个部分可以高效得并行处理,但是,filter会过滤掉任何不满足条件的元素,导致流的size成了未知的

考虑结束操作是廉价的还是昂贵的merge步骤(比如,Collector的combiner方法)。如果是昂贵的,组合并行结果的代价会比并行流带来的好处还要高

下面的表格,总结一些流在可分解性方面的并行友好性

可分解性
ArrayList 优秀
LinkedList
IntStream.range 优秀
Stream.iterate
HashSet
TreeSet

fork/join框架

fork/join框架用来递归地把可并行的任务分解成小任务,然后组合每个子任务的结果,以生成总的结果。它实现了ExecutorService接口,这样所有的子任务都在一个线程池(ForkJoinPool)内工作。

RecursiveTask

要向ForkJoinPool提交任务,你不得不增加RecursiveTask的子类-R是并行任务(以及每个子任务)的返回类型,或者

增加RecursiveAction的子类-当没有返回值的时候。要定义RecursiveTask,需要实现它唯一的抽象方法:

protected abstract R compute();

该方法定义分割任务和不能继续被分割时处理一个子任务的算法的逻辑。该方法的实现,经常像下面的伪代码:

if (任务足够小,不再被分) {
  顺序执行任务
} else {
  把任务分成两个子任务
  递归地调用本方法,尽量分割每个子任务
  等待所有子任务的完成
  组合每个子任务的结果
}

可以发现,这是分治算法的并行实现。我们继续求和的例子,演示怎么使用fork/join框架。首先需要扩展RecursiveTask类:

import java.util.concurrent.RecursiveTask;

/**
 * Created by leishu on 18-12-11.
 */
public class ForkJoinSumCalculator extends RecursiveTask<Long> {
  //分割任务的阈值
  public static final long THRESHOLD = 10_000;
  //要被求和的数组
  private final long[] numbers;
  private final int start;
  private final int end;

  public ForkJoinSumCalculator(long[] numbers) {
    this(numbers, 0, numbers.length);
  }
  //生成子任务的私有构造器
  private ForkJoinSumCalculator(long[] numbers, int start, int end) {
    this.numbers = numbers;
    this.start = start;
    this.end = end;
  }

  @Override
  protected Long compute() {
    //子任务的大小
    int length = end - start;
    if (length <= THRESHOLD) {
      return computeSequentially();//小于阈值,不分割
    }
    //增加第一个子任务
    ForkJoinSumCalculator leftTask = new ForkJoinSumCalculator(numbers, start, start + length / 2);
    //异步执行,新的子任务使用ForkJoinPool的另一个线程
    leftTask.fork();
    ForkJoinSumCalculator rightTask = new ForkJoinSumCalculator(numbers, start + length / 2, end);
    //同步执行第二个子任务,允许递归
    Long rightResult = rightTask.compute();
    //读取第一个子任务的结果,如果没完成就等待
    Long leftResult = leftTask.join();
    //组合
    return leftResult + rightResult;
  }

  //顺序执行
  private long computeSequentially() {
    long sum = 0;
    for (int i = start; i < end; i++) {
      sum += numbers[i];
    }
    return sum;
  }
}

使用fork/join的最佳实践

调用任务的join方法,会阻塞调用者,直到返回结果。所以,要在两个子任务都启动以后在调用它

不要在RecursiveTask内使用ForkJoinPool的invoke方法

子任务的fork方法是用来做调度的。在两个子任务上直接调用它似乎是很自然的,但是,在其中一个上调用compute效率更高,因为这样能重用相同的线程

偷工作

任务被分给ForkJoinPool里的线程。每个线程有一个保存任务的双端链表,顺序地执行链表中的任务。如果由于某种原因(比如I/O),一个线程完成了分配给他的全部任务,它会随机地从其他线程选择一个队列,从队列的尾部偷一个任务。这个过程会持续,直到所有的队列都空了为止。所以,要有大量的小任务,而不是几个大任务,这样可以更好地平衡线程的负荷。

Spliterator

Spliterator是Java 8 提供的新接口,意思是“splitable iterator”,用来并行地迭代源中的元素。也许你不用开发自己的Spliterator,但是,理解了它,也就明白了并行流是如何工作的。Java 8已经在Collections框架内提供了Spliterator的默认实现。Collection接口有一个default方法spliterator(),它就返回一个Spliterator对象。我们先看看Spliterator接口的定义:

public interface Spliterator<T> {
  //用来按顺序消费Spliterator的元素,如果还有元素就返回true
  boolean tryAdvance(Consumer<? super T> action);
  //把一些元素分到一个新的Spliterator,以允许他们并行处理
  Spliterator<T> trySplit();
  //剩余的可被遍历的元素数量估值
  long estimateSize();
  int characteristics();
}

tryAdvance方法的行为类似于迭代器,用来按顺序消费Spliterator的元素,如果还有元素就返回true。trySplit方法

用来把一些元素分到一个新的Spliterator,以允许他们并行处理。

分割过程

把一个流分割成多个部分是一个递归过程,如下图所示。首先,在第一个Spliterator上调用trySplit生成一个新的。然后,在这两个Spliterator上调用trySplit,这样产生四个。一直进行下去,直到该方法返回null,标志着不能再被分割。最后,当所有的trySplit都返回null时,递归过程结束。

分割过程也会受到Spliterator的特征(由characteristics方法声明)的影响。

Spliterator特征

characteristics方法返回一个整数,用来更好地控制和优化Spliterator的用法。

Characteristic 描述
ORDERED 元素是有顺序的(比如List),所以Spliterator使用该顺序做遍历和分区
DISTINCT 对于每对遍历的元素x和y,x.equals(y)返回false
SORTED 遍历的元素遵循预定义的排序顺序
SIZED 源的size是已知的(比如set),所以estimatedSize()返回的值是精确的
NON-NULL 元素不会为空
IMMUTABLE 源是不可变的,说明遍历的时候,元素不会被增加、修改和删除
CONCURRENT 源是并发安全的,并发修改的时候,不用任何同步
SUBSIZED Spliterator和接下来产生的Spliterator都是SIZED

实现自己的Spliterator

我们开发一个简单的方法,用来计算字符串中的单词数。

  public int countWordsIteratively(String s) {
    int counter = 0;
    boolean lastSpace = true;
    for (char c : s.toCharArray()) {
      if (Character.isWhitespace(c)) {
        lastSpace = true;
      } else {
        if (lastSpace) counter++;
        lastSpace = false;
      }
    }
    return counter;
  }

要计算的字符串是但丁的“地域”的第一句

    public static final String SENTENCE =
        " Nel  mezzo del cammin di nostra vita "
            + "mi ritrovai in una selva oscura"
            + " che la dritta via era  smarrita ";

    System.out.println("Found " + countWordsIteratively(SENTENCE) + " words");

注意,两个单词间的空格数是随机的。执行结果

Found 19 words

使用函数式实现

首先需要把字符串转换成一个流。原始类型int、long和double才有原始的的流,所以,我们使用Stream:

Stream<Character> stream = IntStream.range(0, SENTENCE.length())

.mapToObj(SENTENCE::charAt);

可以使用reduction计算单词数量。当reduce的时候,你不得不携带由两个变量组成的状态:整数型的总数和布尔型的字符是否是空格。因为Java没有tuples,你得增加一个新类-WordCounter-封装状态:

  class WordCounter {
    private final int counter;
    private final boolean lastSpace;

    public WordCounter(int counter, boolean lastSpace) {
      this.counter = counter;
      this.lastSpace = lastSpace;
    }

    //遍历,累加
    public WordCounter accumulate(Character c) {
      if (Character.isWhitespace(c)) {
        return lastSpace ? this : new WordCounter(counter, true);
      } else {
        //如果上一个字符是空格,而当前的不是,就加1
        return lastSpace ? new WordCounter(counter + 1, false) : this;
      }
    }

    //组合,求和
    public WordCounter combine(WordCounter wordCounter) {
      return new WordCounter(counter + wordCounter.counter, wordCounter.lastSpace);
    }

    public int getCounter() {
      return counter;
    }
  }

下面是遍历一个新字符时,WordCounter的状态图

然后,我们就可以使用流的reduce方法了

  private int countWords(Stream<Character> stream) {
    WordCounter wordCounter = stream.reduce(new WordCounter(0, true),
        WordCounter::accumulate,
        WordCounter::combine);
    return wordCounter.getCounter();
  }

我们做一下测试

    Stream<Character> stream = IntStream.range(0, SENTENCE.length())
        .mapToObj(SENTENCE::charAt);
    System.out.println("Found " + countWords(stream) + " words");

执行结果是正确的。

并行的实现

我们修改一下代码

System.out.println("Found " + countWords(stream.parallel()) + " words");

执行结果不是找到19个单词了。因为源字符串在随意的位置被分割,一个字符被多次分割。要解决这个问题,就需要实现自己的Spliterator。

  class WordCounterSpliterator implements Spliterator<Character> {

    private final String string;
    private int currentChar = 0;

    private WordCounterSpliterator(String string) {
      this.string = string;
    }

    @Override
    public boolean tryAdvance(Consumer<? super Character> action) {
      //消费当前字符
      action.accept(string.charAt(currentChar++));
      //如果还有字符可被消费,返回true
      return currentChar < string.length();
    }

    @Override
    public Spliterator<Character> trySplit() {
      int currentSize = string.length() - currentChar;
      //小于阈值,不再分割
      if (currentSize < 10) {
        return null;
      }
      //候选的分割位置是字符串的一半长度
      for (int splitPos = currentSize / 2 + currentChar; splitPos < string.length(); splitPos++) {
        //如果是空格,才分割
        if (Character.isWhitespace(string.charAt(splitPos))) {
          Spliterator<Character> spliterator = new WordCounterSpliterator(string.substring(currentChar, splitPos));
          //当前位置修改为分割位置
          currentChar = splitPos;
          return spliterator;
        }
      }
      return null;
    }

    @Override
    public long estimateSize() {
      return string.length() - currentChar;
    }

    @Override
    public int characteristics() {
      return ORDERED + SIZED + SUBSIZED + NONNULL + IMMUTABLE;
    }
  }

然后,我们做测试

    Spliterator<Character> spliterator = new WordCounterSpliterator(SENTENCE);
    Stream<Character> stream = StreamSupport.stream(spliterator, true);

    System.out.println("Found " + countWords(stream) + " words");

这回没问题了。

以上这篇Java 并行数据处理和性能分析就是小编分享给大家的全部内容了,希望能给大家一个参考,也希望大家多多支持我们。

(0)

相关推荐

  • 浅谈在Java中使用Callable、Future进行并行编程

    使用Callable.Future进行并行编程 在Java中进行并行编程最常用的方式是继承Thread类或者实现Runnable接口.这两种方式的缺点是在任务完成后无法直接获取执行结果,必须通过共享变量或线程间通信,使用起来很不方便. 从Java1.5开始提供了Callable和Future两个接口,通过使用它们可以在任务执行完毕后得到执行结果. 下面我们来学习下如何使用Callable.Future和FutureTask. Callable接口 Callable接口位于java.util.co

  • Java通过Fork/Join优化并行计算

    本文实例为大家分享了Java通过Fork/Join优化并行计算的具体代码,供大家参考,具体内容如下 Java代码: package Threads; import java.util.concurrent.ForkJoinPool; import java.util.concurrent.RecursiveAction; /** * Created by Frank */ public class RecursiveActionDemo extends RecursiveAction { sta

  • Java大文本并行计算实现过程解析

    简单提高文本读取效率,使用BufferedReader是个不错的选择.速度最快的方法是MappedByteBuffer,但是,相比BufferedReader而言,效果不是非常明显.也就是说,后者虽然快,但也快的有限(不要抱有性能提升几倍的幻想). 对于大文本的读取,性能瓶颈主要在IO,read占时间多是正常的,硬盘本身就不快,读入内存后还要转成对象,都比较耗时间. 想要提速应当用并行的办法,用多线程同时读取和处理数据,但Java写多线程程序很麻烦,并行分段读同一个文件时还要考虑调整边界,也比较

  • 浅谈Java Fork/Join并行框架

    初步了解Fork/Join框架 Fork/Join 框架是java7中加入的一个并行任务框架,可以将任务分割成足够小的小任务,然后让不同的线程来做这些分割出来的小事情,然后完成之后再进行join,将小任务的结果组装成大任务的结果.下面的图片展示了这种框架的工作模型: 使用Fork/Join并行框架的前提是我们的任务可以拆分成足够小的任务,而且可以根据小任务的结果来组装出大任务的结果,一个最简单的例子是使用Fork/Join框架来求一个数组中的最大/最小值,这个任务就可以拆成很多小任务,大任务就是

  • Java8并行流中自定义线程池操作示例

    本文实例讲述了Java8并行流中自定义线程池操作.分享给大家供大家参考,具体如下: 1.概览 java8引入了流的概念,流是作为一种对数据执行大量操作的有效方式.并行流可以被包含于支持并发的环境中.这些流可以提高执行性能-以牺牲多线程的开销为代价 在这篇短文中,我们将看一下 Stream API的最大限制,同时看一下如何让并行流和线程池实例(ThreadPool instance)一起工作. 2.并行流Parallel Stream 我们先以一个简单的例子来开始-在任一个Collection类型

  • Java中Lambda表达式并行与组合行为

    从串行到并行 串行指一个步骤一个步骤地处理,也就是通常情况下,代码一行一行地执行. 如果将我们常用的迭代器式的循环展开的话,就是串行执行了循环体内所定义的操作: sum += arr.get(0); sum += arr.get(1); sum += arr.get(2); //... 在书的一开始,就提到Java需要支持集合的并行计算(而Lambda为这个需求提供了可能). 这些功能将全部被实现于库代码中,对于我们使用者,实现并行的复杂性被大大降低(最低程度上只需要调用相关方法). 另外,关于

  • java利用CountDownLatch实现并行计算

    本文实例为大家分享了利用CountDownLatch实现并行计算的具体代码,供大家参考,具体内容如下 import java.util.concurrent.CountDownLatch; /** * @Author pipi * @Date 2018/10/15 13:56 **/ public class ParallelComputing { private int[] nums; private String[] info; private CountDownLatch countDow

  • Java 并行数据处理和性能分析

    并行流 并行流是一个把元素分成多个块的流,每个块用不同的线程处理.可以自动分区,让所有的处理器都忙起来. 假设要写一个方法,接受一个数量n做参数,计算1-n的和.可以这样实现: public long sequentialSum(long n) { return Stream.iterate(1L, i -> i + 1) .limit(n) .reduce(0L, Long::sum); } 也许可以使用parallel方法,简单地使用并行计算,提高程序性能: public long sequ

  • java字符串拼接与性能分析详解

    假设有一个字符串,我们将对这个字符串做大量循环拼接操作,使用"+"的话将得到最低的性能.但是究竟这个性能有多差?如果我们同时也把StringBuffer,StringBuilder或String.concat()放入性能测试中,结果又会如何呢?本文将会就这些问题给出一个答案! 我们将使用Per4j来计算性能,因为这个工具可以给我们一个完整的性能指标集合,比如最小,最大耗时,统计时间段的标准偏差等.在测试代码中,为了得到一个准确的标准偏差值,我们将执行20个拼接"*"

  • java 开发使用字符串和数字的性能分析

    java 开发使用字符串和数字的性能分析 前言: 分析使用字符串和数字,在软件产品上线后用户较多的情况下,很有必要考虑的问题,这直接体现了用户的体验程度,总不能让用户用着很卡的感觉吧! 在我多年的开发经验中,经常发现的一个情况就是,很多项目的对象字段或者是数据库字段本来是数字类型的,却被定义成字符串类型,这无关痛痒吗? 对于小项目来说,可能没什么影响,反正只要业务逻辑正确即可,性能没什么问题,因为数据也不多,用户也不多. 然而,对于大数据处理来说,这个可不是小事,从字符串替换为数字类型,可以极大

  • java 单例的五种实现方式及其性能分析

    java 单例的五种实现方式及其性能分析 序言 在23种设计模式中,单例是最简单的设计模式,但是也是很常用的设计模式.从单例的五种实现方式中我们可以看到程序员对性能的不懈追求.下面我将分析单例的五种实现方式的优缺点,并对其在多线程环境下的性能进行测试. 实现 单例模式适用于资源占用较多的类,保证一个类只有一个实例即单例.通用的做法就是构造器私有化,提供一个全局的访问点,返回类的实例. uml图: 1.饿汉式 代码实现: package com.zgh.gof23.singleton; /** *

  • Java CPU性能分析工具代码实例

    这篇文章主要介绍了Java CPU性能分析工具代码实例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 背景 有处理过生产问题的同学基本都能遇到系统忽然缓慢,CPU突然飙升,甚至整个应用请求不可用.当出现这种情况下,在不影响数据准确性的前提下,我们应该尽快导出jstack和内存信息,然后重启系统,尽快回复系统的可用性,避免用户体验过差.本文针对CPU飙升问题,提供该问题的排查思路,从而能够快速定位到某线程甚至某快代码导致CPU飙升,从而提供处理该

  • java中HashMap的7种遍历方式与性能分析

    目录 1.遍历方式 1.1 迭代器 EntrySet 1.2 迭代器 KeySet 1.3 ForEach EntrySet 1.4 ForEach KeySet 1.5 Lambda 表达式 1.6 Stream API 单线程 1.7 Stream API 多线程 1.8 代码汇总 2.性能分析 2.1 引入依赖 2.2 编写测试类 2.3 测试结果 2.4 分析 2.5 总结 1.遍历方式 1.1 迭代器 EntrySet /** * 1. 迭代器 EntrySet */ @Test pu

  • java性能分析jconsole详解

    目录 jconsole简介 jconsole远程 前言: 本章节继续学习java性能优化的相关知识.重点学习什么是jconsole,以及如何使用?它能帮助我们做什么? jconsole简介 提供JVM图形化视图,包括内存.线程.类.cpu等信息.用户可以通过jconsole工具去连接指定的jvm,监控jvm的变化. 我们可以在jdk的安装文件bin当中找到它: 双击运行会打开如下界面,上面是本地的java进程,下面是通过远程的方式连接服务器上面的java进程. 我们随便点击一个本地进程得到如下的

  • java 字符串内存分配的分析与总结(推荐)

    经常在网上各大版块都能看到对于java字符串运行时内存分配的探讨,形如:String a = "123",String b = new String("123"),这两种形式的字符串是存放在什么地方的呢,其实这两种形式的字符串字面值"123"本身在运行时既不是存放在栈上,也不是存放在堆上,他们是存放在方法区中的某个常量区,并且对于相同的字符串字面值在内存中只保留一份.下面我们将以实例来分析. 1.==运算符作用在两个字符串引用比较的两个案例: p

  • java 中锁的性能提高办法

    java 中锁的性能提高办法 我们努力为自己的产品所遇到的问题思考解决办法,但在这篇文章中我将给大家分享几种常用的技术,包括分离锁.并行数据结构.保护数据而非代码.缩小锁的作用范围,这几种技术可以使我们不使用任何工具来检测死锁. 锁不是问题的根源,锁之间的竞争才是 通常在多线程的代码中遇到性能方面的问题时,一般都会抱怨是锁的问题.毕竟锁会降低程序的运行速度和其较低的扩展性是众所周知的.因此,如果带着这种"常识"开始优化代码,其结果很有可能是在之后会出现讨人厌的并发问题. 因此,明白竞争

  • 详解lambda表达式foreach性能分析

    java 8的新特性之一就是lambda表达式,parallelStream()都说性能会比较高,现一探究竟. 话不多说,上代码: @Test public void test2(){ List<String> list = new ArrayList<>(); for(int i=0;i<10000;i++) list.add(String.valueOf(i)); //lambda表达式 long start = System.currentTimeMillis(); /

随机推荐