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) может нарушить инкапсуляцию. Если класс колбэки не использует -- можно использовать синхронизированные методы спокойно.
ну и еще вероятно synchronized(this) не прокатит в статичном методе?
ОтветитьУдалитьНет, это, как раз, не принципиально: в статическом методе вместо this можно использовать MyClass.class.
Удалить