JavaRush /Java блог /Архив info.javarush /Параллелизм в Java. Учебник – потокобезопасные конструкци...
0xFF
9 уровень
Донецк

Параллелизм в Java. Учебник – потокобезопасные конструкции.

Статья из группы Архив info.javarush
После рассмотрения основных рисков при работе параллельных программ (таких как атомарность или видимость), мы посмотрим на некоторые конструкции классов, которые помогут нам предотвратить вышеупомянутые ошибки. Некоторые из этих конструкций создают потокобезопасные объекты, что позволяет нам безопасно делиться ими между потоками. В качестве примера мы рассмотрим неизменяемые и stateless объекты. Другие представления будут предотвращать модифицирование данных разными потоками, таких как локальные переменные потока. Вы можете посмотреть все исходные коды на гитхабе. 1. Неизменяемые объекты Неизменяемы объекты имеют состояние (имеют данные, которые представляют состояние объекта), но оно задано во время создания в конструкторе, как только экземпляр объекта был создан и состояние не может быть изменено. Хотя потоки могут чередоваться, объект все равно имеет одно возможное состояние. Поскольку все поля доступны только для чтения, ни один поток не сможет изменить данные объекта. Из-за этого неизменяемый поток по своей сути потокобезопасный. Класс Product демонстрирует неизменяемый класс. Он заполняет все свои поля в конструкторе и ни одно из них не изменяется: 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; } } В некоторых случаях будет недостаточно сделать поля final. Например, класс MutableProduct не является неизменяемым, хотя все поля final: public final class MutableProduct { private final String id; private final String name; private final double price; private final List 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 getCategories() { return this.categories; } public List getCategoriesUnmodifiable() { return Collections.unmodifiableList(categories); } public String toString() { return new StringBuilder(this.id).append("-").append(this.name) .append(" (").append(this.price).append(")").toString(); } } Почему класс выше не неизменяемый? Причина в том, что мы позволяем получить ссылку из класса. Поле ‘categories’ – это изменяемая ссылка, так что после получения ее клиент может изменять ее. Для того, чтобы показать это, рассмотрим следующую программу: 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); } И вывод в консоль: Product categories A B C Modified Product categories B C Поскольку поле ‘categories’ изменяемое и было получено из объекта, клиент изменил этот список. Объект, который должен быть неизменным, был изменен, это приводит к новому состоянию. Если вы хотите представить содержимое списка, вы можете использовать неизменяемое представление списка: public List getCategoriesUnmodifiable() { return Collections.unmodifiableList(categories); } 2. Stateless объекты Stateless объекты похожи на неизменяемые объекты, но в этом случае они не имеют состояния, даже одного. Когда объект является stateless-объектом, он не должен сохранять какие-либо данные между вызовами. Поскольку не существует ни одного состояния, то ни один поток не может повлиять на результат другого потока вызывая методы объекта. По этой причине stateless объекты по своей сути потокобезопасны. Класс ProductHandler является примером этого типа объектов. Он содержит несколько операций над объектами Product, и он не хранит никаких данных между вызовами. Результат операции не зависит от предыдущих вызовов или любых хранимых данных: 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 cart) { double total = 0.0; for (Product p : cart.toArray(new Product[0])) total += p.getPrice(); return total; } } В методе sumCart ProductHandler преобразует список Product в массив для использования в цикле for-each для перебора всех элементов. Список итераторов не является потокобезопасным и может вызвать исключение ConcurrentModificationException, если будут изменения во время итерации. В зависимости от ваших потребностей вы можете выбрать другую стратегию. 3. Локальные переменные потока Локальные переменные потока – это те переменные, которые определены в рамках потока. Никакие другие потоки не видят их и не изменят их. Первый тип – это локальные переменные. В приведенном ниже примере переменная total хранится в стеке потока: public double sumCart(List cart) { double total = 0.0; for (Product p : cart.toArray(new Product[0])) total += p.getPrice(); return total; } Просто имейте в виду, что если вместо примитивной переменной вы определите ссылку и вернете ее, она покинет свои границы. Вы можете не знать где возвращенная ссылка была создана. Код, который вызывает метод sumCart мог хранить его в статическом поле и позволить ему быть доступным разным потокам. Второй тип – это класс ThreadLocal. Этот класс обеспечивает независимое хранение для каждого потока. Значения, сохраненные в ThreadLocal, доступны любому коду в том же потоке. Класс ClientRequestId показывает пример использования ThreadLocale класса: public class ClientRequestId { private static final ThreadLocal id = new ThreadLocal() { @Override protected String initialValue() { return UUID.randomUUID().toString(); } }; public static String get() { return id.get(); } } Класс ProductHandlerThreadLocal использует ClientRequestId, чтобы вернуть тот же сгенерированный идентификатор в том же потоке: public class ProductHandlerThreadLocal { //Same methods as in ProductHandler class public String generateOrderId() { return ClientRequestId.get(); } } При выполнении метода main вывод на консоль будет показывать различные идентификаторы для каждого потока. Например: T1 - 23dccaa2-8f34-43ec-bbfa-01cec5df3258 T2 - 936d0d9d-b507-46c0-a264-4b51ac3f527d T2 - 936d0d9d-b507-46c0-a264-4b51ac3f527d T3 - 126b8359-3bcc-46b9-859a-d305aff22c7e ... Если вы собираетесь использовать ThreadLocale, вы должны заботиться о некоторых рисках использования при объединении потоков (как в серверных приложениях). Вы можете получить утечки памяти или утечку информации между запросами. Я не буду сильно распространяться по этому поводу т.к. статья “Как выстрелить себе в ногу с ThreadLocale” хорошо демонстрирует как это может случиться. 4. Использование синхронизации Другой способ обеспечения потокобезопасного доступа к объектам – через синхронизацию. Если мы синхронизируем все доступы к ссылке, то только один объект поток будет обращаться к нему в данный момент времени. Мы обсудим это в будущих постах. 5. Заключение Мы рассмотрели несколько методов, позволяющих строить простые объекты, которые могут быть доступны нескольким потокам. Намного трудней предотвратить многопоточные ошибки, если объект может иметь несколько состояний. С другой стороны, если объект может иметь только одно состояние либо не иметь ни одного, мы можем не беспокоиться о доступе нескольких потоков к объекту в одно и то же время. Оригинал тут.
Комментарии (1)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
SergioShapoval Уровень 41
1 июля 2015
Особенно интересно получилось здесь:
for (Product p : cart.toArray(new Product[0])) total += p.getPrice();


Спасибо за перевод!