中級09 - Java多線程初步

中級09 - Java多線程初步

介紹多線程帶來的問題,以及基本解決方案。

  • 競爭條件帶來的數(shù)據(jù)錯誤問題
  • 死鎖的原理、排查與防范
  • 線程安全:同步方法、并發(fā)工具包

一 、線程不安全的表現(xiàn) 競爭條件帶來的數(shù)據(jù)錯誤

  • i++
  • if-then-do

如果最終結(jié)果不正確,那么程序跑地再快也沒有意義。

二、死鎖的產(chǎn)生、排查和防范

1. 產(chǎn)生

Java 中某個鎖只能同時被一個線程持有,當(dāng)兩個線程互相在等待對方持有的鎖時就形成了死鎖。

為了便于理解,想象一手交錢一手交貨的問題:

A 持有現(xiàn)金,B 持有粉,但為了掩人耳目,只能通過一個狹小的窗口進行交易,無法同時一手交錢一手交貨,,倆人都持有對方需要的資源,并且也都在等待對方手上的資源,但誰也不愿先給出自己的資源。
此時便形成了死鎖。

類似的還有 哲學(xué)家就餐問題。

public class DeadlockExample {

    private static final Object lock1 = new Object();
    private static final Object lock2 = new Object();

    public static void main(String[] args) {
        new Thread1().start();
        new Thread2().start();
    }

    static class Thread1 extends Thread {
        @Override
        public void run() {
            synchronized (lock1) {
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                synchronized (lock2) {
                    System.out.println("看不到我");
                }
            }
        }
    }


    static class Thread2 extends Thread {
        @Override
        public void run() {
            synchronized (lock2) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                synchronized (lock1) {
                    System.out.println("還是看不到我");
                }
            }
        }
    }

}

2. 排查

使用jps列出當(dāng)前 Java 進程 ID:

jps

17876 Jps
17928 RemoteMavenServer36
9224 Launcher
19260
9164 DeadlockExample

jstack 打印給定 Java 進程中的所有棧信息進行排查:

jstack 9164

2019-09-29 17:01:49
# ...
# 省略
# ...
Found one Java-level deadlock:
=============================
"Thread-1":
  waiting to lock monitor 0x0000000002d8b648 (object 0x00000000d61a4cb0, a java.lang.Object),
  which is held by "Thread-0"
"Thread-0":
  waiting to lock monitor 0x0000000002d8a1a8 (object 0x00000000d61a4cc0, a java.lang.Object),
  which is held by "Thread-1"

Java stack information for the threads listed above:
===================================================
"Thread-1":
        at com.github.hcsp.calculation.DeadlockExample$Thread2.run(DeadlockExample.java:42)
        - waiting to lock <0x00000000d61a4cb0> (a java.lang.Object)
        - locked <0x00000000d61a4cc0> (a java.lang.Object)
"Thread-0":
        at com.github.hcsp.calculation.DeadlockExample$Thread1.run(DeadlockExample.java:24)
        - waiting to lock <0x00000000d61a4cc0> (a java.lang.Object)
        - locked <0x00000000d61a4cb0> (a java.lang.Object)

Found 1 deadlock.

3. 防范

所有的線程都按照相同的順序獲得資源的鎖。

三、實現(xiàn)線程安全的基本手段

1. 不可變類 Integer/String 等

2. 同步方法

2.1 synchronized

  • synchronized (一個對象) 把這個對象當(dāng)成鎖
  • static synchronized方法 把 Class 對象當(dāng)成鎖
  • 實例的 synchronized方法 把該實例當(dāng)成鎖(等價于 synchronized (this) {})

還是之前多個線程同時修改共享變量時的問題,現(xiàn)在采用 synchronizedd 同步塊,使得同一時刻只能有一個線程持有鎖,去執(zhí)行同步塊中的代碼:

public class Test {
    private static int i = 0;
    private static final Object lock = new Object(); // 定義一個鎖

    public static void main(String[] args) {
        for (int j = 0; j < 1000; j++) {
            new Thread(Test::modifySharedVariable).start();
        }
    }

    private static void modifySharedVariable() {
        try {
            Thread.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        synchronized (lock) { // 使用同步塊
            i++;
            System.out.println("i = " + i);
        }
    }
}

除了使用 synchronized 同步塊,還可以使用 synchronized 關(guān)鍵字:

private synchronized static void modifySharedVariable() {
        try {
            Thread.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        i++;
        System.out.println("i = " + i);
    }

2.2 Collections.synchronized*

如 Collections.synchronizedMap 等,基本只是把原來的 map 相關(guān)方法使用 synchronized 同步塊包裝了一遍。
但是注意,使用 Collections.synchronized* 時,如果存在非集合本身的,未經(jīng) synchronized 的操作,那么并不能保證線程安全,還是需要 synchronized 同步塊。

3. 并發(fā)工具包

3.1 ConcurrentHashMap

下圖中的 Collection 和 Map 都是線程不安全的:
[圖片上傳失敗...(image-96a1e3-1575509314567)]

多個線程并發(fā)訪問 map 是不安全的:

import java.util.HashMap;
import java.util.Map;
import java.util.Random;

public class Main {
    private static final Map<Integer, Integer> map = new HashMap<>();

    public static void main(String[] args) {
        for (int j = 0; j < 1000; j++) {
            new Thread(Main::concurrentlyAccess).start();
        }
    }

    private static void concurrentlyAccess() {
        Integer r = new Random().nextInt();
        map.put(r, r);
        for (Integer i : map.keySet()) {
            System.out.println(i);
        }
    }
}

報錯如下:

java.util.ConcurrentModificationException
    at java.util.HashMap$HashIterator.nextNode(HashMap.java:1445)
    at java.util.HashMap$KeyIterator.next(HashMap.java:1469)
    at com.github.hcsp.calculation.Main.concurrentlyAccess(Main.java:25)
    at java.lang.Thread.run(Thread.java:748)

解決辦法除了使用剛才提到的 synchronized,還可以無腦地使用 JUC 并發(fā)工具包提供的線程安全的 ConcurrentHashMap 替換原本涉及到線程安全問題時的 HashMap。

3.2 Atomic*

可以創(chuàng)建具有原子性操作的數(shù)據(jù),如 AtomicInteger。

3.3 ReentrantLock 可重入鎖

ReentrantLock 通過一個雙向鏈表來實現(xiàn)鎖機制,每個節(jié)點代表一條線程,head 節(jié)點擁有
鎖,其他節(jié)點均被掛起等待喚醒。等到當(dāng)前鎖釋放成功后,會喚醒鏈表中的下一個節(jié)點,并將其更新為新的 head 節(jié)點。

可重入鎖是指:當(dāng)線程請求一個由其它線程持有的鎖時,該線程會阻塞,而當(dāng)線程請求由自己持有的鎖時,如果該鎖是可重入鎖,請求就會成功,否則阻塞。

即當(dāng)一個線程獲取了某個對象鎖后,還可以再次獲得該對象鎖。

ReentrantLock 作為一個可重入鎖,內(nèi)部是通過 state 作為計數(shù)器來統(tǒng)計總共執(zhí)行了多少次 lock 方法,如果同一個鎖 lock 兩次,state 為 2,那么只調(diào)用一次 unlock,state 為 1,鎖尚未釋放,再 unlock 一次,state 為 0,鎖成功釋放。

ReentrantLock 和 synchronized 方法/聲明具有相似的行為和語義,但是更靈活強大:

import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockTest {

    private static int i = 0;
    private static final ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {
        for (int i = 0; i < 1000; i++) {
            new Thread(ReentrantLockTest::concurrentlyAccess).start();
        }
    }

    private static void concurrentlyAccess() {
        lock.lock();  // block until condition holds
        // 這里可以掩飾 lock.lock();  // 
        try {
            i = i + 1;
            System.out.println("i = " + i);
        } finally {
            lock.unlock();
        }
    }

}

可重入鎖的作用就是為了避免死鎖,因為 Java 中某個鎖只能同時被一個線程持有,如下例所示,如果 synchronized 鎖不可重入,不可再次獲得,將會造成死鎖,該線程一直等待進入方法 b 需要的這把對象鎖,而該鎖明明又被自己占用著。

所以 synchronized 作為可重入鎖,進入 a 方法時持有當(dāng)前對象鎖,緊接著進入 b 方法時又要請求相同的鎖,此時可重復(fù)持有當(dāng)前對象鎖:

private synchronized void a() {
    b();
}
private synchronized void b() {
}

四、線程的狀態(tài)與Object類中的線程方法

1. 線程的狀態(tài)

Java中線程的狀態(tài)分為 6 種:

  1. 初始(NEW):新創(chuàng)建了一個線程對象,但還沒有調(diào)用start()方法。
  2. 運行(RUNNABLE):Java線程中將就緒(ready)和運行中(running)兩種狀態(tài)籠統(tǒng)的稱為“運行”。

線程對象創(chuàng)建后,其他線程(比如main線程)調(diào)用了該對象的 start() 方法。該狀態(tài)的線程位于可運行線程池中,等待被線程調(diào)度選中,獲取 CPU 的使用權(quán),此時處于就緒狀態(tài)(ready)。就緒狀態(tài)的線程在獲得 CPU 時間片后變?yōu)檫\行中狀態(tài)(running)。

  1. 阻塞(BLOCKED):線程因為需要等待一個鎖時被阻塞的狀態(tài),拿不到鎖,鎖在別的線程手里。

  2. 等待(WAITING):前提是線程已經(jīng)擁有鎖了,然后進入該狀態(tài),等待其他線程做出一些特定動作(通知或中斷)

  3. 超時等待(TIMED_WAITING):該狀態(tài)不同于 WAITING,它可以在指定的時間后自行返回。

  4. 終止(TERMINATED):表示該線程已經(jīng)執(zhí)行完畢。

2. Obect 類中的線程方法

為什么 Java 中所有的對象都可以作為鎖?因為所有的對象都繼承自 Object 類,而 Object 類提供了線程相關(guān)的方法。

2.1 wait

只有持有某個對象鎖的線程(假設(shè)是 A)才能調(diào)用該對象的 wait() 方法,調(diào)用后 A 會釋放鎖并進入waiting 狀態(tài),直到其他某個線程(B)搶到了對象鎖,調(diào)用了該對象鎖的 notify()/notifyAll() 方法后,被 wait 的 A 線程才能重新獲得鎖的所有權(quán)并繼續(xù)執(zhí)行代碼。

2.2 notify

B 線程調(diào)用 notify() 喚醒一個正在等待該鎖的線程 A,若有多個線程都在等待,那么也只會根據(jù) JVM 內(nèi)部機制喚醒其中一個。

2.3 notifyAll

notifyAll() 喚醒所有正在等待該對象鎖的線程。

notify()/notifyAll() 方法所在的 synchronized 方法或塊結(jié)束后,線程 B 釋放該對象鎖,然后這些被喚醒的線程會和其他線程一起按照常規(guī)自由競爭該鎖。

五、生產(chǎn)者和消費者模型(多線程經(jīng)典問題)

每當(dāng)條件滿足時,一個線程被喚醒,做完事情后,讓自己繼續(xù)沉睡的同時順便喚醒別的線程。

請實現(xiàn)一個生產(chǎn)者/消費者模型,其中:
生產(chǎn)者生產(chǎn)10個隨機的整數(shù)供消費者使用(隨機數(shù)可以通過new Random().nextInt()獲得)
使得標(biāo)準(zhǔn)輸出依次輸出它們,例如:

Producing 42
Consuming 42
Producing -1
Consuming -1
...
Producing 10086
Consuming 10086
Producing -12345678
Consuming -12345678

下面演示三種實現(xiàn)方式:

  • wait/notify/notifyAll
  • Lock/Condition
  • BlockingQueue

1. wait/notify/notifyAll

使用最基本的 Object 類中的線程方法和 synchronized。
Boss,整體調(diào)度:

package com.github.hcsp.multithread;

public class Boss {
    public static void main(String[] args) throws InterruptedException {
        Object lock = new Object();
        Container container = new Container();

        Producer producer = new Producer(container, lock);
        Consumer consumer = new Consumer(container, lock);

        producer.start();
        consumer.start();

        producer.join();
        producer.join();
    }
}

Container,盛放制造品的容器:

package com.github.hcsp.multithread;

import java.util.Optional;

public class Container {
    private Optional<Integer> value = Optional.empty();

    public Optional<Integer> getValue() {
        return value;
    }

    public void setValue(Optional<Integer> value) {
        this.value = value;
    }
}

Producerr,生產(chǎn)者:

package com.github.hcsp.multithread;

import java.util.Optional;
import java.util.Random;

public class Producer extends Thread {
    private Container container;
    private Object lock;

    public Producer(Container container, Object lock) {
        this.container = container;
        this.lock = lock;
    }

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            synchronized (lock) {
                while (container.getValue().isPresent()) {
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                Integer r = new Random().nextInt();
                container.setValue(Optional.of(r));
                System.out.println("Producing " + r);
                lock.notify();
            }

        }
    }
}

Consumer,消費者:

package com.github.hcsp.multithread;

import java.util.Optional;

public class Consumer extends Thread {
    private Object lock;
    private Container container;

    public Consumer(Container container, Object lock) {
        this.container = container;
        this.lock = lock;
    }

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            synchronized (lock) {
                while (!container.getValue().isPresent()) {
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                Integer v = container.getValue().get();
                container.setValue(Optional.empty());
                System.out.println("Consuming " + v);
                lock.notify();
            }

        }
    }
}

2. Lock/Condition

使用 ReentrantLock 和 Condition。

ReentrantLock 比 synchronized 自動加解鎖機制更靈活。

Condition 是個接口,在 Java1.5 中出現(xiàn),用來替代傳統(tǒng)的 Object Monitor Methods 中 wait/notify/notifyAll() 的 是 await/signal/signalAll(),這種方式使線程間協(xié)作更加安全和高效,推薦使用。
Condition 依賴于 Lock 接口,生成一個 Condition 的基本代碼是 lock.newCondition()。
Condition 抽離出了線程調(diào)用機制,可以和任意的鎖結(jié)合在一起,使得同一個對象可以有多個等待隊列(一個鎖可以 newCondition() 多次)。
使得在下面的例子中,當(dāng)容器滿了或者空了時,同一時間內(nèi)只需要通知一條對應(yīng)的等待中的線程。

Boss2:

package com.github.hcsp.multithread;

import java.util.concurrent.locks.ReentrantLock;

public class Boss2 {
    public static void main(String[] args) throws InterruptedException {
        ReentrantLock lock = new ReentrantLock();
        Container2 container = new Container2(lock);

        Producer2 producer = new Producer2(container, lock);
        Consumer2 consumer = new Consumer2(container, lock);

        consumer.start();
        producer.start();

        producer.join();
        producer.join();
    }
}

Container2:

package com.github.hcsp.multithread;

import java.util.Optional;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class Container2 {

    private Condition notConsumedYet; // 尚未被消費掉
    private Condition notProducedYet; // 尚未被生產(chǎn)出來
    private Optional<Integer> value = Optional.empty();

    public Container2(ReentrantLock lock) {
        this.notConsumedYet = lock.newCondition();
        this.notProducedYet = lock.newCondition();
    }

    public Condition getNotConsumedYet() {
        return notConsumedYet;
    }

    public Condition getNotProducedYet() {
        return notProducedYet;
    }

    public Optional<Integer> getValue() {
        return value;
    }

    public void setValue(Optional<Integer> value) {
        this.value = value;
    }
}

Producer2:

package com.github.hcsp.multithread;

import java.util.Optional;
import java.util.Random;
import java.util.concurrent.locks.ReentrantLock;

public class Producer2 extends Thread {
    private Container2 container;
    private ReentrantLock lock;

    public Producer2(Container2 container, ReentrantLock lock) {
        this.container = container;
        this.lock = lock;
    }

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            lock.lock();
            try {
                while (container.getValue().isPresent()) {
                    try {
                        container.getNotProducedYet().await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                Integer r = new Random().nextInt();
                container.setValue(Optional.of(r));
                System.out.println("Producing " + r);
                container.getNotConsumedYet().signal();
            } finally {
                lock.unlock();
            }
        }
    }
}

Consumer2:

package com.github.hcsp.multithread;

import java.util.Optional;
import java.util.concurrent.locks.ReentrantLock;

public class Consumer2 extends Thread {
    private Container2 container;
    private ReentrantLock lock;

    public Consumer2(Container2 container, ReentrantLock lock) {
        this.container = container;
        this.lock = lock;
    }

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            lock.lock();
            try {
                while (!container.getValue().isPresent()) {
                    try {
                        container.getNotConsumedYet().await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                Integer v = container.getValue().get();
                container.setValue(Optional.empty());
                System.out.println("Consuming " + v);
                container.getNotProducedYet().signal();
            } finally {
                lock.unlock();
            }
        }
    }
}

另外說一下個人對以上這 2 種生產(chǎn)者和消費者模型實現(xiàn)方式的理解:

設(shè)置條件判斷是因為需要先生產(chǎn)后才能消費, 未消費前也不能再生產(chǎn), 所以當(dāng)生產(chǎn)或消費條件不滿足時, 需要令當(dāng)前線程進入 waiting。
另一方面,是為了當(dāng)前線程被中斷和假喚醒時,要繼續(xù)進入 waiting, 并且要用循環(huán)一直"盯"著。
另外,在 for 循環(huán)中,synchronized 和 ReentrantLock 作為可重入鎖可以重復(fù)加鎖, 條件不成熟時也可重新進入鎖,故也需要條件判斷。

3. BlockingQueue

BlockingQueue 取回元素時要等待隊列非空,存儲元素時要等待隊列非滿,可以方便的實現(xiàn)生產(chǎn)者和消費者模型,創(chuàng)建兩條 BlockingQueue,一條負(fù)責(zé)管理產(chǎn)品,一條負(fù)責(zé)管理調(diào)度信號,不再需要手動創(chuàng)建容器。

Boss3:

package com.github.hcsp.multithread;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

public class Boss3 {
    public static void main(String[] args) throws InterruptedException {
        BlockingQueue queue = new ArrayBlockingQueue(1);
        BlockingQueue signalQueue = new ArrayBlockingQueue(1);

        Producer3 producer = new Producer3(queue, signalQueue);
        Consumer3 consumer = new Consumer3(queue, signalQueue);

        producer.start();
        consumer.start();

        producer.join();
        producer.join();
    }
}

Producer3:

package com.github.hcsp.multithread;

import java.util.Random;
import java.util.concurrent.BlockingQueue;

public class Producer3 extends Thread {
    BlockingQueue<Integer> queue;
    BlockingQueue<Integer> signalQueue;

    public Producer3(BlockingQueue<Integer> queue, BlockingQueue<Integer> signalQueue) {
        this.queue = queue;
        this.signalQueue = signalQueue;
    }

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            int r = new Random().nextInt();
            System.out.println("Producing " + r);
            try {
                queue.put(r);
                signalQueue.take();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

Consumer3:

package com.github.hcsp.multithread;

import java.util.concurrent.BlockingQueue;

public class Consumer3 extends Thread {
    BlockingQueue<Integer> queue;
    BlockingQueue<Integer> signalQueue;

    public Consumer3(BlockingQueue<Integer> queue, BlockingQueue<Integer> signalQueue) {
        this.queue = queue;
        this.signalQueue = signalQueue;
    }

        @Override
        public void run () {
            for (int i = 0; i < 10; i++) {
                try {
                    System.out.println("Consuming " + queue.take());
                    signalQueue.put(0);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

六、線程池與 Callable/Future

1. Callable/Future

1.1 Callable

Callable 接口引入自 Java1.5,和 Runnable 一樣,它們的實例設(shè)計用于在其他線程中執(zhí)行,但不同的是, Callable 可以返回值,也能拋異常。

1.2 Future

Future 接口代表?個“未來才會返回的結(jié)果”,調(diào)用 get() 方法會等待直到拿到計算結(jié)果。

2. 什么是線程池

2.1 定義

Java 的線程調(diào)度完全依賴于操作系統(tǒng)的線程調(diào)度,線程是昂貴的,不能無節(jié)制地開辟線程,以免將操作系統(tǒng)的資源耗盡,所以線程池是預(yù)先定義好的若干個線程。

2.2 Java 中的線程池

public static void main(String[] args) throws ExecutionException, InterruptedException {
    // 簡單場景下為方便起見,直接使用 Executors 工具類快速創(chuàng)建線程池
    ExecutorService threadPool = Executors.newFixedThreadPool(10);

    // submit 方法會立刻返回一個 Future(類似于 JS 中的 Promise)
    // 提交的任務(wù)會異步執(zhí)行,不會阻塞當(dāng)前線程
    Future<Integer> future1 = threadPool.submit(new Callable<Integer>() {
        @Override
        public Integer call() throws Exception {
            Thread.sleep(3000);
            return 0;
        }
    });

    Future<String> future2 = threadPool.submit(new Callable<String>() {
        @Override
        public String call() throws Exception {
            Thread.sleep(1000);
            return "ojbk";
        }
    });

    Future<Object> future3 = threadPool.submit(new Callable<Object>() {
        @Override
        public Object call() throws Exception {
            throw new RuntimeException();
        }
    });

    System.out.println(future1.get());
    System.out.println(future2.get());
    System.out.println(future3.get());
}

2.3 線程池的構(gòu)造函數(shù)(有坑待填)

3. 實戰(zhàn):多線程的 WordCount

創(chuàng)建 WordCount 對象時傳入文件列表,可統(tǒng)計這些文件中的單詞數(shù)。

使用方法:

List<File> files = xxx;
WordCount wordCount = new WordCount(10);
Map<String, Integer> countResult = wordCount.count(files);
System.out.println(countResult);

WordCount 類的實現(xiàn):

package com.github.hcsp.multithread;

import org.apache.commons.io.FileUtils;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class WordCount {

    private final int threadNum;
    private ExecutorService threadPool;

    public WordCount(int threadNum) {
        threadPool = Executors.newFixedThreadPool(threadNum);
        this.threadNum = threadNum;
    }

    /**
     * 統(tǒng)計文件中各單詞的數(shù)量
     *
     * @param files 文件列表
     * @return 單詞統(tǒng)計結(jié)果
     * @throws IOException          文件讀寫出錯時拋出
     * @throws ExecutionException   Future 取回結(jié)果出錯時拋出
     * @throws InterruptedException 線程被中斷時拋出
     */
    public Map<String, Integer> count(List<File> files) throws IOException, ExecutionException, InterruptedException {
        BufferedReader reader = new BufferedReader(new FileReader(mergeFilesIntoSingleFile(files)));
        List<Future<Map<String, Integer>>> futures = new ArrayList<>();
        // 開辟若干個線程,每個線程讀取文件的一行內(nèi)容,并將單詞統(tǒng)計結(jié)果返回
        // 最后主線程將工作線程返回的結(jié)果匯總在一起
        for (int i = 0; i < threadNum; i++) {
            futures.add(threadPool.submit(new WorkerJob(reader)));
        }
        // 最終結(jié)果集
        Map<String, Integer> finalResult = new HashMap<>();
        // 將futures中的每個子結(jié)果集合并到終集中
        for (Future<Map<String, Integer>> future : futures) {
            Map<String, Integer> resultFromWorker = future.get();
            mergeWorkerResultIntoFinalResult(resultFromWorker, finalResult);
        }
        threadPool.shutdown();
        return finalResult;
    }

    /**
     * 將文件列表中的文件合并為一個文件
     *
     * @param files 文件列表
     * @return 結(jié)果文件
     * @throws IOException 文件讀寫出錯時拋出
     */
    private File mergeFilesIntoSingleFile(List<File> files) throws IOException {
        File result = File.createTempFile("tmp", "");
        for (File file : files) {
            String encoding = "UTF-8";
            FileUtils.write(result, FileUtils.readFileToString(file, encoding), encoding, true);
        }
        return result;
    }

    /**
     * 將子集合并到終集中
     *
     * @param resultFromWorker 終集
     * @param finalResult      子集
     */
    private void mergeWorkerResultIntoFinalResult(Map<String, Integer> resultFromWorker,
                                                  Map<String, Integer> finalResult) {
        for (Map.Entry<String, Integer> entry : resultFromWorker.entrySet()) {
            String word = entry.getKey();
            int mergedResult = finalResult.getOrDefault(word, 0) + entry.getValue();
            finalResult.put(word, mergedResult);
        }
    }

    static class WorkerJob implements Callable<Map<String, Integer>> {
        private BufferedReader reader;

        private WorkerJob(BufferedReader reader) {
            this.reader = reader;
        }

        @Override
        public Map<String, Integer> call() throws Exception {
            String line;
            Map<String, Integer> result = new HashMap<>();
            while ((line = reader.readLine()) != null) {
                String[] words = line.split(" ");
//                System.out.println(Thread.currentThread().getName());
//                System.out.println(line);
//                System.out.println();
                for (String word : words) {
                    result.put(word, result.getOrDefault(word, 0) + 1);
                }
            }
            return result;
        }
    }
}

簡單刪除一些代碼就可以實現(xiàn)單文件統(tǒng)計(略)。


參考:

  1. Java線程的6種狀態(tài)及切換(透徹講解)
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,443評論 6 532
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,530評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,407評論 0 375
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,981評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 71,759評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,204評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,263評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,415評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,955評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,782評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,528評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,222評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,650評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,892評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,675評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 47,967評論 2 374

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