Содержание


Пять секретов... многопоточного Java-программирования

О тонкостях высокопроизводительной многопоточной обработки данных

Comments

Немногие Java™-разработчики могут позволить себе игнорировать многопоточное программирование и поддерживающие его библиотеки платформы Java, но еще меньше тех, кто располагает временем для глубинного изучения потоков. Вместо этого мы изучаем их спонтанно, добавляя в свой инструментарий новые рекомендации и приемы, когда нуждаемся в них. Это не лучший путь, хотя он и позволяет построить и запустить достойное приложение. Понимание потоковых механизмов компилятора Java и JVM поможет вам написать более эффективный, качественный и быстродействующий код Java.

В этой статье из цикла Пять секретов я расскажу о некоторых тонких аспектах многопоточного программирования с применением синхронизированных методов, volatile-переменных и атомарных классов. В частности, мы обсудим взаимодействие некоторых из этих конструкций с JVM и компилятором Java и влияние различных взаимодействий на производительность Java-приложения.

1. Синхронизированный метод или синхронизированный блок?

Развить навыки по этой теме

Этот материал — часть knowledge path для развития ваших навыков. Смотри Стать Java-программистом

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

Когда JVM выполняет синхронизированный метод, исполняемый поток определяет, что в структуре method_info этого метода установлен флаг ACC_SYNCHRONIZED, а затем автоматически блокирует объект, вызывает метод и разблокирует объект. В случае исключительной ситуации поток автоматически снимает блокировку.

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

Листинг 1. Два подхода к синхронизации
package com.geekcap;

public class SynchronizationExample {
    private int i;

    public synchronized int synchronizedMethodGet() {
        return i;
    }

    public int synchronizedBlockGet() {
        synchronized( this ) {
            return i;
        }
    }
}

Метод synchronizedMethodGet() генерирует следующий байт-код:

	0:	aload_0
	1:	getfield
	2:	nop
	3:	iconst_m1
	4:	ireturn

А вот байт-код метода synchronizedBlockGet():

	0:	aload_0
	1:	dup
	2:	astore_1
	3:	monitorenter
	4:	aload_0
	5:	getfield
	6:	nop
	7:	iconst_m1
	8:	aload_1
	9:	monitorexit
	10:	ireturn
	11:	astore_2
	12:	aload_1
	13:	monitorexit
	14:	aload_2
	15:	athrow

Для создания синхронизированного блока понадобилось 16 строк байт-кода, тогда как для синхронизации метода достаточно пяти.

2. Переменные ThreadLocal

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

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

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

Использование ThreadLocal имеет смысл, когда нужно хранить экземпляры переменных для каждого потока.

3. Volatile-переменные

По моей оценке, примерно половине всех Java-разработчиков известно о наличии в языке Java ключевого слова volatile. Из них лишь около 10% знают, что оно означает, и еще меньше ― как его эффективно использовать. Вкратце, определение переменной с ключевым словом volatile означает, что значение этой переменной может изменяться другими потоками. Чтобы как следует понять, что делает ключевое слово volatile, полезно разобраться, как потоки обрабатывают обычные переменные.

В целях повышения производительности спецификация языка Java допускает сохранение в JRE локальной копии переменной для каждого потока, который на нее ссылается. Такие «локальные» копии переменных напоминают кэш и помогают потоку избежать обращения к главной памяти каждый раз, когда требуется получить значение переменной.

Но давайте посмотрим, что происходит в следующем сценарии: запускаются два потока, и один из них считывает переменную A как 5, а второй ― как 10. Если значение переменной А изменилось с 5 на 10, то первый поток не узнает об изменении и будет хранить неправильное значение A. Однако если переменная А помечена как volatile, то когда бы поток не считывал значение A, он будет обращаться к главной копии A и считывать ее текущее значение.

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

4. Volatile- или синхронизированные переменные?

Если переменная объявлена как volatile, это означает, что она может изменяться разными потоками. Естественно ожидать, что JRE обеспечит ту или иную форму синхронизации таких volatile-переменных. JRE действительно неявно обеспечивает синхронизацию при доступе к volatile-переменным, но с одной очень большой оговоркой: чтение volatile-переменной и запись в volatile-переменную синхронизированы, а неатомарные операции ― нет.

Это означает, что следующий код не является потокобезопасным:

myVolatileVar++;

Предыдущий оператор можно записать и так:

int temp = 0;
synchronize( myVolatileVar ) {
  temp = myVolatileVar;
}

temp++;

synchronize( myVolatileVar ) {
  myVolatileVar = temp;
}

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

5. Атомарные корректоры полей

При увеличении или уменьшении значения примитива в многопоточной среде гораздо выгоднее использовать один из новых атомарных классов из пакета java.util.concurrent.atomic, чем писать свой собственный синхронизированный блок кода. Атомарные классы гарантируют выполнение определенных операций, таких как увеличение и уменьшение, обновление или добавление значения, потокобезопасным способом. В перечень атомарных классов входят классы AtomicInteger, AtomicBoolean, AtomicLong, AtomicIntegerArray и т.п.

Проблема использования атомарных классов состоит в том, что все операции класса, включая get, set и семейство операций get-set, оказываются атомарными. Это означает, что операции read и write, которые не изменяют значения атомарной переменной, ― это не просто важные, а синхронизированные операции read-update-write. Если требуется более детальное управление развертыванием синхронизированного кода, то обходной путь заключается в использовании атомарного корректора полей.

Использование атомарных обновлений

Атомарные корректоры полей, такие как AtomicIntegerFieldUpdater, AtomicLongFieldUpdater и AtomicReferenceFieldUpdater, по сути, представляют собой оболочку volatile-поля. Они используются внутри библиотек Java-классов. Эти корректоры не нашли широкого применения в коде приложений, но избегать их нет никаких причин.

В листинге 2 приведен пример класса, использующего атомарные корректоры для изменения названия книги, которую кто-то читает.

Листинг 2. Класс Book
package com.geeckap.atomicexample;

public class Book
{
    private String name;

    public Book()
    {
    }

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

    public String getName()
    {
        return name;
    }

    public void setName( String name )
    {
        this.name = name;
    }
}

Класс Book - это простой объект Java (POJO) с единственным полем: name.

Листинг 3. Класса MyObject
package com.geeckap.atomicexample;

import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;

/**
 *
 * @author shaines
 */
public class MyObject
{
    private volatile Book whatImReading;

    private static final AtomicReferenceFieldUpdater<MyObject,Book> updater =
            AtomicReferenceFieldUpdater.newUpdater( 
                       MyObject.class, Book.class, "whatImReading" );

    public Book getWhatImReading()
    {
        return whatImReading;
    }

    public void setWhatImReading( Book whatImReading )
    {
        //this.whatImReading = whatImReading;
        updater.compareAndSet( this, this.whatImReading, whatImReading );
    }
}

Класс MyObject в листинге 3 предоставляет свое свойство whatAmIReading обычным образом, с помощью методов get и set, но метод set делает нечто особенное. Вместо того чтобы просто присвоить свою внутреннюю ссылку Book указанному объекту Book (что достигается с помощью кода, закомментированного в листинге 3), он использует AtomicReferenceFieldUpdater.

AtomicReferenceFieldUpdater

В Javadoc класс AtomicReferenceFieldUpdater определяется следующим образом:

Основанная на отражении (reflection-based) утилита, которая обеспечивает атомарное обновление указанных опорных volatile-полей указанных классов. Этот класс предназначен для использования в атомарных структурах данных, где несколько опорных полей одного и того же узла независимо подлежат атомарному обновлению.

В листинге 3 класс AtomicReferenceFieldUpdater создается с помощью вызова его статического метода newUpdater, принимающего три параметра:

  • класс объекта, содержащий поле (в данном случае, MyObject);
  • класс объекта, подлежащий атомарному обновлению (в данном случае, Book);
  • имя поля, подлежащего атомарному обновлению.

Реальная выгода здесь заключается в том, что метод getWhatImReading выполняется без всякой синхронизации, в то время как setWhatImReading выполняется как атомарная операция.

В листинге 4 показано, как использовать метод setWhatImReading() с гарантией правильного изменения значения.

Листинг 4. Пример атомарного обновления
package com.geeckap.atomicexample;

import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;

public class AtomicExampleTest
{
    private MyObject obj;

    @Before
    public void setUp()
    {
        obj = new MyObject();
        obj.setWhatImReading( new Book( "Java 2 From Scratch" ) );
    }

    @Test
    public void testUpdate()
    {
        obj.setWhatImReading( new Book( 
                "Pro Java EE 5 Performance Management and Optimization" ) );
        Assert.assertEquals( "Incorrect book name", 
                "Pro Java EE 5 Performance Management and Optimization", 
                obj.getWhatImReading().getName() );
    }

}

Подробнее об атомарных классах см. в разделе Ресурсы.

Заключение

Многопоточное программирование всегда сложно, но по мере развития платформы Java некоторые из его задач упрощаются. В этой статье я раскрыл пять секретов создания многопоточных приложений на платформе Java, включая разницу между методами синхронизации и синхронизированными блоками кода, важность использования переменных ThreadLocal, сохраняющихся в каждом потоке, малоизвестное ключевое слово volatile (и опасность опоры на volatile при решении задач синхронизации) и краткий экскурс в тонкости атомарных классов. Подробнее см. в разделе Ресурсы.


Ресурсы для скачивания


Похожие темы

  • Оригинал статьи: 5 things you didn't know about ... multithreaded Java programming.
  • Пять секретов...: цикл статей, содержащих полезные советы по Java-программированию.
  • Java Concurrency in Practice (Brian Goetz, et. al. Addison-Wesley, 2006): удивительная способность Брайана объяснять сложные концепции простым языком делает эту книгу необходимой в библиотеке любого Java-программиста.
  • Code Tracing (Steven Haines, InformIT, август 2010 г.): подробнее о трассировке кода с помощью переменных ThreadLocal.
  • Java bytecode: Understanding bytecode makes you a better programmer (Peter Haggar, developerWorks, июль 2001 г.): введение в малоизвестные области байткода, включая приведенный выше пример, иллюстрирующий разницу между синхронизированными методами и синхронизированными блоками.
  • Java theory and practice: Going atomic (Brian Goetz, developerWorks, ноябрь 2004 г.): о том, как атомарные классы позволяют разрабатывать высокомасштабируемые неблокирующие алгоритмы на языке Java.
  • Java theory and practice: Concurrency made simple (sort of) (Brian Goetz, developerWorks, ноябрь 2002 г.): экскурс в пакет java.util.concurrent.
  • 5 things you didn't know about ... java.util.concurrent, Part 1 (Ted Neward, developerWorks, май 2010 г.): о пяти классах параллельных коллекций, которые модифицируют стандартные классы коллекций для нужд параллельного программирования.

Комментарии

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

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Технология Java
ArticleID=843334
ArticleTitle=Пять секретов... многопоточного Java-программирования
publish-date=10292012