網頁

2020/8/22

Java synchronized block refactor 考題

最近面試的問題,測驗對於synchronized的理解。

題目類似如下,有一個類別Item記錄狀態。

Item

package com.abc.demo;

public class Item {

    private String name;
    private int value;

    public Item(String name, int value) {
        this.name = name;
        this.value = value;
    }

    public void accumulateValue() {
        value++;
    }

    public String getName() {
        return name;
    }

    public int getValue() {
        return value;
    }
}

WholeItem持有全部的Item,為單例類別,並有一方法可依傳入的名稱累加Item的值。

WholeItem

package com.abc.demo;

import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

public class WholeItem {

    private static final Set<Item> items = Collections.synchronizedSet(new HashSet<>());
    static {
        items.add(new Item("A", 0));
        items.add(new Item("B", 0));
        items.add(new Item("C", 0));
        items.add(new Item("D", 0));
    }

    private static WholeItem instance = null;

    private WholeItem() {}

    public static WholeItem getInstance() {
        if (instance == null) {
            instance = new WholeItem();
        }
        return instance;
    }

    public void add (String name) {
        synchronized (items) { // <--
            for (Item i : items) {
                if (i.getName().equals(name)) {
                    i.accumulateValue();
                }
            }
        }
    }

    public Set<Item> getItems() {
        return items;
    }

}

請問上面的synchronized是否有可以調整的地方?




上面synchronized的問題在於放在一個for loop迴圈外,synchronized區塊一次只能有一條執行緒進入,就代表每條進入同步區塊的執行緒都必須執行完迴圈後才能讓下個執行緒進入,如果今天items的數量有很多個則效能會非常差。

使用synchronized的大方向就是範圍越小越好。同步區塊範圍越大代表被鎖定的時間越久,區塊中如有迴圈存在那要檢視一下是否必要。

因此上面的synchronized可移到Item.accumulateValue()使其成為同步方法,也可避免同時被不同執行緒累加值。修改如下。

Item

package com.abc.demo;

public class Item {

    private String name;
    private int value;

    public Item(String name, int value) {
        this.name = name;
        this.value = value;
    }

    public synchronized void accumulateValue() { // 加上synchronized
        value++;
    }

    public String getName() {
        return name;
    }

    public int getValue() {
        return value;
    }
}

WholeItem

package com.abc.demo;

import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

public class WholeItem {

    private static final Set<Item> items = Collections.synchronizedSet(new HashSet<>());
    static {
        items.add(new Item("A", 0));
        items.add(new Item("B", 0));
        items.add(new Item("C", 0));
        items.add(new Item("D", 0));
    }

    private static WholeItem instance = null;

    private WholeItem() {}

    public static WholeItem getInstance() {
        // 移除原本的synchronized block
        if (instance == null) {
            instance = new WholeItem();
        }
        return instance;
        
    }

    public void add (String name) {

        for (Item i : items) {
            if (i.getName().equals(name)) {
                i.accumulateValue();
            }
        }

    }

    public Set<Item> getItems() {
        return items;
    }

}

下面這段是我自己寫來測試看看的。

Main

package com.abc.demo;

import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class Main {

    public static void main(String[] args) throws InterruptedException {

        WholeItem wholeItem = WholeItem.getInstance();

        List<String> itemNames = Arrays.asList("A", "B", "C"); // 要累加值得item名稱

        ExecutorService es = Executors.newCachedThreadPool();

        // 每個名稱跑一條thread來累加
        itemNames.forEach(name -> {
            es.execute(new Runnable() {
                @Override
                public void run() {
                    wholeItem.add(name);
                }
            });
        });

        es.shutdown();

        boolean finished = es.awaitTermination(1, TimeUnit.SECONDS); // 執行緒是否結束

        while (finished) { // 執行緒結束印出每個item的值
            wholeItem.getItems().forEach(item -> {
                System.out.println(item.getName() + ":" + item.getValue());
            });
            finished = false; // 印完值後離開while迴圈
        }
    }
}

因為之前工作都沒寫過執行緒,這次面試考了不少執行緒問題讓我頭很大:p,這也是我第一次在面試中碰到執行緒問題。


沒有留言:

張貼留言