使用Java8新特性parallelStream遇到的坑

简介: 使用Java8新特性parallelStream遇到的坑

1 问题测试代码

public static void main(String[] args) {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        List<Calendar> list = new ArrayList<>();
        for (int i = 0; i < 20; i++) {
              Calendar startDay = new GregorianCalendar();
              Calendar checkDay = new GregorianCalendar();
              checkDay.setTime(startDay.getTime());//不污染入参
              checkDay.add(checkDay.DATE,i);
              list.add(checkDay);
              checkDay = null;
              startDay = null;
        }
        list.stream().forEach(day ->  System.out.println(sdf.format(day.getTime())));
        System.out.println("-----------------------");
        list.parallelStream().forEach(day ->  System.out.println(sdf.format(day.getTime())));
        System.out.println("-----------------------");
  }


说明:


(1) 使用stream().forEach(),就是单纯的串行遍历循环,和使用for循环得到的效果一样,只是这种方式可以使代码更精简;


(2) 使用parallelStream().forEach(),是并行遍历循环,相当于是使用了多线程处理.这样可以在一定程度上提高执行效率.而程序在运行过程中具体会使用多少个线程进行处理,系统会根据运行服务器的资源占用情况自动进行分配.


2 运行结果


网络异常,图片无法展示
|


image.png


3 原因排查


网上搜索查询搜索到相关的文章如下:


<<JAVA使用并行流(ParallelStream)时要注意的一些问题>>,

<<java8的ParallelStream踩坑记录>>.


这些文章中描述的问题归根结底都是同一类问题,那就是在使用


parallelStream().forEach()时,都操作了线程不安全的对象(ArrayList).


查看ArrayList的源码如下:

transient Object[] elementData; // non-private to simplify nested  class access   
   /**
     * Appends the specified element to the end of this list.
     * @param e element to be appended to this list
     * @return <tt>true</tt> (as specified by {@link Collection#add})
     */
    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }


通过查看源码可以看到,ArrayList本身底层是通过一个名为elementData的数组实现的,而add()方法上并没有加同步锁,可见在多线程并发情况下存在线程不安全的问题.


这些文章最后的解决方案都是将操作ArrayList转化为一个同步的集合:

Collections.synchronizedList(new ArrayList<>())


这样并行流操作同一个ArrayList的对象中add()方法时,就都是同步串行操作的了,就不存在线程安全的问题了,也即是解决了文章中反馈的问题.


那么出问题的原因就找到了,那就是在使用parallelStream().forEach()时,都操作了线程不安全的对象.


4 结合自己的问题


上面找到的出问题的原因,就是在parallelStream().forEach()中使用了线程不安全的对象.

SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
...
list.parallelStream().forEach(**day** ->  System.out.println(sdf.format(day.getTime())));


如上面代码所示,从list中遍历的day和day.getTime()肯定不会有线程安全问题.那么就只剩下SimpleDateFormat实例对象了.下面咱查看SimpleDateFormat对象的format()源码深挖得到如下信息:

public abstract class DateFormat extends Format {
    /**
     * The {@link Calendar} instance used for calculating the date-time  fields
     * and the instant of time. This field is used for both formatting  and
     * parsing.
     *
     * <p>Subclasses should initialize this field to a {@link Calendar}
     * appropriate for the {@link Locale} associated with this
     * <code>DateFormat</code>.
     * @serial
     */
    protected Calendar calendar;   
    ...
 // Called from Format after creating a FieldDelegate
    private StringBuffer format(Date date, StringBuffer toAppendTo,
                                FieldDelegate delegate) {
        // Convert input date to time field list
        calendar.setTime(date);
        boolean useDateFormatSymbols = useDateFormatSymbols();
        for (int i = 0; i < compiledPattern.length; ) {
            int tag = compiledPattern[i] >>> 8;
            int count = compiledPattern[i++] & 0xff;
            if (count == 255) {
                count = compiledPattern[i++] << 16;
                count |= compiledPattern[i++];
            }
            switch (tag) {
            case TAG_QUOTE_ASCII_CHAR:
                toAppendTo.append((char)count);
                break;
            case TAG_QUOTE_CHARS:
                toAppendTo.append(compiledPattern, i, count);
                i += count;
                break;
            default:
                subFormat(tag, count, delegate, toAppendTo,  useDateFormatSymbols);
                break;
            }
        }
        return toAppendTo;
    }


format()方法中操作了一个成员变量calendar,且该方法上未加同步锁,说明该方法在多线程并发访问时,存在线程安全问题.这就是上面测试代码中出现重复数据的根本原因.


进一步查询得知,Java8以前的老版本中的日期和时间类全部都是线程不安全的,而在Java8新推出的日期类LocalDate和LocalDateTime非常友好的解决了上述问题.


5 针对测试代码中问题的根本解决之道


弃用Java8之前旧版本中的日期和时间类,改用新版本中的时间类.新修改后的代码如下:

public static void main1(String[] args) {
            DateTimeFormatter fmt =  DateTimeFormatter.ofPattern("yyyy-MM-dd");
            LocalDate date = LocalDate.now();
            List<LocalDate> list = new ArrayList<>();
            for (int i = 0; i < 20; i++) {
                  LocalDate date1 = date.plusDays(i);
                  list.add(date1);
            }
            list.stream().forEach(day ->  System.out.println(day.format(fmt)));
            System.out.println("-----------------------");
            list.parallelStream().forEach(day ->  System.out.println(day.format(fmt)));
      }     
      public static void main2(String[] args) {
            DateTimeFormatter fmt =  DateTimeFormatter.ofPattern("yyyy-MM-dd");
            LocalDateTime date = LocalDateTime.now();
            List<LocalDateTime> list = new ArrayList<>();
            for (int i = 0; i < 20; i++) {
                  LocalDateTime date1 = date.plusDays(i);
                  list.add(date1);
            }
            list.stream().forEach(day ->  System.out.println(day.format(fmt)));
            System.out.println("-----------------------");
            list.parallelStream().forEach(day ->  System.out.println(day.format(fmt)));
      }


看一下LocalDate和LocalDateTime的源码:通过查看源码,可以看到LocalDate和LocalDateTime类都是不可变和线程安全的.这样的下面的代码中的day每一次都是不同的对象

list.parallelStream().forEach(day ->  System.out.println(day.format(fmt)));


再来对比最初问题代码:并行操作时,在使用同一个sdf实例.

SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
...
list.parallelStream().forEach(day ->  System.out.println(sdf.format(day.getTime())));


LocalDate类源码:

* @implSpec
* This class is immutable and thread-safe.
* @since 1.8
*/
public **final** class LocalDate
        implements Temporal, TemporalAdjuster, ChronoLocalDate,  Serializable {
...


LocalDateTime类源码:

* @implSpec
* This class is immutable and thread-safe.
*
* @since 1.8
*/
public final class LocalDateTime
        implements Temporal, TemporalAdjuster,  ChronoLocalDateTime<LocalDate>, Serializable {


至此,测试代码中出问题的根本原因找到,根本解决之道找到.OK!

相关文章
|
28天前
|
存储 安全 Java
Java Map新玩法:探索HashMap和TreeMap的高级特性,让你的代码更强大!
【10月更文挑战第17天】Java Map新玩法:探索HashMap和TreeMap的高级特性,让你的代码更强大!
57 2
|
29天前
|
存储 Java
深入探讨了Java集合框架中的HashSet和TreeSet,解析了两者在元素存储上的无序与有序特性。
【10月更文挑战第16天】本文深入探讨了Java集合框架中的HashSet和TreeSet,解析了两者在元素存储上的无序与有序特性。HashSet基于哈希表实现,添加元素时根据哈希值分布,遍历时顺序不可预测;而TreeSet利用红黑树结构,按自然顺序或自定义顺序存储元素,确保遍历时有序输出。文章还提供了示例代码,帮助读者更好地理解这两种集合类型的使用场景和内部机制。
38 3
|
29天前
|
存储 Java 数据处理
Java Set接口凭借其独特的“不重复”特性,在集合框架中占据重要地位
【10月更文挑战第16天】Java Set接口凭借其独特的“不重复”特性,在集合框架中占据重要地位。本文通过快速去重和高效查找两个案例,展示了Set如何简化数据处理流程,提升代码效率。使用HashSet可轻松实现数据去重,而contains方法则提供了快速查找的功能,彰显了Set在处理大量数据时的优势。
32 2
|
1月前
|
存储 算法 Java
Java Set因其“无重复”特性在集合框架中独树一帜
【10月更文挑战第14天】Java Set因其“无重复”特性在集合框架中独树一帜。本文深入解析Set接口及其主要实现类(如HashSet、TreeSet)如何通过特定的数据结构(哈希表、红黑树)确保元素唯一性,并提供最佳实践建议,包括选择合适的Set实现类和正确实现自定义对象的`hashCode()`与`equals()`方法。
29 3
|
11天前
|
分布式计算 Java API
Java 8引入了流处理和函数式编程两大新特性
Java 8引入了流处理和函数式编程两大新特性。流处理提供了一种声明式的数据处理方式,使代码更简洁易读;函数式编程通过Lambda表达式和函数式接口,简化了代码书写,提高了灵活性。此外,Java 8还引入了Optional类、新的日期时间API等,进一步增强了编程能力。这些新特性使开发者能够编写更高效、更清晰的代码。
25 4
|
25天前
|
存储 Java API
优雅地使用Java Map,通过掌握其高级特性和技巧,让代码更简洁。
【10月更文挑战第19天】本文介绍了如何优雅地使用Java Map,通过掌握其高级特性和技巧,让代码更简洁。内容包括Map的初始化、使用Stream API处理Map、利用merge方法、使用ComputeIfAbsent和ComputeIfPresent,以及Map的默认方法。这些技巧不仅提高了代码的可读性和维护性,还提升了开发效率。
50 3
|
25天前
|
存储 安全 Java
Java Map新玩法:深入探讨HashMap和TreeMap的高级特性
【10月更文挑战第19天】Java Map新玩法:深入探讨HashMap和TreeMap的高级特性,包括初始容量与加载因子的优化、高效的遍历方法、线程安全性处理以及TreeMap的自然排序、自定义排序、范围查询等功能,助你提升代码性能与灵活性。
24 2
|
29天前
|
Java 开发者
在Java集合世界中,Set以其独特的特性脱颖而出,专门应对重复元素
在Java集合世界中,Set以其独特的特性脱颖而出,专门应对重复元素。通过哈希表和红黑树两种模式,Set能够高效地识别并拒绝重复元素的入侵,确保集合的纯净。无论是HashSet还是TreeSet,都能在不同的场景下发挥出色的表现,成为开发者手中的利器。
26 2
|
29天前
|
Java
Java Set以其“不重复”的特性,为我们提供了一个高效、简洁的处理唯一性约束数据的方式。
【10月更文挑战第16天】在Java编程中,Set接口确保集合中没有重复元素,每个元素都是独一无二的。HashSet基于哈希表实现,提供高效的添加、删除和查找操作;TreeSet则基于红黑树实现,不仅去重还能自动排序。通过这两个实现类,我们可以轻松处理需要唯一性约束的数据,提升代码质量和效率。
35 2
|
1月前
|
存储 Java 数据处理
在Java集合框架中,Set接口以其独特的“不重复”特性脱颖而出
【10月更文挑战第14天】在Java集合框架中,Set接口以其独特的“不重复”特性脱颖而出。本文通过两个案例展示了Set的实用性和高效性:快速去重和高效查找。通过将列表转换为HashSet,可以轻松实现去重;而Set的contains方法则提供了快速的元素查找功能。这些特性使Set成为处理大量数据时的利器。
16 4