Java併發容器和同步工具類

同步容器類

早期的同步容器類Vector、Hashtable和Collections.synchronizedXXX創建的同步容器,封裝所有public方法,以保證線程安全。

問題:迭代操作期間可能拋ArrayIndexOutOfBoundsException或ConcurrentModificationException

示例代碼:

//遍歷vector時,其他線程修改vector,可能拋ArrayIndexOutOfBoundsException

for (int i = 0; i < vector.size(); i++) {

    //操作vector.get(i);

}

List<Person> personList = Collections.synchronizedList(new ArrayList<Person>());

//遍歷personList時,其他線程修改personList,導致計數器變化,再調用next或hasNext時可能會拋ConcurrentModificationException

for (Person person : personList) {

    //操作person

}

所以,需要在遍歷操作期間持有容器的鎖,可能會導致併發性和吞吐量降低。

注:容器的toString、hashCode、equals、containsAll、removeAll、retainAll等也隱含執行遍歷操作


併發容器

Java 5.0中增加了ConcurrentHashMap和CopyOnWriteArrayList以代替同步Map和List,還增加了Queue用來臨時保存一組等待處理的元素,提供了幾種實現ConcurrentLinkedQueue、PriorityQueue;還增加了Queue的擴展類BlockingQueue阻塞隊列

Java6也增加了ConcurrentSkipListMap和ConcurrentSkipListSet,分別替代SortedMap和SortedSet

ConcurrentHashMap(Java)使用了分段鎖(Lock Striping)以使讀取操作線程和寫入操作線程可以併發的訪問Map,它提供的迭代器不會拋出ConcurrentModificationException;在JDK1.8中,ConcurrentHashMap的實現原理摒棄了這種設計,而是選擇了與HashMap類似的數組+鏈表+紅黑樹的方式實現,而加鎖則採用CAS和synchronized實現。

示例代碼:

public class ConcurrentContainer {
    private static final ConcurrentHashMap conHashMap = new ConcurrentHashMap();
    private static CountDownLatch latch = new CountDownLatch(1);

    public static void readMap() {
        Thread rThread = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    latch.await();
                    //每隔1秒遍歷一次
                    Map.Entry entry;
                    while (true) {
                        System.out.println("***********************");
                        for (Object enObj : conHashMap.entrySet()) {
                            entry = (Map.Entry) enObj;
                            System.out.println("[Key=" + entry.getKey() + "]-[Value=" + entry.getValue() + "]");
                        }
                        System.out.println("***********************");
                        System.out.println("");
                        Thread.sleep(1000);
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        rThread.start();
    }

    public static void writeMap() {
        Thread wThread = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    latch.await();
                    //每隔10秒遍歷一次
                    Random ran = new Random(1);
                    while (true) {
                        conHashMap.put("key" + (int) (ran.nextFloat() * 1000), Math.abs(ran.nextInt() * 1000));
                        Thread.sleep(1000);
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            }
        });
        wThread.start();
    }


    public static void main(String[] args) {
        readMap();
        writeMap();
        latch.countDown();
    }
}

CopyOnWriteArrayList/CopyOnWriteArraySet,在每次修改時,都會創建並重新發佈一個新的容器副本,從而實現可變性,它提供的迭代器不會拋出ConcurrentModificationException。每當修改容器時都會複製底層數組,需要一定的開銷,因此,僅當迭代操作遠多於修改操作時,才 應該使用“寫入時複製”容器。

示例代碼:

public class ConListContainer {
    private static final CopyOnWriteArrayList cowList = new CopyOnWriteArrayList();
    private static CountDownLatch latch = new CountDownLatch(1);

    public static void readAndWriteList() throws InterruptedException {
        Thread rThread = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    latch.await();
                    //每隔1秒遍歷一次
                    while (true) {
                        System.out.println("***********************");
                        System.out.println("cowList is: " + cowList);
                        System.out.println("***********************");
                        System.out.println("");
                        Thread.sleep(1000);
                        if (cowList.size() > 10)
                            break;
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        Thread wThread = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    latch.await();
                    //每隔10秒遍歷一次
                    Random ran = new Random(1);
                    while (true) {
                        cowList.add(ran.nextInt());
                        Thread.sleep(500);
                        if (cowList.size() > 10)
                            break;
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        rThread.start();
        wThread.start();
        latch.countDown();
        rThread.join();
        wThread.join();
        System.out.println("Finally, cowList is " + cowList);
    }

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

阻塞隊列

阻塞隊列提供了可阻塞的put和take方法,以及支持定時的offer和poll方法。如果隊列已經滿了,那麼put方法將阻塞直到有空間可用;如果隊列爲空,那麼take方法將會阻塞知道有元素可用。BlockingQueue的實現包括ArrayBlockingQueue(基於數組,可做有界隊列)、LinkedBlockingQueue(基於鏈表,可做有界隊列和無界隊列)、SynchronousQueue(隊列中最多只能有一個元素,使用上可選公平模式和非公平模式)等。LinkedBlockingQueue、ArrayBlockingQueue是FIFO隊列,分別類似LinkedList和ArrayList,但比同步List有更好的併發性。BlockingQueue適用於生產者-消費者模式(所有消費者共享一個工作隊列)的問題。

示例代碼:

public class BlockingQueueTest {
    private static final BlockingQueue blockingQueue = new ArrayBlockingQueue(20);

    private static void testBlockingQueue() throws InterruptedException {
        Thread pThread = new Thread(new Runnable() {
            @Override
            public void run() {
                Random rand = new Random(1);
                int temp;
                try {
                    while (blockingQueue.size() < 20) {
                        temp = (int) (1000 * rand.nextFloat());
                        System.out.println("pThread 放入: " + temp);
                        blockingQueue.put(temp);
                        System.out.println("pThread讀到blockingQueue當前有" + blockingQueue.size() + "個元素");
                        Thread.sleep(1000);
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    Thread.currentThread().interrupt();
                }
            }
        });

        Thread cThread = new Thread(new Runnable() {
            @Override
            public void run() {
                Object obj;
                try {
                    //只要有就一直取
                    while ((obj = blockingQueue.take()) != null) {
                        //take方法會一直阻塞知道有元素可拿
                        Thread.sleep(3000);
                        System.out.println("cThread拿到了:" + obj);
                        System.out.println("cThread讀到blockingQueue當前有" + blockingQueue.size() + "個元素");
                        if (0 == blockingQueue.size()){
                            break;
                        }
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        pThread.start();
        cThread.start();
        pThread.join();
        cThread.join();
        System.out.println("最後,blockingQueue當前有: " + blockingQueue.size() + "個元素");
    }

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

Deque是一個雙端隊列,支持FIFO、FILO,實現了在隊列頭和隊列尾的高效插入和移除,具體實現包括ArrayDeque(非線程安全)和LinkedBlockingDeque(線程安全)。Deque適用於工作密取(Work Stealing)模式(每個消費者都有各自的雙端隊列)的問題,提供了更高的可伸縮性。

示例代碼:

public class BlockingDequeTest {
    private static final BlockingDeque blockingDeque = new LinkedBlockingDeque();
    private static final CountDownLatch latch = new CountDownLatch(1);
    private static final AtomicLong ATOM_INT = new AtomicLong(100L);
    private static final String ALL_LETTERS = "abcdefghijklmnopqrstuvwxyz";

    private static void testBlockingDeque() throws InterruptedException {
        Thread lrThread = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    latch.await();
                    while (true) {
                        Object obj = blockingDeque.pollFirst();
                        if (obj != null) {
                            System.out.println("lrThread gets: [" + obj + "] from Deque");
                            if (ATOM_INT.decrementAndGet() < 0L) {
                                System.out.println("lrThread: tired of this stupid game, BYE!");
                                break;
                            }
                        }
                        Thread.sleep(40);
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            }
        });

        Thread lwThread = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    latch.await();
                    String str;
                    for (; ; ) {
                        str = genRandomStr();
                        blockingDeque.offerFirst(str);
                        System.out.println("lwThread writes: [" + str + "] to Deque");
                        if (ATOM_INT.longValue() < 0L) {
                            System.out.println("lwThread: I've got a bad feeling about this...");
                            break;
                        }
                        Thread.sleep(200);
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        Thread rrThread = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    latch.await();
                    while (true) {
                        Object obj = blockingDeque.pollLast();
                        if (null != obj) {
                            System.out.println("rrThread gets: [" + obj + "] from Deque");
                            if (ATOM_INT.decrementAndGet() < 0L) {
                                System.out.println("rrThread: OUT!");
                                break;
                            }
                        }
                        Thread.sleep(30);
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        Thread rwThread = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    latch.await();
                    String str;
                    for (; ; ) {
                        str = genRandomStr();
                        blockingDeque.addLast(str);
                        System.out.println("lwThread writes: [" + str + "] to Deque");
                        if (ATOM_INT.longValue() < 0L) {
                            System.out.println("rwThread: Ew!");
                            break;
                        }
                        Thread.sleep(150);
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        lrThread.start();
        lwThread.start();
        rrThread.start();
        rwThread.start();
        latch.countDown();
        lrThread.join();
        lwThread.join();
        rrThread.join();
        rwThread.join();
        System.out.println("Finally, blockingDeque is: " + blockingDeque);
    }

    private static String genRandomStr() {
        Random ran = new Random();
        String tempStr = "";
        int idx1;
        for (int i = 0; i < 3; i++) {
            idx1 = Math.abs((int) (26 * ran.nextFloat()));
            tempStr += ALL_LETTERS.substring(idx1, idx1 + 1);
        }
        return tempStr + Math.abs((int) (10 * ran.nextFloat()));
    }

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

阻塞方法與中斷方法

線程可能會阻塞或暫停執行,原因有多種:等待I/O操作結束(BLOCKED),等待獲得一個鎖(WAITING),等待從Thread.sleep方法中醒來(TIMED_WAITING),或是等待另一個線程的結算結果。當代碼中調用了一個將拋出InterruptedException異常的方法時,通常可以傳遞InterruptedException給調用者或通過調用當前線程上的interrupt方法恢復中斷。如:

public class TaskRunnable implements Runnable {
    BlockingQueue<Task> queue;

	 @Override
     public void run() {
        try {
            processTask(queue.take());
        } catch (InterruptedException e) {
            //恢復被中斷的狀態
            Thread.currentThread().interrupt();
        }
     }
}

同步工具類

閉鎖(CountDownLatch)

閉鎖是一種同步工具類,可以延遲線程的進度直到其到達中止狀態。底層是基於 AQS(AbstractQueuedSynchronizer)實現,可以比join方法對線程有更靈活的控制。

示例代碼:

public class TestHarness {
    public long timeTasks(int nThreads, final Runnable task) throws InterruptedException {
        final CountDownLatch startGate = new CountDownLatch(1);
        final CountDownLatch endGate = new CountDownLatch(nThreads);

        for (int i = 0; i < nThreads; i++) {
            Thread t = new Thread() {
                @Override
                public void run() {
                    try {
                        startGate.await();
                        try {
                            task.run();
                        } finally {
                            endGate.countDown();
                        }
                    } catch (InterruptedException e) {
                    }
                }
            };
        }
        long start = System.nanoTime();
        startGate.countDown();
        endGate.await();
        long end = System.nanoTime();
        return end - start;
    }
}

FutureTask也可以用作閉鎖

FutureTask是一個可取消的異步計算,實現了Runnable和Future接口,通常用來包裝一個Callable對象,可以異步執行,並將計算結果返回調用主線程。

示例代碼:

public class FutureTaskTest {
    private static AtomicInteger atomicInteger = new AtomicInteger(1);
    private static CountDownLatch countDownLatch = new CountDownLatch(3);

    private static void testFutureTask() throws ExecutionException, InterruptedException {
        ExecutorService executor = Executors.newCachedThreadPool();
        List<FutureTask<Integer>> taskList = new ArrayList<FutureTask<Integer>>(10);

        for (int i = 0; i < 10; i++) {
            FutureTask<Integer> futureTask = new FutureTask<Integer>(new Callable<Integer>() {
                @Override
                public Integer call() throws Exception {
                    countDownLatch.await();
                    Random ran = new Random();
                    int res = 0;
                    for (int j = 0; j < 10; j++) {
                        res += Math.abs(ran.nextInt() / 100);
                    }
                    int sleepSec = atomicInteger.getAndIncrement();
                    System.out.println("Thread-" + Thread.currentThread().getId() + " will run for " + sleepSec + " seconds");
                    Thread.sleep(1000 * sleepSec);
                    System.out.println("Thread-" + Thread.currentThread().getId() + " returns " + res);
                    return res;
                }
            });
            executor.submit(futureTask);
            taskList.add(futureTask);
        }

        countDownLatch.countDown();//3
        countDownLatch.countDown();//2
        countDownLatch.countDown();//1

        int totRes = 0;
        for (FutureTask<Integer> task : taskList) {
            totRes += task.get().intValue();
        }
        System.out.println("Final result: " + totRes);
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        testFutureTask();
    }
}

信號量(Semaphore)

計數信號量用來控制同時訪問某個特定資源的操作數量,或者同時執行某個指定操作的數量。Semaphore可以用於實現資源池。

示例代碼:

/**
 * Running 24/7
 */
public class PublicToilet {
    private static final int SPOTS = 5;
    private static final Random RANDOM = new Random();
    private static final SimpleDateFormat FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");
    private static final Semaphore SEMAPHORE = new Semaphore(SPOTS, false); // Not a fair WC
    private static final AtomicInteger USE_TIMES = new AtomicInteger(0);

    private void startService() {
        for (int i = 0; i < 20; i++) {
            WcGoer goer = new WcGoer();
            goer.setErgentLevel((short) (RANDOM.nextInt() % 2));
            Thread thread = new Thread(new WcGoer());
            thread.start();
        }
    }

    private class WcGoer implements Runnable {
        private short ergentLevel;

        public short getErgentLevel() {
            return ergentLevel;
        }

        public void setErgentLevel(short ergentLevel) {
            this.ergentLevel = ergentLevel;
        }

        @Override
        public void run() {
            try {
                int useTime;
                if (ergentLevel == 0) {
                    SEMAPHORE.acquire();
                } else {
                    while (!SEMAPHORE.tryAcquire()) {
                        System.out.println(FORMAT.format(new Date()) + ":" + Thread.currentThread().getName() + " keep waiting");
                        Thread.sleep(1000 * 3);
                    }
                }
                System.out.println(FORMAT.format(new Date()) + ":" + Thread.currentThread().getName() + " enters WC" +
                        ((ergentLevel == 0) ? "" : " in a hurry"));
                useTime = Math.abs(RANDOM.nextInt() % 11);
                System.out.println(FORMAT.format(new Date()) + ":" + Thread.currentThread().getName() + " will use for " + useTime + " seconds");
                Thread.sleep(1000 * useTime);
                System.out.println(FORMAT.format(new Date()) + ":" + Thread.currentThread().getName() + " exits WC");
                SEMAPHORE.release();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    private static void enterMaintenance() throws InterruptedException {
        //occupy all spots
        while (!SEMAPHORE.tryAcquire(SPOTS)) {
        }
        System.exit(0);
    }

    public static void main(String[] args) {
        PublicToilet pt = new PublicToilet();
        pt.startService();
    }

}

柵欄(Barrier)

柵欄(Barrier)類似於閉鎖,它能阻塞一組線程直到某個事件發生。柵欄與閉鎖的關鍵區別在於,所有線程必須同時到達柵欄位置,才能繼續執行。閉鎖用於等待事件,而柵欄用於等待其他線程。

CyclicBarrier可以使一定數量的參與方反覆地在柵欄位置彙集,它在並行迭代算法中非常有用:這種算法通常將一個問題拆分成一系列互相獨立的子問題。當線程到達柵欄位置時將調用await方法,這個方法將阻塞直到所有線程都到達柵欄位置。如果所有線程都到達了柵欄位置,那麼柵欄將打開,此時所有線程都被釋放,而柵欄將被重置以便下次使用。

另一種形式的柵欄是Exchanger,它是一種兩方(Two-Party)柵欄,各方在柵欄位置上交還數據。

示例代碼:

public class BankRobbing {
    private static Random random = new Random();
    private static String CHICKEN_OUT = "";
    private CyclicBarrier TOUGH_TANK = new CyclicBarrier(5, new Runnable() {
        @Override
        public void run() {
            try {
                TimeUnit.SECONDS.sleep(3L);
                System.out.println("Take a deep breath, guys..");
                TimeUnit.SECONDS.sleep(3L);
                System.out.println("Ok, let's go make some money!");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }
    });

    private class BankRobber extends Thread {
        private String name;

        public BankRobber(String name) {
            this.name = name;
        }

        @Override
        public void run() {
            int time;
            System.out.println("Criminal " + name + " setting off for TOUGH_TANK");
            try {
                time = Math.abs(random.nextInt() % 20);
                TimeUnit.SECONDS.sleep(time);
                System.out.println("Criminal " + name + " reaches TOUGH_TANK");
                if (time < 5 && "".equals(CHICKEN_OUT)) {
                    CHICKEN_OUT = name;
                    TOUGH_TANK.await(5, TimeUnit.SECONDS);
                } else {
                    TOUGH_TANK.await();
                }
                System.out.println("Go,go,go!!!");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (BrokenBarrierException e) {
                System.out.println(CHICKEN_OUT + " chickens out, " + name + " flees...");
                //e.printStackTrace();
            } catch (TimeoutException e) {
                System.out.println(name + " can't bear the waiting......");
                //e.printStackTrace();
            }
        }
    }

    public void doRobBank() {
        String[] robbers = {"Carl", "Coughlin", "Ben", "Mike", "Douglas"};
        for (int i = 0; i < robbers.length; i++) {
            new BankRobber(robbers[i]).start();
        }
    }

    public static void main(String[] args) {
        BankRobbing bankRobbing = new BankRobbing();
        bankRobbing.doRobBank();
    }
}

示例代碼:

public class MethBooth {
    private Exchanger<Object> exchanger;

    public MethBooth(Exchanger<Object> exchanger) {
        this.exchanger = exchanger;
    }

    private class DrugDealer implements Runnable {
        private String infiniteDrug;
        private int money;

        public DrugDealer(String infiniteDrug, int money) {
            this.infiniteDrug = infiniteDrug;
            this.money = money;
        }

        @Override
        public void run() {
            Object object;
            System.out.println("DrugDealer: Walking to rendezvous");
            try {
                for (; ; ) {
                    TimeUnit.SECONDS.sleep(5);
                    object = exchanger.exchange(infiniteDrug);
                    if (object != null && "NARC".equals(object.toString())) {
                        System.out.println("DrugDealer:COPS! Running away......");
                        Thread.currentThread().interrupt();
                    } else {
                        Integer drugMoney = (Integer) object;
                        System.out.println("DrugDealer: Getting $" + drugMoney);
                        money += drugMoney;
                    }
                }
            } catch (InterruptedException e) {
                System.out.println("DrugDealer: I'm gone, collecting money $" + money + " in total.");
                e.printStackTrace();
            }
        }
    }

    private class DrugUser implements Runnable {
        private int money;

        public DrugUser(int money) {
            this.money = money;
        }

        @Override
        public void run() {
            System.out.println("DrugUser: Driving to rendezvous");
            Object object;
            try {
                while (money > 0) {
                    TimeUnit.SECONDS.sleep(5);
                    object = exchanger.exchange(5);
                    money -= 5;
                    System.out.println("DrugUser: Giving $5, getting " + object + ", left " + money);
                }
                object = exchanger.exchange("NARC");
                System.out.println("DrugUser: Giving NARC, getting " + object);
                object = exchanger.exchange(100, 3, TimeUnit.SECONDS);
                System.out.println("DrugUser: Giving $100, getting " + object);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (TimeoutException e) {
                e.printStackTrace();
                System.out.println("DrugUser: DrugDealer is gone...");
            }
        }
    }

    public void drugDealing() {
        ExecutorService executorService = Executors.newCachedThreadPool();
        executorService.submit(new Thread(new DrugDealer("BLUE CHERRY", 0)));
        executorService.submit(new Thread(new DrugUser(10)));
        executorService.shutdown();
    }

    public static void main(String[] args) {
        MethBooth methBooth = new MethBooth(new Exchanger<Object>());
        methBooth.drugDealing();
    }
}

參考:《Java併發編程實戰》;

Java併發包concurrent——ConcurrentHashMap

ArrayList和CopyOnWriteArrayList解讀 java 併發隊列 BlockingQueue

阻塞隊列之六:LinkedBlockingDeque

Java 併發編程系列之閉鎖(CountDownLatch)

Java併發編程筆記之 CountDownLatch閉鎖的源碼分析

Java進階之FutureTask的用法及解析

Java多線程之Semaphore的使用(五)

Java併發33:Semaphore基本方法與應用場景實例

JUC回顧之-CyclicBarrier底層實現和原理

多線程編程的常用類(CountDownLatch, Semaphore, CyclicBarrier 和 Exchanger)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章