2 июня 2013 г.

Использование защелок (java.util.concurrent.CountDownLatch)

Защелки необходимы тогда, когда необходимо ускорить выполнение последовательных независимых задач за счет их распараллеливания. Раз уж я остановился на строительных примерах, то ситуацией их жизни может служить приготовление бетона.
В моем случае бетон состоит из 4 составляющих: песок, цемент, щебень и вода. Допустим, что все материалы для приготовления бетона находятся на разном расстоянии от емкости, где его приготавливают.
При последовательном выполнении данных операций нам небходима сумма времени на выполнение каждой операции. Если у нас в распоряжении есть 4 работника, то время наполнения емкости, готовой к замесу, становится равно времени выполнения максимальной операции.
Дополнительным условием для начала замеса является полное заполнение емкости.
Рассмотрим на примере кода.

1. Главный класс, где создаются работники и защелка на 4 события. Этот класс ожидает завершения работы всех работников (latch.await())
import com.google.common.base.Stopwatch;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

public class Main {

    public static void main(String[] args) throws InterruptedException {
        Stopwatch sw = new Stopwatch();
        sw.start();
        
        CountDownLatch latch = new CountDownLatch(4);
        
        new Worker(latch,"Sand").start();
        new Worker(latch,"Cement").start();
        new Worker(latch,"Water").start();
        new Worker(latch,"Breakstone").start();
        
        System.out.println("Waiting for all workers");
        latch.await();
        System.out.println("All workers finished. Now we can shake.");
        
        sw.stop();
        System.out.println("Program time: " + sw.elapsed(TimeUnit.MILLISECONDS) + " ms");
    }
}

2. Класс для генерации случайного времени задержки  (время условно определяет удаленность материала от емкости)
public class RandomGenerator {

    public static long getRandom(long min, long max) {
        return min + (long)(Math.random() * (max - min + 1));
    }
}

3.Класс работника, который выполняет задачу и извещает о выполнении
import com.google.common.base.Stopwatch;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

public class Worker extends Thread {

    private CountDownLatch _cdl;
    private String _name;

    public Worker (CountDownLatch cdl, String name) {
        _cdl = cdl;
        _name = name;
    }

    @Override
    public void run() {
        Stopwatch sw = new Stopwatch();
        sw.start();

        System.out.println(_name + " working...");
        try {
            Thread.sleep(RandomGenerator.getRandom(500, 1000));
        } catch (InterruptedException e) {
            e.printStackTrace(System.err);
        }
        _cdl.countDown();

        sw.stop();
        System.out.println(_name + " time: " + sw.elapsed(TimeUnit.MILLISECONDS) + " ms");
    }
}

Результат выполнения программы
Sand working...
Breakstone working...
Waiting for all workers
Water working...
Cement working...
Water time: 720 ms
Sand time: 887 ms
Cement time: 927 ms
Breakstone time: 959 ms
All workers finished. Now we can shake.
Program time: 960 ms

По результатам работы видно, что общее время выполнения программы 960 мс, а при последовательном выполнении мы б получили время, равное сумме: 720 + 887 + 927 + 959 = 3493 мс.
В данном примере использована библиотека guava для подсчета времени выполнения метода.
Примером для параллельного выполнения из мира Java может служить получение данных о клиенте. На входе в программу мы имеем некий userId клиента и из разных источников запрашиваем данные. Например, по http-запросу №1 мы получаем фамилию и имя клиента, запросом в базу данных год рождения и запросом №2 получаем список банковских счетов.

Использование семафоров (java.util.concurrent.Semaphore)

Семафоры используются для того, чтоб перед использованием ресурса проверить его доступность.
Примером из жизни может служить тележка (общий ресурс) и два работника (потоки java). Один работник, к примеру, наполняет тележку песком. В это время второй работник, который перевозит груз и затем разгружает, не может взять тележку и отвезти ее.
В то же время, если второй работник увез тележку, то первый работник не должен ничего наполнять.
Ниже приведен небольшой пример программы из 3 классов.

1. Главнй класс
import java.util.concurrent.Semaphore;

public class SemaphoreExample {
    public static void main(String[] args) {
        Semaphore semaphore = new Semaphore(1);
        new Worker(semaphore, "Adder", true).start();
        new Worker(semaphore, "Reducer", false).start();
    }
} 

2. Класс тележки
public class Cart {
    private static int weight = 0;

    public static void addWeight(){
        weight--;
    }

    public static void reduceWeight(){
        weight++;
    }

    public static int getWaight(){
        return weight;
    }
}

3. Класс работника (boolean переменная определяет это работник по загрузке тележки либо по разгрузке)
import java.util.concurrent.Semaphore;

public class Worker extends Thread {

    private Semaphore semaphore;
    private String workerName;
    private boolean isAdder;
    
    public Worker(Semaphore semaphore, String workerName, boolean isAdder) {
        this.semaphore = semaphore;
        this.workerName = workerName;
        this.isAdder = isAdder;
    }

    @Override
    public void run() {
        System.out.println(workerName + " started working...");
        try {
            System.out.println(workerName + " waiting for cart...");
            semaphore.acquire();
            System.out.println(workerName + " got access to cart...");
            for (int i = 0 ; i < 10 ; i++) {
                if (isAdder)
                    Cart.reduceWeight();
                else
                    Cart.addWeight();
                
                System.out.println(workerName + " changed weight to: " + Cart.getWaight());
                Thread.sleep(10L);
            }
            System.out.println(workerName + " finished working with cart...");
        } catch (Exception e) {
            e.printStackTrace(System.err);
        } finally {
            semaphore.release(); 
        }
    }
}

Данный пример не гарантирует, что наполнение тележки произойдет раньше, чем ее разгрузка. Если есть такая необходимость, то одни из способов - это поставить задержку после создания первого потока Worker.

Результат работы программы:
Adder started working...
Adder waiting for cart...
Adder got access to cart...
Adder changed weight to: 1
Reducer started working...
Reducer waiting for cart...
Adder changed weight to: 2
Adder changed weight to: 3
Adder changed weight to: 4
Adder changed weight to: 5
Adder changed weight to: 6
Adder changed weight to: 7
Adder changed weight to: 8
Adder changed weight to: 9
Adder changed weight to: 10
Adder finished working with cart...
Reducer got access to cart...
Reducer changed weight to: 9
Reducer changed weight to: 8
Reducer changed weight to: 7
Reducer changed weight to: 6
Reducer changed weight to: 5
Reducer changed weight to: 4
Reducer changed weight to: 3
Reducer changed weight to: 2
Reducer changed weight to: 1
Reducer changed weight to: 0
Reducer finished working with cart...