Статьи

Мелкозернистый параллелизм с классом полосатой гуавы

В этом посте будет рассказано, как использовать класс Striped из Guava для достижения более детального параллелизма. ConcurrentHashMap использует подход с чередованием блокировок для повышения параллелизма, а класс Striped расширяет этот принцип, предоставляя нам возможность иметь чередующиеся блокировки , ReadWriteLocks и семафоры . При доступе к объекту или структуре данных, такой как Array или HashMap, мы обычно синхронизируем весь объект или структуру данных, но всегда ли это необходимо? В некоторых случаях ответ «да», но могут быть случаи, когда мы не достигаем такого уровня блокирования курса и можем улучшить производительность приложения, используя более точный подход.

Вариант использования для полосатой блокировки / семафоров

Рекомендуется всегда синхронизировать наименьшую возможную часть кода. Другими словами, мы хотим синхронизировать только те части кода, которые меняют общие данные. Чередование делает этот шаг еще дальше, позволяя нам сократить синхронизацию за счет отдельных задач . Например, допустим, у нас есть ArrayList URL-адресов, представляющих удаленные ресурсы, и мы хотим ограничить общее количество потоков, обращающихся к этим ресурсам в любой момент времени. Ограничение доступа для N потоков является естественным соответствием для объекта java.util.concurrent.Semaphore . Но проблема ограничения доступа к нашему ArrayList становится узким местом. Вот где помогает класс Striped . Что мы действительно хотим сделать, так это ограничить доступ к ресурсам, а не к контейнеру URL. Предполагая, что каждый из них отличается, мы используем «чередующийся» подход, при котором доступ к каждому URL-адресу ограничен N потоками одновременно, что увеличивает пропускную способность и быстродействие приложения.

Создание Полосатых Замков / Семафоров

Чтобы создать экземпляр Striped, мы просто используем один из методов статического фактора, доступных в классе Striped . Например, вот как мы должны создать экземпляр чередующегося ReadWriteLock :

1
Striped<ReadWriteLock> rwLockStripes = Striped.readWriteLock(10);

В приведенном выше примере мы создали экземпляр Striped котором охотно создаются отдельные «полосы», сильные ссылки. В результате блокировки / семафоры не будут собираться мусором, если не удален сам объект Striped. Тем не менее, класс Striped дает нам еще один вариант, когда речь идет о создании замков / семафоров:

1
2
3
int permits = 5;
int numberStripes = 10;
Striped<Semaphore> lazyWeakSemaphore = Striped.lazyWeakSemaphore(numberStripes,permits);

В этом примере экземпляры Semaphore будут лениво создаваться и оборачиваться в WeakReferences таким образом, сразу же доступными для сборки мусора, если другой объект не удерживает их.

Доступ / Использование Полосатых Замков / Семафоров

Чтобы использовать Striped ReadWriteLock созданный ранее, мы делаем следующее:

1
2
3
4
5
6
7
8
String key = "taskA";
ReadWriteLock rwLock = rwLockStripes.get(key);
try{
     rwLock.lock();
     .....
}finally{
     rwLock.unLock();
}

При вызове метода Striped.get нам возвращается экземпляр ReadWriteLock который соответствует данному ключу объекта. Ниже мы приведем более подробные примеры с использованием класса Striped.

Блокировка / Семафор Гарантия поиска

Когда дело доходит до извлечения экземпляров блокировки / семафора, класс Striped гарантирует, когда objectA равняется objectB (при условии, что правильно реализованные методы equals и hashcode) вызов метода Striped.get вернет тот же экземпляр блокировки / семафора. Но обратное не гарантируется, то есть, когда objectA не равен ObjectB, мы все равно можем получить блокировки / семафоры, которые являются одной и той же ссылкой. Согласно Javadoc для класса Striped, указание меньшего числа полос увеличило вероятность получения одинаковых ссылок для двух разных ключей.

Пример

Давайте рассмотрим очень простой пример, основанный на сценарии, который мы изложили во введении. Мы создали класс ConcurrentWorker котором мы хотим ограничить одновременный доступ к некоторому ресурсу. Но давайте предположим, что существует некоторое количество отдельных ресурсов, поэтому в идеале мы хотим ограничить доступ для каждого ресурса, а не просто ограничить ConcurrentWorker .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class ConcurrentWorker {
 
    private Striped<Semaphore> stripedSemaphores = Striped.semaphore(10,3);
    private Semaphore semaphore = new Semaphore(3);
 
    public void stripedConcurrentAccess(String url) throws Exception{
       Semaphore stripedSemaphore  = stripedSemaphores.get(url);
        stripedSemaphore.acquire();
       try{
           //Access restricted resource here
           Thread.sleep(25);
       }finally{
           stripedSemaphore.release();
       }
    }
 
    public void nonStripedConcurrentAccess(String url) throws Exception{
        semaphore.acquire();
        try{
           //Access restricted resource here
            Thread.sleep(25);
        }finally{
            semaphore.release();
        }
    }
}

В этом примере у нас есть два метода ConcurrentWorker.stripedConcurrentAccess и ConcurrentWorker.nonStripedConcurrentAccess поэтому мы можем сделать простое сравнение. В обоих случаях мы моделируем работу, вызывая Thread.sleep течение 25 миллисекунд. Мы создали один экземпляр Semaphore экземпляр Striped который содержит 10 активно созданных объектов Semaphore ссылками. В обоих случаях мы указали 3 разрешения, поэтому в любое время 3 потока будут иметь доступ к нашему «ресурсу». Вот простой класс драйвера, используемый для измерения пропускной способности между двумя подходами.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
public class StripedExampleDriver {
 
    private ExecutorService executorService = Executors.newCachedThreadPool();
    private int numberThreads = 300;
    private CountDownLatch startSignal = new CountDownLatch(1);
    private CountDownLatch endSignal = new CountDownLatch(numberThreads);
    private Stopwatch stopwatch = Stopwatch.createUnstarted();
    private ConcurrentWorker worker = new ConcurrentWorker();
    private static final boolean USE_STRIPES = true;
    private static final boolean NO_STRIPES = false;
    private static final int POSSIBLE_TASKS_PER_THREAD = 10;
    private List<String> data = Lists.newArrayList();
 
    public static void main(String[] args) throws Exception {
        StripedExampleDriver driver = new StripedExampleDriver();
        driver.createData();
        driver.runStripedExample();
        driver.reset();
        driver.runNonStripedExample();
        driver.shutdown();
    }
 
    private void runStripedExample() throws InterruptedException {
        runExample(worker, USE_STRIPES, "Striped work");
    }
 
    private void runNonStripedExample() throws InterruptedException {
        runExample(worker, NO_STRIPES, "Non-Striped work");
    }
 
    private void runExample(final ConcurrentWorker worker, final boolean isStriped, String type) throws InterruptedException {
        for (int i = 0; i < numberThreads; i++) {
            final String value = getValue(i % POSSIBLE_TASKS_PER_THREAD);
            executorService.submit(new Callable<Void>() {
                @Override
                public Void call() throws Exception {
                    startSignal.await();
                    if (isStriped) {
                        worker.stripedConcurrentAccess(value);
                    } else {
                        worker.nonStripedConcurrentAccess(value);
                    }
                    endSignal.countDown();
                    return null;
                }
            });
        }
        stopwatch.start();
        startSignal.countDown();
        endSignal.await();
        stopwatch.stop();
        System.out.println("Time for" + type + " work [" + stopwatch.elapsed(TimeUnit.MILLISECONDS) + "] millis");
    }
  //details left out for clarity

Мы взяли 300 потоков и вызвали оба метода за два прохода и использовали класс StopWach для записи времени.

Полученные результаты

Как и следовало ожидать, полосатая версия сработала намного лучше. Вот вывод консоли из одного из тестовых прогонов:

1
2
Time forStriped work work [261] millis
Time forNon-Striped work work [2596] millis

Хотя класс Striped не подходит для каждой ситуации, он действительно пригодится, когда вам нужен параллелизм вокруг отдельных данных. Спасибо за ваше время.

Ресурсы