Статьи

Java Concurrency Tutorial — Потокобезопасные конструкции

Изучив основные риски при работе с параллельными программами (например, атомарность или видимость ), мы рассмотрим некоторые классы, которые помогут нам предотвратить вышеупомянутые ошибки. Некоторые из этих конструкций приводят к созданию потоковобезопасных объектов, что позволяет нам безопасно делить их между потоками. В качестве примера рассмотрим неизменяемые и не имеющие состояния объекты. Другие конструкции не позволят различным потокам изменять одни и те же данные, например локальные переменные потока.

Вы можете увидеть весь исходный код на github .

1. Неизменные предметы

Неизменяемые объекты имеют состояние (имеют данные, которые представляют состояние объекта), но оно строится на основе конструкции, и как только объект создается, состояние не может быть изменено.

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

Продукт показывает пример неизменяемого класса. Он строит все свои данные во время построения, и ни одно из его полей не может быть изменено:

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
public final class Product {
    private final String id;
    private final String name;
    private final double price;
     
    public Product(String id, String name, double price) {
        this.id = id;
        this.name = name;
        this.price = price;
    }
     
    public String getId() {
        return this.id;
    }
     
    public String getName() {
        return this.name;
    }
     
    public double getPrice() {
        return this.price;
    }
     
    public String toString() {
        return new StringBuilder(this.id).append("-").append(this.name)
            .append(" (").append(this.price).append(")").toString();
    }
     
    public boolean equals(Object x) {
        if (this == x) return true;
        if (x == null) return false;
        if (this.getClass() != x.getClass()) return false;
        Product that = (Product) x;
        if (!this.id.equals(that.id)) return false;
        if (!this.name.equals(that.name)) return false;
        if (this.price != that.price) return false;
         
        return true;
    }
     
    public int hashCode() {
        int hash = 17;
        hash = 31 * hash + this.getId().hashCode();
        hash = 31 * hash + this.getName().hashCode();
        hash = 31 * hash + ((Double) this.getPrice()).hashCode();
         
        return hash;
    }
}

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

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
public final class MutableProduct {
    private final String id;
    private final String name;
    private final double price;
    private final List<String> categories = new ArrayList<>();
     
    public MutableProduct(String id, String name, double price) {
        this.id = id;
        this.name = name;
        this.price = price;
        this.categories.add("A");
        this.categories.add("B");
        this.categories.add("C");
    }
     
    public String getId() {
        return this.id;
    }
     
    public String getName() {
        return this.name;
    }
     
    public double getPrice() {
        return this.price;
    }
     
    public List<String> getCategories() {
        return this.categories;
    }
     
    public List<String> getCategoriesUnmodifiable() {
        return Collections.unmodifiableList(categories);
    }
     
    public String toString() {
        return new StringBuilder(this.id).append("-").append(this.name)
            .append(" (").append(this.price).append(")").toString();
    }
}

Почему вышеупомянутый класс не является неизменным? Причина в том, что мы позволяем ссылке выйти из области видимости своего класса. Поле ‘ Categories ‘ является изменяемой ссылкой, поэтому после ее возврата клиент может ее изменить. Чтобы показать это, рассмотрим следующую программу:

01
02
03
04
05
06
07
08
09
10
public static void main(String[] args) {
    MutableProduct p = new MutableProduct("1", "a product", 43.00);
     
    System.out.println("Product categories");
    for (String c : p.getCategories()) System.out.println(c);
     
    p.getCategories().remove(0);
    System.out.println("\nModified Product categories");
    for (String c : p.getCategories()) System.out.println(c);
}

И вывод консоли:

1
2
3
4
5
6
7
Product categories
 
A
 
B
 
C
1
2
3
4
5
Modified Product categories
 
B
 
C

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

Если вы хотите раскрыть содержимое списка, вы можете использовать неизменяемое представление списка:

1
2
3
public List<String> getCategoriesUnmodifiable() {
    return Collections.unmodifiableList(categories);
}

2. Объекты без гражданства

Объекты без состояния похожи на неизменяемые объекты, но в этом случае у них нет состояния, даже одного. Когда объект не имеет состояния, ему не нужно запоминать какие-либо данные между вызовами.

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

ProductHandler является примером этого типа объектов. Он содержит несколько операций над объектами Product и не сохраняет никаких данных между вызовами. Результат операции не зависит от предыдущих вызовов или любых сохраненных данных:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
public class ProductHandler {
    private static final int DISCOUNT = 90;
     
    public Product applyDiscount(Product p) {
        double finalPrice = p.getPrice() * DISCOUNT / 100;
         
        return new Product(p.getId(), p.getName(), finalPrice);
    }
     
    public double sumCart(List<Product> cart) {
        double total = 0.0;
        for (Product p : cart.toArray(new Product[0])) total += p.getPrice();
         
        return total;
    }
}

В своем методе sumCart ProductHandler преобразует список продуктов в массив, поскольку цикл for-each использует итератор для внутренней итерации своих элементов. Итераторы списка не являются поточно-ориентированными и могут генерировать исключение ConcurrentModificationException, если оно изменено во время итерации В зависимости от ваших потребностей, вы можете выбрать другую стратегию .

3. Потоково-локальные переменные

Локальные переменные потока — это переменные, определенные в области видимости потока. Никакие другие темы не увидят и не изменят их.

Первый тип — это локальные переменные. В следующем примере переменная total хранится в стеке потока:

1
2
3
4
5
6
public double sumCart(List<Product> cart) {
    double total = 0.0;
    for (Product p : cart.toArray(new Product[0])) total += p.getPrice();
     
    return total;
}

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

Второй тип — класс ThreadLocal . Этот класс предоставляет хранилище, независимое для каждого потока. Значения, хранящиеся в экземпляре ThreadLocal, доступны из любого кода в том же потоке.

Класс ClientRequestId показывает пример использования ThreadLocal:

01
02
03
04
05
06
07
08
09
10
11
12
public class ClientRequestId {
    private static final ThreadLocal<String> id = new ThreadLocal<String>() {
        @Override
        protected String initialValue() {
            return UUID.randomUUID().toString();
        }
    };
     
    public static String get() {
        return id.get();
    }
}

Класс ProductHandlerThreadLocal использует ClientRequestId для возврата того же сгенерированного идентификатора в том же потоке:

1
2
3
4
5
6
7
public class ProductHandlerThreadLocal {
    //Same methods as in ProductHandler class
     
    public String generateOrderId() {
        return ClientRequestId.get();
    }
}

Если вы выполните метод main, вывод консоли будет показывать разные идентификаторы для каждого потока. Например:

1
2
3
4
5
6
7
8
9
T1 - 23dccaa2-8f34-43ec-bbfa-01cec5df3258
 
T2 - 936d0d9d-b507-46c0-a264-4b51ac3f527d
 
T2 - 936d0d9d-b507-46c0-a264-4b51ac3f527d
 
T3 - 126b8359-3bcc-46b9-859a-d305aff22c7e
 
...

Если вы собираетесь использовать ThreadLocal, вам следует позаботиться о некоторых рисках его использования при объединении потоков (как на серверах приложений). Вы можете столкнуться с утечками памяти или утечкой информации между запросами. Я не буду углубляться в эту тему, так как пост « Как выстрелить себе в ногу с ThreadLocals» хорошо объясняет, как это может произойти.

4. Использование синхронизации

Другой способ обеспечить потокобезопасный доступ к объектам — через синхронизацию. Если мы синхронизируем все обращения к ссылке, только один поток получит к ней доступ в данный момент времени. Мы обсудим это в следующих постах.

5. Заключение

Мы видели несколько методов, которые помогают нам создавать более простые объекты, которые можно безопасно разделить между потоками. Намного сложнее предотвратить параллельные ошибки, если объект может иметь несколько состояний. С другой стороны, если у объекта может быть только одно состояние или нет, нам не нужно будет беспокоиться о том, что разные потоки будут получать к нему доступ одновременно.