使用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());//不污染入?yún)?              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(),就是單純的串行遍歷循環(huán),和使用for循環(huán)得到的效果一樣,只是這種方式可以使代碼更精簡;

(2) 使用parallelStream().forEach(),是并行遍歷循環(huán),相當(dāng)于是使用了多線程處理.這樣可以在一定程度上提高執(zhí)行效率.而程序在運(yùn)行過程中具體會(huì)使用多少個(gè)線程進(jìn)行處理,系統(tǒng)會(huì)根據(jù)運(yùn)行服務(wù)器的資源占用情況自動(dòng)進(jìn)行分配.

2 運(yùn)行結(jié)果

image.png

3 原因排查

網(wǎng)上搜索查詢搜索到相關(guān)的文章如下:
<<JAVA使用并行流(ParallelStream)時(shí)要注意的一些問題>>,
<<java8的ParallelStream踩坑記錄>>.

這些文章中描述的問題歸根結(jié)底都是同一類問題,那就是在使用parallelStream().forEach()時(shí),都操作了線程不安全的對象(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本身底層是通過一個(gè)名為elementData的數(shù)組實(shí)現(xiàn)的,而add()方法上并沒有加同步鎖,可見在多線程并發(fā)情況下存在線程不安全的問題.

這些文章最后的解決方案都是將操作ArrayList轉(zhuǎn)化為一個(gè)同步的集合:

Collections.synchronizedList(new ArrayList<>())

這樣并行流操作同一個(gè)ArrayList的對象中add()方法時(shí),就都是同步串行操作的了,就不存在線程安全的問題了,也即是解決了文章中反饋的問題.

那么出問題的原因就找到了,那就是在使用parallelStream().forEach()時(shí),都操作了線程不安全的對象.

4 結(jié)合自己的問題

上面找到的出問題的原因,就是在parallelStream().forEach()中使用了線程不安全的對象.

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

如上面代碼所示,從list中遍歷的day和day.getTime()肯定不會(huì)有線程安全問題.那么就只剩下SimpleDateFormat實(shí)例對象了.下面咱查看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()方法中操作了一個(gè)成員變量calendar,且該方法上未加同步鎖,說明該方法在多線程并發(fā)訪問時(shí),存在線程安全問題.這就是上面測試代碼中出現(xiàn)重復(fù)數(shù)據(jù)的根本原因.

進(jìn)一步查詢得知,Java8以前的老版本中的日期和時(shí)間類全部都是線程不安全的,而在Java8新推出的日期類LocalDate和LocalDateTime非常友好的解決了上述問題.

5 針對測試代碼中問題的根本解決之道

棄用Java8之前舊版本中的日期和時(shí)間類,改用新版本中的時(shí)間類.新修改后的代碼如下:

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

再來對比最初問題代碼:并行操作時(shí),在使用同一個(gè)sdf實(shí)例.

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!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末趁猴,一起剝皮案震驚了整個(gè)濱河市揭蜒,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌萌腿,老刑警劉巖,帶你破解...
    沈念sama閱讀 223,002評論 6 519
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件抖苦,死亡現(xiàn)場離奇詭異毁菱,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)锌历,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,357評論 3 400
  • 文/潘曉璐 我一進(jìn)店門贮庞,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人辩涝,你說我怎么就攤上這事贸伐。” “怎么了怔揩?”我有些...
    開封第一講書人閱讀 169,787評論 0 365
  • 文/不壞的土叔 我叫張陵捉邢,是天一觀的道長。 經(jīng)常有香客問我商膊,道長伏伐,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 60,237評論 1 300
  • 正文 為了忘掉前任晕拆,我火速辦了婚禮藐翎,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘实幕。我一直安慰自己吝镣,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,237評論 6 398
  • 文/花漫 我一把揭開白布昆庇。 她就那樣靜靜地躺著末贾,像睡著了一般。 火紅的嫁衣襯著肌膚如雪整吆。 梳的紋絲不亂的頭發(fā)上拱撵,一...
    開封第一講書人閱讀 52,821評論 1 314
  • 那天辉川,我揣著相機(jī)與錄音,去河邊找鬼拴测。 笑死乓旗,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的集索。 我是一名探鬼主播屿愚,決...
    沈念sama閱讀 41,236評論 3 424
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼抄谐!你這毒婦竟也來了渺鹦?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 40,196評論 0 277
  • 序言:老撾萬榮一對情侶失蹤蛹含,失蹤者是張志新(化名)和其女友劉穎毅厚,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體浦箱,經(jīng)...
    沈念sama閱讀 46,716評論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡吸耿,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,794評論 3 343
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了酷窥。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片咽安。...
    茶點(diǎn)故事閱讀 40,928評論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖蓬推,靈堂內(nèi)的尸體忽然破棺而出妆棒,到底是詐尸還是另有隱情,我是刑警寧澤沸伏,帶...
    沈念sama閱讀 36,583評論 5 351
  • 正文 年R本政府宣布糕珊,位于F島的核電站,受9級特大地震影響毅糟,放射性物質(zhì)發(fā)生泄漏红选。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,264評論 3 336
  • 文/蒙蒙 一姆另、第九天 我趴在偏房一處隱蔽的房頂上張望喇肋。 院中可真熱鬧,春花似錦迹辐、人聲如沸蝶防。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,755評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽间学。三九已至,卻和暖如春贺喝,著一層夾襖步出監(jiān)牢的瞬間菱鸥,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,869評論 1 274
  • 我被黑心中介騙來泰國打工躏鱼, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留氮采,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 49,378評論 3 379
  • 正文 我出身青樓染苛,卻偏偏與公主長得像鹊漠,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子茶行,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,937評論 2 361

推薦閱讀更多精彩內(nèi)容

  • 本文主要總結(jié)了《Java8實(shí)戰(zhàn)》躯概,適用于學(xué)習(xí) Java8 的同學(xué),也可以作為一個(gè) API 手冊文檔適用畔师,平時(shí)使用時(shí)...
    _曉__閱讀 1,185評論 1 6
  • 前言:Java 8 已經(jīng)發(fā)布很久了娶靡,很多報(bào)道表明Java 8 是一次重大的版本升級。在Java Code Geek...
    糖寶_閱讀 1,323評論 1 1
  • java8新特性 原創(chuàng)者:文思 一看锉、特性簡介 速度更快 代碼更少姿锭,增加了Lambda 強(qiáng)大的Stream API ...
    文思li閱讀 3,052評論 1 1
  • 原鏈接:http://www.cnblogs.com/langtianya/p/3757993.html JDK各...
    把愛放下會(huì)走更遠(yuǎn)閱讀 1,116評論 0 10
  • 本教程將Java8的新特新逐一列出,并將使用簡單的代碼示例來指導(dǎo)你如何使用默認(rèn)接口方法伯铣,lambda表達(dá)式呻此,方法引...
    daidai呆呆7閱讀 878評論 0 4