中級09 - Java多線程初步

中級09 - Java多線程初步

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

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

一 、線程不安全的表現 競爭條件帶來的數據錯誤

  • i++
  • if-then-do

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

二、死鎖的產生、排查和防范

1. 產生

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

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

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

類似的還有 哲學家就餐問題。

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列出當前 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. 防范

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

三、實現線程安全的基本手段

1. 不可變類 Integer/String 等

2. 同步方法

2.1 synchronized

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

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

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 關鍵字:

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 相關方法使用 synchronized 同步塊包裝了一遍。
但是注意,使用 Collections.synchronized* 時,如果存在非集合本身的,未經 synchronized 的操作,那么并不能保證線程安全,還是需要 synchronized 同步塊。

3. 并發工具包

3.1 ConcurrentHashMap

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

多個線程并發訪問 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 并發工具包提供的線程安全的 ConcurrentHashMap 替換原本涉及到線程安全問題時的 HashMap。

3.2 Atomic*

可以創建具有原子性操作的數據,如 AtomicInteger。

3.3 ReentrantLock 可重入鎖

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

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

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

ReentrantLock 作為一個可重入鎖,內部是通過 state 作為計數器來統計總共執行了多少次 lock 方法,如果同一個鎖 lock 兩次,state 為 2,那么只調用一次 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 方法時持有當前對象鎖,緊接著進入 b 方法時又要請求相同的鎖,此時可重復持有當前對象鎖:

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

四、線程的狀態與Object類中的線程方法

1. 線程的狀態

Java中線程的狀態分為 6 種:

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

線程對象創建后,其他線程(比如main線程)調用了該對象的 start() 方法。該狀態的線程位于可運行線程池中,等待被線程調度選中,獲取 CPU 的使用權,此時處于就緒狀態(ready)。就緒狀態的線程在獲得 CPU 時間片后變為運行中狀態(running)。

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

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

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

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

2. Obect 類中的線程方法

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

2.1 wait

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

2.2 notify

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

2.3 notifyAll

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

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

五、生產者和消費者模型(多線程經典問題)

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

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

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

下面演示三種實現方式:

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

1. wait/notify/notifyAll

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

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,生產者:

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

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; // 尚未被生產出來
    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 種生產者和消費者模型實現方式的理解:

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

3. BlockingQueue

BlockingQueue 取回元素時要等待隊列非空,存儲元素時要等待隊列非滿,可以方便的實現生產者和消費者模型,創建兩條 BlockingQueue,一條負責管理產品,一條負責管理調度信號,不再需要手動創建容器。

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 一樣,它們的實例設計用于在其他線程中執行,但不同的是, Callable 可以返回值,也能拋異常。

1.2 Future

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

2. 什么是線程池

2.1 定義

Java 的線程調度完全依賴于操作系統的線程調度,線程是昂貴的,不能無節制地開辟線程,以免將操作系統的資源耗盡,所以線程池是預先定義好的若干個線程。

2.2 Java 中的線程池

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

    // submit 方法會立刻返回一個 Future(類似于 JS 中的 Promise)
    // 提交的任務會異步執行,不會阻塞當前線程
    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 線程池的構造函數(有坑待填)

3. 實戰:多線程的 WordCount

創建 WordCount 對象時傳入文件列表,可統計這些文件中的單詞數。

使用方法:

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

WordCount 類的實現:

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

    /**
     * 統計文件中各單詞的數量
     *
     * @param files 文件列表
     * @return 單詞統計結果
     * @throws IOException          文件讀寫出錯時拋出
     * @throws ExecutionException   Future 取回結果出錯時拋出
     * @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<>();
        // 開辟若干個線程,每個線程讀取文件的一行內容,并將單詞統計結果返回
        // 最后主線程將工作線程返回的結果匯總在一起
        for (int i = 0; i < threadNum; i++) {
            futures.add(threadPool.submit(new WorkerJob(reader)));
        }
        // 最終結果集
        Map<String, Integer> finalResult = new HashMap<>();
        // 將futures中的每個子結果集合并到終集中
        for (Future<Map<String, Integer>> future : futures) {
            Map<String, Integer> resultFromWorker = future.get();
            mergeWorkerResultIntoFinalResult(resultFromWorker, finalResult);
        }
        threadPool.shutdown();
        return finalResult;
    }

    /**
     * 將文件列表中的文件合并為一個文件
     *
     * @param files 文件列表
     * @return 結果文件
     * @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;
        }
    }
}

簡單刪除一些代碼就可以實現單文件統計(略)。


參考:

  1. Java線程的6種狀態及切換(透徹講解)
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容