27 августа 2010 г.

Immutable vs Mutable объекты

Когда я узнал, что final поля "гарантированно видимы любому потоку" после завершения инициализации, я подумал, что ведь это чего-то да стоит. В том смысле, что создание immutable объекта с final полями должно быть дороже (как минимум, в многопоточном окружении, чтобы JIT не смог это оптимизировать). Т.е. как минимум нужен же мембар по завершении конструктора. Сегодня попытался это проверить -- объявил два идентичных класса, у одного из которых все поля final, и создавал их в 150 потоков параллельно. Черта с два -- одинаково. Даже наоборот, есть слабое преимущество у immutable класса, но я бы не назвал его статистически значимым -- где-то 0.5%, слишком мало, чтобы принимать во внимание в случае микробенчмарка.

P.S. Постфактум это становится довольно очевидным. Семантика final полей обеспечивается StoreStore барьером между инициализацией полей и публикацией ссылки на созданный объект:

localRef = alloc(...);
localRef.field1 = ...;
localRef.field2 = ...;
....
StoreStore;
globallyAvailableRef = localRef;

Но на x86, где я тестировал, StoreStore барьер не нужен -- тамошняя "железная" модель памяти (TSO -- total store order) достаточно строгая сама по себе.

Более того, по словам DL в его кулинарной книге StoreStore барьеры, даже если они и требуются на какой-то архитектуре, обычно одни из самых дешевых. Так что, с высокой степенью вероятности, их влияние на производительность даже там, где они есть (и требуются) будет теряться на фоне, например, затрат на выделение памяти

Производительность

Тестировал, ради интереса, пиковую производительность разных методов доступа к разделяемому состоянию в многопоточном окружении. Участники соревнования -- sunchronized, ReentrantLock, AtomicInteger, volatile+AtomicIntegerFieldUpdater.

Результаты, в целом, предсказуемы -- атомики в 2-3 раза быстрее блокировок. Две интересные вещи:


ReentrantLock опять обогнал synchronized! Заметно -- на четверть почти. Как-то я уже обнаруживал этот феномен. До сих пор не могу понять, в чем здесь цимус -- Сановцы забили на оптимизацию "старого" способа синхронизации? -- но на ус придется намотать :)


AtomicInteger немного обогнал volatile+AtomicIntegerFieldUpdater. Чуть-чуть -- процентов на 5 в среднем. Не то, чтобы неожиданно -- накладные расходы на reflection-based доступ -- но все же приятно видеть их вживую в цифрах. Разница есть, она измерима, но по величине явно не критична. Т.е. на AtomicIntegerFieldUpdater можно закладываться как на альтернативу AtomicInteger.

22 августа 2010 г.

Lock-free BufferedQueue

Дали тестовое задание перед собеседованием -- написать буфферизированную очередь. Т.е. на вход метода append(T item) из нескольких потоков сыплются некие объекты, и, раз в какой-то интервал времени, задаваемый при создании, накопившиеся объекты сбрасываются списком в заранее заданный callback. Причем, понятно, очередь должна иметь как можно большую пропускную способность, т.е. засинхронизировать метод append -- это не выход.

Первая моя версия использовала ReadWriteLock -- неэксклюзивным ReadLock-ом защищался append, эксклюзивным WriteLock-ом -- flush. Но потом я взмедитнул, и подумал, что блокировки там вообще лишние. И получилась у меня lock-free версия вот такой:
public class LockFreeBufferedQueue<ItemType> {

private final ICallback<Collection<ItemType>> callback;
private final long delay;
private final TimeUnit timeUnit;

private final ScheduledExecutorService executor;

private final CallableVoid> flushBufferTask = new Callable<Void>() {
public Void call() {
assert ( pending.get() > 0 ) : "Call without being scheduled";
flushBuffer();
return null;
}
};

private final AtomicInteger pending = new AtomicInteger( 0 );

/**
* volatile чтобы обойтись без блокировки в методе isClosed()
*/
private volatile boolean closed = false;

private volatile ScheduledFuture<Void> scheduledTask = null;

/**
* lock-free Queue implementation
*/
private final ConcurrentLinkedQueue<ItemType> queue = new ConcurrentLinkedQueue<ItemType>();

public LockFreeBufferedQueue( final ICallback<Collection<ItemType>> callback,
final long delay,
final TimeUnit timeUnit,
final ScheduledExecutorService executor ) {        
this.callback = callback;
this.delay = delay;
this.timeUnit = timeUnit;
this.executor = executor;
}

/**
* Не специфицированно, что должен делать метод close(), поэтому мы делаем так:
* 
* Уже добавленные задачи будут выполнены немедленно, в текущем потоке, добавление
* новых приведет к IllegalStateException в методе append().
*/
public void close() {
if ( !closed ) {
closed = true;
if ( scheduledTask != null ) {
if ( scheduledTask.cancel( false ) ) {
flushBuffer();
}
scheduledTask = null;
}
//todo but if executor is external?
executor.shutdown();
}
}

/**
* @throws IllegalStateException если очередь уже закрыта вызовом close()
* @throws NullPointerException  если item == null
*/
public void append( final ItemType item ) {
if ( closed ) {
throw new IllegalStateException( "Queue was closed" );
}

queue.add( item );

if ( pending.getAndIncrement() == 0 ) {
schedule();
}
}

private void flushBuffer() {
final int size = pending.get();

assert ( !queue.isEmpty() ) : "Call with empty queue";
assert ( size > 0 ) : "Call with 0 pending";

final Collection<ItemType> buffer = new ArrayList<ItemType>( size );

for ( int i = 0; i < size; i++ ) {
buffer.add( queue.poll() );
}

if ( pending.getAndAdd( -size ) > size ) {
schedule();
}

//todo что делать с исключениями?
callback.call( buffer );
}

private void schedule() {
scheduledTask = executor.schedule( flushBufferTask, delay, timeUnit );
}

public boolean isClosed() {
return closed;
}
}
//=============================
public interface ICallback<T> {
public void call( final T arg );
}


Это первый мой опыт в самостоятельном написании lock-free алгоритмов на CAS-примитивах, поэтому написал я этот код часа за два, а медитировал над ним дня 3. Но косяков так и не нашел -- все должно работать как задумано. Увлекательное это занятие -- обдумывать корректность таких алгоритмов, мозг работает явно на повышенных оборотах

18 августа 2010 г.

Clipboard

Несколько дней уже разбираюсь с задачей экспорта картинки из Матконструктора в офисный пакет. Хочется картинку выдавать векторную, чтобы в, скажем, Word можно было ее потом масштабировать и редактировать как удобно. Собственно, экспортировать картинку в какой-нибудь векторный формат не проблема -- библиотека FreeHEP дает сразу полдюжины вариантов, от SVG до родного виндового EMF. Вопрос в том, как объяснить буферу обмена, что тот набор, казалось бы, случайных байт, что в нем лежит -- это не данные датчика случайных чисел, а-таки EMF.

Оказывается, сделать это все-таки можно. Похоже, первыми на решение набрели разработчики JFreeChart (во всяком случае, остальные источники ссылаются на них как на оригинал). Внимание, сейчас будет фокус:

//на самом деле mime-type здесь не важен, 
//можно использовать image/emf, application/emf, image/x-mgx-emf, etc
public static final DataFlavor EMF_FLAVOR= new DataFlavor("image/x-emf", "Enhanced Meta File");

static {
    // EMF graphics clipboard format
    try {
        final SystemFlavorMap sfm = (SystemFlavorMap)SystemFlavorMap.getDefaultFlavorMap();
        sfm.addUnencodedNativeForFlavor(EMF_FLAVOR, "ENHMETAFILE");//seems to be a key command!!
    } catch(Exception e) {
        e.printStackTrace();
    }
}

(источник)
Если я все правильно понял, волшебное заклинание sfm.addUnencodedNativeForFlavor(EMF_FLAVOR, "ENHMETAFILE"); объясняет джаве, что данные, маркированные как EMF_FLAVOR при передаче в системный буфер обмена никак не надо обрабатывать, надо отдать их туда как есть (массивом байт), и снабдить описанием типа "ENHMETAFILE". Интересно, какие еще типы можно так передавать? Пока я нашел только вариант, где удалось передать MathML.

Второй способ придумал автор Java Vector Cut and Paste Library. Финт ушами -- формат rtf поддерживается джавой из коробки, в том числе его передача через Clipboard. А rtf может служить контейнером для таких форматов как MACPICT, EMF и WMF. Через одно место, но уже неплохо -- в частности, MACPICT по его словам понимается как на Windows так и на Mac OS X. Но rtf не воспринимается, например, PowerPoint-ом -- в отличие от первого способа.

4 августа 2010 г.

MAC-адрес в java

Периодически обнаруживаю в JDK функциональность, которую я там не ожидал. Например, сегодня узнал, что можно получить MAC-адрес сетевой карты вызовом NetworkInterface.getHardwareAddress(). Причем судя по документации, метод еще с 1.4 тянется. А я почему-то был уверен, что хрен там, а не MAC из java. Приятная неожиданность.

28 июля 2010 г.

java + USB, java + bluetooth (JSR-80/82)

Сегодня был на собеседовании. Задача -- программа на джаве должна общаться с девайсами через USB. Пришел на работу, полез смотреть, что есть на эту тему готового -- ну как-то не верилось мне, что до сих пор никому в голову не пришло работать с USB из java.

Пришло. Даже JSR есть -- №80. Пакет javax.usb официально зарезервирован. Есть reference implementation от IBM. Текущий статус? -- Черт его знает. Последние изменения датированы годом 2003 -- я еще в институте учился. Реализация для linux вроде худо-бедно рабочая. Реализации для windows -- нет. Внимание: вопрос на засыпку. Какой смысл в JSR для java, если реализация есть только для одной платформы? Кому нужно чисто для линукс -- мог бы и через usbfs извернуться.

Есть альтернатива: jUSB. Текущий статус -- такой же, все бросили. Реализация есть для linux/bsd/mac. Для windows -- какая-то alpha, на базе libusb. libusb в свою очередь -- проект про кроссплатформенному C-API для USB. Тоже с неизвестным статусом, тоже заброшен. win-реализация требует для сборки cygwin, и работает через свой драйвер USB уровня ядра (kernel mode). У меня смутное ощущение, что либо лыжи не едут, либо я ебанутый я чего-то здесь не понимаю -- USB что, настолько сложный стандарт? В винде невозможно работать с USB устройством иначе как проинсталлировав в систему свой драйвер? Я даже не столько жалуюсь, сколько реально не понимаю ситуации. Вроде USB очень современный, актуальный стандарт, over 9000 устройств через него подключаются -- мне бы казалось, что уже везде должны быть красивые и удобные привязки (binding) для работы с ним -- ан нет, в Java такого нет. И причина не понятна

Кстати, такая же подстава с java bluetooth. Есть JSR-82, есть какие-то реализации - в основном для J2ME. Здесь ситуация обратная -- с windows версией все вроде ок, linux/bsd версия есть, но с некоторыми шаманскими плясками. Опять же, проект стоит на месте уже лет 5. Странно.

16 июля 2010 г.

NIO

С завидной регулярностью забываю, что пакет NIO -- это не только ценный мех не только для ввода-вывода. В нем еще есть много полезных операций с примитивами -- в частности, такие вещи, которые бы в С делались простым приведением типа указателя, а в java так сходу даже и не вспоминается, как бы это сделать, и чтобы не сильно через задницу. Так вот — не через задницу это через java.nio. Вот, например, потребовалось сконвертировать int[] в byte[] -- пожалуйста:

final int[] idata = ...;
final ByteBuffer buff = ByteBuffer.allocate( idata.length * 4 );
buff.asIntBuffer().put( idata );
data = buff.array();

Интересное изнутри F/J

В статье про Fork/Join framework Дог Ли отмечает интересную деталь. Он реализовывал двунаправленную очередь (Deque) для задач на базе массива. И обнаружил, что если хранить в массиве непосредственно ссылки на объекты, то в многоядерном окружении это будет работать медленнее, чем если ввести дополнительный уровень, и хранить в массиве ссылку на промежуточный объект (Entry), а уж из объекта ссылаться на саму задачу. Он предполагает, что этот эффект возникает из-за уменьшения cache contention, потому что данные массива в памяти идут подряд, а Entry создаются джавой как получится, и чаще всего будут более-менее разбросаны по памяти. Я так понимаю, речь идет о cache contention при записи -- насколько мне помнится, в многоядерном окружении если разные ядра читают-пишут в одну и ту же область памяти -- это действительно довольно затратно, потому что приходится постоянно блокировать/разблокировать сегмент кэша на эксклюзивный доступ для конкретного ядра.

Ну а еще таким образом можно сделать ссылку на объект volatile, и упросить синхронизацию :)

JSR-166y -- fork/join

Ух ты блин! Читал "Scala 2.8.0: what's new" и наткнулся на загадочную фразу: "Actors can be configured to use the efficient JSR166y fork/join pool, resulting in significant performance improvements on 1.6 JVMs". Поскольку я никогда не слышал о fork/join в 1.6, полез копать. Оказывается, группа разработки JSR-166 (который java.util.concurrent) не угомонилась, когда их работа была включена в jdk. Нет, эти гарные хлопцы продолжили свои бесчеловечные экспериментыразработки. Они разработали расширение для JSR-166, называется JSR-166y, иначе Fork/Join framework. Это фреймворк, упрощающий разработку кода для многоядерных систем. В целом можно сказать, что это что-то в духе (отдаленно) map/reduce идеологии, когда код пишется в терминах элементарных операций, независимых друг от друга, которым совершенно по барабану, на каком ядре/узле сети они выполняются, а уже специальный управляющий модуль раскидывает куски данных по вычислительным узлам, и собирает результаты работы.

Статья Дога Ли с описанием исходного Fork/Join фраймворка здесь. Вкратце: вычисления обертываются в наследника ForkJoinTask (который, в свою очередь, наследник Future) и помещаются в ForkJoinPool (менеджер потоков). Выполняемый в пуле ForkJoinTask может внутри себя создать и запустить (асинхронно) другой ForkJoinTask, используя метод fork(). И может дождаться его завершения методом join() (близкий аналог Future.get()). В такой семантике легко записываются алгоритмы типа "разделяй и властвуй" -- в примерах обычно показывается, как удобно реализовывать merge sort в такой нотации.

Основной бонус перед ExecutorService/Future, как я его сейчас понял -- не нужно явно указывать ExecutorService каждый раз при создании новой задачи. То есть вместо
final ExecutorService es = ...;
final Future<?> f = es.submit( task );
final Object res = f.get();

мы имеем

final ForkJoinTask task = ...;
task.fork();
final Object res = task.join();


Разумеется, "верхний" ForkJoinTask все равно нужно явно поместить выполняться в какой-то пул -- но внутренняя реализация ForkJoinTask.exec не имеет никакой информации о пуле -- она вообще пул не видит, она видит только задачи (ForkJoinTask). На мой взгляд, это очень удобно. Последний раз, когда я реализовывал асинхронный рекурсивный обход дерева, я сильно ругался на то, что мне все время приходится тащить с собой ссылку на ExecutorService. Т.е. именно для рекурсивных алгоритмов, как я вижу, такой подход становится особенно удобным.

Дополнение можно просто скачать как отдельную библиотеку. Возможно, оно будет включено в jdk 1.7. Я бы приветствовал :)

9 июля 2010 г.

Не пущать!

Вчера и сегодня потратил два дня, пытаясь локализовать реализацию javax.measure из org.jscience. Уже не первый раз наталкиваюсь на странную штуку -- грамотные, вроде бы, программисты (Жан-Мари Дотель -- очень грамотный программист) создавая библиотеку, предназначенную для расширения ухитряются закрыть все возможности для этого.

Класс UnitFormat extends java.util.text.Format. Изначально, по задумке Жан-Мари, предназначен для локализации. Там даже комментарии в коде такие есть. И локализация была бы просто элементарной, если бы Жан-Мари не ухитрился во всех удобных точках вмешательства проставить такие права доступа, что к ним хрен доберешься.

В прошлый раз такая же засада была с gui-commands -- в конечном счете, штук 10 классов из этой библиотеки перекочевали в source path моего проекта, и подверглись вивисекции. Хотя этот метод мне всегда напоминал "guriella patching"

6 июля 2010 г.

Никогда, никогда, никогда блядь не пишите так:
public MyObject(final int param){    
    if( param < 0 ){        
        throw new IllegalArgumentException("Incorrect param");   
    }
}
надо как минимум вот так:
public MyObject(final int param){    
    if( param < 0 ){
        throw new IllegalArgumentException("Incorrect param: must be >=0, but got "+param);
    }
}
Это всего на пару дюжин символов длиннее -- но знали бы вы, сколько времени это иногда может сэкономить при отладке... Собственно, часто это может сделать саму отладку просто не нужной -- будет сразу понятно, где возник косяк. Особенно обидно, что такие вещи часто встречаются в самом jdk -- класс Color тому наглядный пример. В свое время в Матконструкторе мы частенько создавали Color из javascript, и когда получаешь "Color parameter outside of expected range: Alpha" это ни разу не помогает что-либо понять.

7 июня 2010 г.

IntellJ IDEA 9 Community Edition

Она меня достала. Я не знаю, как нежно любимая мною компания JetBrains могла выпустить на рынок такое говно. Я не знаю, какие цели они при этом преследовали. Но на мой взгляд, это epic fail. Она дико тормозит, на совершенно скромных по размерам проектах. Она жрет память так, как будто рассчитывает, что я буду втыкать новые планки по мере ее желания. На P4 3.2Ghz, у нее в распоряжении 700Мб памяти -- и проект открывается 10 минут!!! Перерасчет индексов -- 10 минут! Разработчики -- да вы просто охуели!

Более того, она глючит чуть ли не каждый день. Это само по себе не так важно -- старые версии тоже подглючивали, безглючных программ не бывает. Но в старых версиях это ничему не мешало -- ну не выполнилась команда сейчас, выполнится в следующий раз. Они глючили безопасно -- а эта падла то настройки собьет, то JDK забудет.

Лучи поноса JetBrains.

P.S. Ах, да. Совсем забыл. Ее папка кэшей занимает 7Гб. Семь гигабайт! Разработчики, да вы что, совсем белены объелись? Чего там хранить на 7Гб??? content.dat.storageData -- 7.5Гб один файл. Неудивительно, что ей нужно 10 минут чтобы хотя бы прочитать его.

30 апреля 2010 г.

ReentrantLock и synchronized(lock)

Пока рылся в исходниках JetLang, наткнулся на такой класс, как RunnableBlockingQueue.java. Мне показалось интересным, что в нем используется ReentrantLock, хотя по функциональности вполне подойдет обычный synchronized() и wait()/notify() -- никакой новой функциональности из ReentrantLock не используется. Стало интересно, есть ли какой-либо эффект в плане производительности?

Взял, прямолинейно переписал RunnableBlockingQueue с использованием plain old synchronized style. Создал свой класс MyPoolFiber, использующий именно эту реализацию очереди, и сравнил производительность на примере пинг-понга.

С дефолтными настройками оба варианта неразличимы по производительности. А вот результат с -server меня удивил -- ReentrantLock весьма заметно быстрее. Примерно 7 к 6 в пользу ReentrantLock. Не ожидал -- мне казалось, что новое concurrency API более мощное, но и более медленное -- в тех случаях, когда используется теми же методами, что и старое.

JetLang

Игрался с библиотекой JetLang. Библиотека довольно компактная, и реализует идею message-based concurrency для джавы. Основной плюс идеи организации взаимодействия потоков через отправку-прием сообщений в том, что, при грамотной реализации, можно обеспечить очень низкий уровень столкновений блокировок (lock contention).

Вообще, я поначалу был несколько разочарован -- ожидал что-то вроде Scala Actors в миниатюре, пусть с ограничениями. Но JetLang это скорее бэкграунд, на фоне которого можно самому реализовать что-то вроде Акторов, если захочется.

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

20 апреля 2010 г.

JFormattedTextField.selectAll

Нужно было сделать, чтобы JFormattedTextField выделял свое содержимое при получении фокуса. Стандартный addFocusListener -> .selectAll не сработал. Оказывается (см Sun bug #4740914) JFormattedTextField переопределяет processFocusEvent и внутри сначала зовет базовую реализацию (которая, в числе прочего, и FocusListener-ы оповещает), а потом переформатирует свое содержимое -- что, разумеется, все выделение напрочь убивает.

Решений нашлось два. Либо уныло-универсальное addFocusListener -> SwingUtilities.invokeLater -> selectAll, либо отнаследовать свой собственный класс от JFTF, и переопределить processFocusEvent, дописав в конец selectAll().

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

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

4 апреля 2010 г.

Вышел jdk 1.6 под Snow Leopard

В последнем обновлении наконец-то появилась шестая джава под мак (SL).

Тут же появились и проблемы с ней. java.io.IOException: Keystore was tampered with, or password was incorrect -- вылетает при работе с SSL ссылка

Корень проблем оказался прост -- эппл сменила (скорее всего, просто по ошибке) дефолтный пароль на хранилище сертификатов с changeit на changeme. Если сменить его обратно -- все заработает как встарь. Например, вот так:

sudo -i
cd /System/Library/Frameworks/JavaVM.framework/Home/lib/security/
keytool -storepasswd -keystore cacerts

1 апреля 2010 г.

Кодировка консоли в java

Кодировка консоли в Win не совпадает с системной кодировкой (которая Charset.defaultCharset()). А знать ее часто бывает полезно. Например, запускаемые внешние процессы могут возвращать описания ошибок на русском -- и фиг вы их прочитаете. Конечно, можно просто зашить кодировку Cp866 для русской версии -- но это неправославное решение. А ну как другая локаль будет? Хотелось бы иметь возможность спросить, в какой кодировке работает наша консоль.

Я долгое время был уверен, что такого метода в jdk не предусмотрено. Но, совершенно случайно, раскапывая исходники стандартной библиотеки обнаружил решение для Sun JDK 1.6+ -- приватный статический метод java.io.Console.encoding()

final Class<Console> clazz = Console.class;
final Method method = clazz.getDeclaredMethod( "encoding", new Class[0] );
method.setAccessible( true );
final String encoding = ( String )method.invoke( null );


возвращает на моей системе Cp866, как положено.

23 марта 2010 г.

Scala

Первое впечатление от Scala IDEA plugin: "как же медленно оно компилируется!" Натуральный С++ камбэк. Успеваю заметить штук 5 стадий компиляции. Все-таки javac, пересобирающий проект из 500 файлов за 20 секунд сильно развращает.

18 марта 2010 г.

Зачем synchronized вокруг wait/notify?

Еще один из вопросов, который мучил меня во время знакомства с java threading, и так и остался, в то время, не отвеченным. Зачем сановские инженеры спроектировали wait/notify так, что они обязательно требуют на входе захваченной блокировки? Причем на входе они ее требуют, но внутри себя они ее отпускают -- что-то очень хитрое стояло за таким решением, что-то, что я не мог понять.

Ответ оказался довольно прост. Другое поведение не имеет смысла, потому что не дает возможность реализовать то, для чего нужен wait/notify.

Зачем нужен wait? Точнее -- каков сценарий его использования? Если мы хотим просто приостановить поток -- есть Thread.sleep(). А wait нужен тогда, когда мы ждем какого-то события. Еще точнее, мы ждем выполнения какого-то условия, за которое отвечает какой-то другой поток. Но это означает, что у нашего потока с этим другим потоком есть общее, разделяемое состояние (shared state). Второй поток это состояние меняет, и в какой-то момент оно становится "подходящим" для нас, и мы хотим об этом узнать. И wait/notify это всего лишь инструмент, который дает нам такую возможность. Но если у нас есть разделяемое состояние -- нам просто необходима блокировка в обоих потоках, чтобы избежать проблем с data race и memory visibility. Сами-то по себе методы wait/notify можно организовать без требования блокировки -- но они будут бесполезны

На конкретном примере: вот как выглядит типичный код с wait/notify (представим, что секции synchronized не обязательны)
//общие переменные 
    boolean condition = false;
    final Object event = new Object();
    ...
    //первый поток
    while(!condition){
        event.wait();
    }
    ...
    //второй поток
    condition = true;
    event.notify();

Что помешает второму потоку вклиниться между строчками 7 и 8 -- когда первый уже решил, что он должен ждать, но еще не вызвал метод wait? В этом случае notify() вызванный вторым уйдет в пустоту (пока еще никто ничего не ждет), а wait(), вызванный первым никогда не пробудится -- некому больше будить (забудем пока про внезапные пробуждения). Другой вопрос -- кто гарантирует, что обновленное вторым потоком значение condition=true будет увидено первым? Никаких memory barrier-ов здесь нет, первый поток спокойно может закэшировать condition хоть в регистрах процессора, и быть свято уверенным, что оно все еще false. Еще можно вообразить разнообразные insruction reordering, в ходе которых компилятор может решить переставить condition = true после event.notify(), например.

Ок, договорились: синхронизация необходима. Но зачем так плотно привязывать wait/notify к synchronized? Спроектировали бы wait/notify независимо от синхронизации, и просто указывали бы в recommended practices что правильно писать так-то. Были бы очевидные бенефиты -- сейчас, например, старый-добрый Object.wait/notify работает только со старым-добрым synchronized, а новый Lock.lock()/unlock() только с новым же Condition.await/signal -- а между собой они не работают, что, в общем-то, странно. Казалось бы -- какая разница, каким методом обеспечивать синхронизацию разделяемого состояния между потоками?

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

Вот мы и приходим к той реализации, что имеем. Сначала нужно захватить монитор какого-нибудь объекта, чтобы обеспечить согласованность состояния condition. Потом можно вызвать wait(), и он должен захваченный монитор отпустить -- иначе как другой поток сможет изменять условие? Но wait нужно вызвать на том же объекте -- иначе как wait() узнает, какой именно монитор из 10 захваченных текущим потоком выше по стеку ему надо освободить? Перед notify нужно захватить тот же самый монитор, чтобы синхронизировать изменения в condition между потоками. Ну и очевидно, что notify() нужно вызывать на том же объекте, чтобы он знал, какой wait ему нужно будить. Такой вот расклад

Чем плохо synchronized(this)?

Несколько лет назад, бродя по исходникам JDK я задался вопросом -- почему там так часто встречается организация блокировки через
private final Object lock = new Object();
....

synchronized(lock){
...
}

хотя для этих целей легко можно использовать synchronized(this)? Я еще понимаю, если нужно несколько мониторов для разных наборов атомарных изменений (хотя, на мой взгляд, это часто первый звоночек что объекту нужна декомпозиция), но я часто видел такой код и когда монитор только один. Какое-то время это было для меня загадкой, пока у кого-то из гуру я не встретил ответа -- использование this как монитора синхронизации может нарушать инкапсуляцию вашего объекта. Монитор синхронизации -- это штука с состоянием: у него есть владелец (owner) который может меняться. Давая возможность клиентам работать с вашим объектом вы не можете запретить им работать с его монитором -- а это может менять его состояние, и нарушать те инварианты, на которые вы рассчитывали, проектируя класс.

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

Но вот вчера у меня, наконец, сложился явный и четкий пример. Итак, код:
public class Runner {

private Thread owner = null;

public synchronized void run( final Callable task ) throws Exception {
    owner = Thread.currentThread();
    try {
        task.call();
    } finally {
        owner = null;
    }
}

public synchronized void check() {
    final boolean invariant = ( owner == null ) || ( owner == Thread.currentThread() );
    if ( !invariant ) {
       //can this code be executed?
       throw new AssertionError( "Lock is broken: " + owner + " is owner, but " + Thread.currentThread() + " is here!" );
    }
}

}

на первый взгляд кажется, что код в строках 17-18 никогда не может быть выполнен. Оба метода синхронизированы, никакой посторонний поток не может влезть, пока текущий поток внутри run(Callable). Однако, сломать этот объект крайне просто:
final Runner runner = new Runner();

final Callable task = new Callable() {
    public Object call() throws Exception {
        runner.wait( 2000 );
        return null;
    }
};

final Thread thread = new Thread( "runner" ) {
    public void run() {
        try {
            runner.run( task );
        } catch ( Exception e ) {
            throw new RuntimeException( e );
        }
    }
};

thread.start();

//ensure thread started
Thread.yield();
Thread.sleep( 100 );
//check the invariant
runner.check();

object.wait() должен вызываться внутри synchronized(object), и, на время ожидания он отпускает монитор. То есть пока поток thread ждет 2 секунды на мониторе runner этот монитор свободен, несмотря на то, что выше по стеку есть synchronized(runner). И в эти 2 секунды любой другой поток может захватить этот монитор -- что мы и делаем, демонстрируя нарушение инварианта класса.

Какой отсюда вывод? Вывод такой: если вы используете callback интерфейсы -- т.е. если ваш код выполняет внутри себя какой-то другой код, пришедший "со стороны" -- вы должны делать это либо вне синхронизации, либо использовать монитор синхронизации, до которого клиентский код не сможет добраться, вроде private final Object lock = new Object()

На данный момент я не вижу других способов (кроме callback), как "открытая" синхронизация с помощью synchronized(this) может нарушить инкапсуляцию. Если класс колбэки не использует -- можно использовать синхронизированные методы спокойно.