Управление непостоянством (volatility)

Указания по использованию volatile-переменных


Автор Брайан Гётц, 19 июня 2007 года. Оригинал: Managing Volatility

Volatile-переменные в Java можно назвать «synchronized-лайт»; для их использования нужно меньше кода, чем для synchronized-блоков, часто они выполняются быстрее, но при этом могут делать лишь часть из того, что делают synchronized. В этой статье представлено несколько паттернов эффективного использования volatile — и несколько предупреждений о том, где их использовать не надо.

У локов (locks) есть две основные черты: взаимное исключение (mutual exclusion, mutex) и видимость. Взаимное исключение означает, что лок может быть захвачен только одной нитью в отдельный момент времени, и это свойство можно использовать для реализации протоколов управления доступом к общедоступным ресурсам, так что только одна нить будет их использовать в отдельный момент времени. Видимость — вопрос более тонкий, ее задача обеспечить, что изменения, сделанные в общедоступных ресурсах до освобождения замка будут видимы следующей нити, захватившей этот замок. Если бы синхронизация не гарантировала видимость, нити могли бы получать устаревшие или неверные значения общедоступных переменных, что привело бы к целому ряду серьезных проблем.

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

Volatile-переменные обладают свойствами видимости, присущими synchronized, но лишены их атомарности. Это означает, что нити автоматически будут использовать самые актуальные значения volatile-переменных. Их можно использовать для нитебезопасности (thread safety, чаще переводится как потокобезопасности), но в очень ограниченном наборе случаев: тех, что не вводят связи между несколькими переменными или между текущими и будущими значениями переменной. Таким образом, одной volatile недостаточно для реализации счетчика, мьютекса или любого класса, чьи неизменные части связаны с несколькими переменными (например, «start <=end»).

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

Условия правильного использования volatile

Заменить локи на volatile можно в ограниченном числе обстоятельств. Для нитебезопасности необходимо, чтобы выполнялись оба критерия:

  1. То, что записывается в переменную, не зависит от ее текущего значения.
  2. Переменная не участвует в инвариантах с другими переменными.

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

В большинстве ситуаций будет нарушено либо первое, либо второе условия, что делает volatile-переменные менее используемым подходом к достижению нитебезопасности, чем synchronized. В листинге 1 показан не-нитебезопасный класс с диапазоном чисел. Он содержит инвариант — нижняя граница всегда меньше или равна верхней.

@NotThreadSafe 
public class NumberRange {
    private int lower, upper;
 
    public int getLower() { return lower; }
    public int getUpper() { return upper; }
 
    public void setLower(int value) { 
        if (value > upper) 
            throw new IllegalArgumentException(...);
        lower = value;
    }
 
    public void setUpper(int value) { 
        if (value < lower) 
            throw new IllegalArgumentException(...);
        upper = value;
    }
}


Поскольку переменные состояния диапазона ограничены таким образом, будет недостаточным сделать поля lower и upper volatile, чтобы обеспечить нитебезопасность класса; по-прежнему будет нужна синхронизация. Иначе рано или поздно не повезет и две нити, выполнившие setLower() и setUpper() с неподходящими значениями могут привести диапазон в противоречивое состояние.

Например, если начальное значение (0, 5), нить A вызывает setLower(4), и в то же время нить B вызывает setUpper(3), эти перемежающиеся операции приведут к ошибке, хотя обе пройдут проверку, которая должна защищать инвариант. В итоге диапазон будет (4, 3) — неверные значения. Нам нужно сделать setLower() и setUpper() атомарными по отношению к другим операциям над диапазоном — и присвоение полям volatile этого не сделает.

Соображения производительности

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

Чрезвычайно трудно сделать точные всеобъемлющие заявления вида «X всегда быстрее, чем Y,» особенно когда речь идет о внутренних операциях виртуальной машины Java. (Например, JVM может полностью снять блокировку в некоторых ситуациях, что затрудняет абстрактное обсуждение затрат на volatile по отношению к синхронизации). Тем не менее, на большинстве современных процессорных архитектур затраты на чтение volatile мало отличаются от затрат на чтение обычных переменных. Затраты на запись volatile значительно больше, чем на запись обычных переменных, из-за ограждения памяти, необходимого для обеспечения видимости, но в целом дешевле, чем установка локов.

Паттерны для правильного использования volatile
Многие эксперты по параллелизму склонны избегать использования volatile-переменных вообще, потому что их труднее использовать правильно, чем локи. Однако существуют некоторые четко определенные паттерны, которые, если следовать им внимательно, могут безопасно использоваться в самых разных ситуациях.
Всегда учитывайте ограничения volatile — используйте только volatile, которые никак не зависят от всего остального в программе, и это должно не позволить вам залезть с этими паттернами на опасную территорию.
Паттерн №1: флаги состояния
Возможно, каноническое использование изменчивых переменных — это простые булевы флаги состояния, указывающие на то, что произошло важное одноразовое событие жизненного цикла, такое как завершение инициализации или запрос на завершение работы. Многие приложения включают конструкцию управления формы: «Пока мы не готовы выключиться, продолжаем работать», как показано в листинге 2:

volatile boolean shutdownRequested;
 
...
 
public void shutdown() { shutdownRequested = true; }
 
public void doWork() { 
    while (!shutdownRequested) { 
        // do stuff
    }
}


Вероятно, метод shutdown () будет вызываться откуда-то извне цикла — в другой нити — поэтому требуется синхронизация для обеспечения правильной видимости переменной shutdownRequested. (Он может быть вызван из слушателя JMX, слушателя действий в нити событий GUI, через RMI, через веб-службу и т. д.). Однако цикл с синхронизированными блоками будет гораздо громоздким, чем цикл с volatile-флагом состояния как в листинге 2. Поскольку volatile упрощает написание кода, а флаг состояния не зависит от какого-либо другого состояния программы, это пример хорошего использования volatile.

Для таких флагов статуса характерно то, что обычно существует только один переход состояния; флаг shutdownRequested переходит из false в true, а затем программа выключается. Этот паттерн можно расширить до флагов состояния, которые могут изменяться туда и обратно, но только если цикл перехода (от false до true to false) будет происходить без внешних вмешательств. В противном случае необходим какой-то атомарный механизм перехода, такой как атомарные переменные.

Паттерн № 2: одноразовая безопасная публикация

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

Один из способов безопасной публикации объекта состоит в том, чтобы сделать ссылку на объект volatile. В листинге 3 показан пример, где во время запуска фоновый поток загружает некие данные из базы данных. Другой код, когда может попытаться использовать эти данные, проверяет, был ли он опубликован, прежде чем пытаться его использовать.

public class BackgroundFloobleLoader {
    public volatile Flooble theFlooble;
 
    public void initInBackground() {
        // делаем много всякого
        theFlooble = new Flooble();  // единственная запись в theFlooble
    }
}
 
public class SomeOtherClass {
    public void doWork() {
        while (true) { 
            // чё-то там делаем...
            // используем theFolooble, но только если она готова
            if (floobleLoader.theFlooble != null) 
                doSomething(floobleLoader.theFlooble);
        }
    }
}


Если бы ссылка на theFlooble не была volatile, код в doWork() рисковал бы увидеть частично сконструированный Flooble при попытке обратиться по theFlooble.

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

Паттерн № 3: независимые наблюдения

Другой простой пример безопасного применения volatile — ситуация, когда наблюдения периодически “публикуются” для использования в рамках программы. Например, есть датчик окружающей среды, который определяет текущую температуру. Фоновая нить может считывать показания этого датчика с периодом в несколько секунд и обновлять volatile-переменную, содержащую текущую температуру. Затем другие нити могут считывать эту переменную, зная, что значение в ней всегда самое актуальное.

Еще одно использование этого паттерна — сбор статистики о программе. В листинге 4 показано, как механизм аутентификации может запоминать имя последнего залогинившегося пользователя. Ссылка lastUser будет повторно использоваться для публикации значения для использования остальной частью программы.

public class UserManager {
    public volatile String lastUser;
 
    public boolean authenticate(String user, String password) {
        boolean valid = passwordIsValid(user, password);
        if (valid) {
            User u = new User();
            activeUsers.add(u);
            lastUser = user;
        }
        return valid;
    }
}


Этот паттерн расширяет предыдущий; значение публикуется для использования где-то еще в программе, но публикация не одноразовое событие, а серия независимых. Этот паттерн требует, чтобы опубликованное значение было фактически неизменным — что его состояние после публикации не менялось. Код, использующий значение, должен знать, что оно может в любой момент измениться.

Паттерн № 4: паттерн «volatile bean»

Паттерн “volatile bean” применим во фреймворках, использующих JavaBeans как “glorified structs”. В паттерне “volatile bean” JavaBean используется как контейнер для группы независимых свойств с геттерами и/или сеттерами. Обоснованием необходимости паттерна “volatile bean” является то, что многие фреймворки предоставляют контейнеры для изменяемых держателей данных (например, HttpSession), но объекты, помещенные в эти контейнеры, должны быть нитебезопасными.

В патттерне volatile bean все элементы данных JavaBean являются volatile, а геттеры и сеттеры должны быть тривиальными — они не должны содержать никакой логики, кроме получения или установки соответствующего свойства. Кроме того, для членов данных, которые являются объектными ссылками, упомянутые объекты должны быть эффективно неизменными. (Это запрещает наличие полей-ссылок на массивы, так как когда ссылка массива объявлена volatile, только эта ссылка, а не сами элементы, имеет свойство volatile.) Как и в любой volatile-переменной, не может быть никаких инвариантов или ограничений, связанных с свойствами JavaBeans. Пример JavaBean, написанного по паттерну “volatile bean”, показан в листинге 5:

@ThreadSafe
public class Person {
    private volatile String firstName;
    private volatile String lastName;
    private volatile int age;
 
    public String getFirstName() { return firstName; }
    public String getLastName() { return lastName; }
    public int getAge() { return age; }
 
    public void setFirstName(String firstName) { 
        this.firstName = firstName;
    }
 
    public void setLastName(String lastName) { 
        this.lastName = lastName;
    }
 
    public void setAge(int age) { 
        this.age = age;
    }
}


Более сложные volatile-паттерны

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

Более продвинутые паттерны использования volatile могут быть чрезвычайно хрупкими. Крайне важно, чтобы ваши предположения были тщательно задокументированы, а эти паттерны сильно инкапсулироваными, потому что даже мельчайшие изменения могут сломать ваш код! Кроме того, учитывая, что основной причиной для более сложных вариантов использования volatile является производительность, убедитесь, что у вас действительно есть выраженная потребность в предполагаемом усилении производительности, прежде чем применять их. Эти паттерны являются компромиссами, которые жертвуют читабельностью или легкостью поддержки ради возможного повышения производительности — если вам не требуется повышение производительности (или вы не можете доказать, что вам это нужно, с помощью строгой программы измерения), то это, вероятно, плохая сделка, потому что вы отказываетесь от чего-то ценного и получаете что-то меньшее взамен.

Паттерн № 5: дешевый лок чтения-записи

Сейчас вы должны уже хорошо понимать, что volatile слишком слаба для реализации счетчика. Поскольку ++ x по факту сокращение трех операций (чтение, добавление, хранение), при неудачном стечении обстоятельств вы потеряете обновленное значение, если несколько потоков попытаются одновременно увеличить volatile-счетчик.

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

@ThreadSafe
public class CheesyCounter {
    // Employs the cheap read-write lock trick
    // All mutative operations MUST be done with the 'this' lock held
    @GuardedBy("this") private volatile int value;
 
    public int getValue() { return value; }
 
    public synchronized int increment() {
        return value++;
    }
}


Причина, по которой этот метод называется «дешевым локом чтения-записи», заключается в том, что вы используете разные механизмы синхронизации для чтения и записи. Поскольку операции записи в этом случае нарушают первое условие использования volatile, вы не можете использовать volatile для безопасной реализации счетчика — вы должны использовать блокировку. Однако вы можете использовать volatile для обеспечения видимости текущего значения при чтении, поэтому вы используете блокировку для всех операций изменения и volatile для операций read-only. Если лок позволяет только одной нити за раз получать доступ к значению, volatile-чтения допускают более одного, поэтому, когда вы используете volatile для защиты чтения, вы получаете более высокий уровень обмена, чем если бы вы использовали блокировку для всего кода: и чтения, и записи. Однако имейте в виду хрупкость этого паттерна: с двумя конкурирующими механизмами синхронизации он может стать очень сложным, если вы выйдете за пределы самого базового приложения этого паттерна.

Резюме

Volatile-переменные — это более простая, но более слабая форма синхронизации, чем блокировка, которая в некоторых случаях обеспечивает лучшую производительность или масштабируемость, чем встроенная блокировка. Если вы соблюдаете условия безопасного использования volatile — переменная действительно независима и от других переменных, и от своих собственных предыдущих значений — иногда вы можете упростить код, заменив synchronized на volatile. Однако код с использованием volatile часто бывает более хрупким, чем код с блокировкой. Предлагаемые здесь паттерны охватывают наиболее распространенные случаи, когда волатильность — разумная альтернатива синхронизации. Следуя этим паттернам — и заботясь о том, чтобы не вытеснять их за их собственные пределы — вы сможете безопасно использовать volatile в тех случаях, когда они дают выигрыш.

synchronized не синхронизирует

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

import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Test {
    volatile static Test test = new Test();
    volatile Lock lock = new ReentrantLock();
    // volatile AtomicInteger x = new AtomicInteger(2);
    volatile int x = 2;

    private synchronized void method() {
        synchronized (test) {
            lock.lock();
            try {
                x++;
                Thread.yield();
                x--;
            } finally {
                lock.unlock();
            }
        }
    }

    public static void main(String[] args) {
        Thread th1 = new Thread(new RunThread());
        Thread th2 = new Thread(new RunThread());

        th1.start();
        th2.start();

    }

    static class RunThread implements Runnable {
        int count = 0;

        @Override
        public void run() {
            for (int i = 0; i < 10000; i++) {
                test.method();

                if (test.x != 2) {
                    System.out.println("--------------- " + test.x);
                    count++;
                }
            }
            System.out.println(count);
            if (count > 0) System.exit(77);
        }
    }
}


В данном примере переизбыток синхронизирующих механизмов, но это я оставил для того что бы показать какими способами я пытался добиться результата. При удалении части этих механизмов проблема все равно остается.
Если кто подскажет в чём ошибка, буду очень признателен.
  • ,

Некорректность com.javarush.test.level17.lesson10.home04

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

Тяжело заходит Synhronized? Делюсь решением.

Сто пудов я не оригинален. Большинство и без моего совета смотрят видео Головоча, но все же… Если Вам тяжело дается Synchronized, да и вообще Multithreading гляньте следующие лекции:

Multithreading #1
Multithreading #2
Multithreading #3

Мне очень помог момент когда Головач нарисовал два set'а, wait и blocked. Стола понятно кто кого ждет и зачем. Именно этот момент помог разложить концепцию Multithreading в голове.
  • ,

Вопрос про synchronized

Ребята обьясните как правильно применять synchronized, из лекций JR да и вообще почитав различные статьи, я понял, что он нужен для того:
1) Чтобы контролировать доступ к помеченном методу или блоку, пока в нем есть поток, он занят и никто другой не может из потоков в него зайти, пока он не освободиться.
2) Чтобы провести синхронизацию данных, то есть если какие то данные в данном блоке или методе изменились, они изменятся для всех других потоков.
Так я понял его применение, но решая задачу level17.lesson10.home04, там где нужно было расставить synchronized в нужных местах, я в упор не понимаю для чего он нужен в методе 3, если для каждого потока который будет вызывать method3 никакой синхронизации не нужно, ведь param не изменяется никак, а переменная random создается для каждого потока.

private  double param = Math.random();

    private void method0() {
        double i = method3();
    }

    protected  void method1(String param1) {
        Solution solution = new Solution();
        solution.method0();
    }

    public  void  method2(int param1) {
        param1++;
    }

      double  method3() {
        double random = Math.random();
        return random + param;
    }
  • ,

level17.lesson10.home05

Подскажите, где ошибка в логике?
Я рассуждаю так: если метод синхронизирован, то внутри него любая синхронизация избыточна, поэтому синхронизацию внутри методов убираю (в условии задачи необходимо удрать синхронизацию именно внутри методов). Однако тест все равно не проходит. В последнем методе осуществляется чтение объекта, я там тоже убирал синхронизацию, тоже не помогает.

Другие ветки почитал, тоже логичное рассуждение, и тоже подобного рода, и все равно что-то я упускаю. А методом тыка как-то не хочется перебирать варианты.
  • ,

Синхронизация потоков, блокировка объекта и блокировка класса

March 8, 2013 Lokesh Gupta
Синхронизация относится к многопоточности. Синхронизированый блок кода может быть выполнен только одним потоком одновременно.

Java поддерживает несколько потоков для выполнения. Это может привести к тому, что два или более потока получат доступ к одному и тому же полю или объекту. Синхронизация это процесс, который позволяет выполнять все параллельные потоки в программе синхронно. Синхронизация позволяет избежать ошибок согласованности памяти, вызванные из-за непоследовательного доступа к общей памяти.
Когда метод объявлен как синхронизированный — нить держит монитор для объекта, метод которого исполняется. Если другой поток выполняет синхронизированный метод, ваш поток заблокируется до тех пор, пока другой поток не отпустит монитор.
Синхронизация достигается в Java использованием зарезервированного слова synchronized. Вы можете использовать его в своих классах определяя синхронизированные методы или блоки. Вы не сможете использовать synchronized в переменных или атрибутах в определении класса.
  • ,

level27.lesson09.home01

Producer–consumer
В классе TransferObject расставьте вызов методов wait/notify/notifyAll,
чтобы обеспечить последовательное создание и получение объекта.
Ожидаемый вывод:

Put: M
Got: M
Put: N
Got: N
Put: K
Got: K

где M, N, K — числа
Метод main не участвует в тестировании
PS: всегда старайтесь использовать любой concurrent список вместо реализации wait/notify/notifyAll.
Однако понимать основные методы необходимо

package com.javarush.test.level27.lesson09.home01;

import java.util.concurrent.atomic.AtomicInteger;

public class ProducerTask implements Runnable {
    private TransferObject transferObject;
    protected volatile boolean stopped;
    static volatile AtomicInteger i = new AtomicInteger(0);

    public ProducerTask(TransferObject transferObject) {
        this.transferObject = transferObject;
        new Thread(this, "ProducerTask").start();
    }

    public void run() {
        while (!stopped) {
            if (transferObject != null)
            synchronized (transferObject) {
                while (transferObject.isValuePresent) {
                    try {
                        transferObject.wait();
                    } catch (InterruptedException ignore) {
                    }
                }
                transferObject.put(i.incrementAndGet());
                transferObject.isValuePresent = true;
                transferObject.notifyAll();
            }
        }
    }

    public void stop() {
        stopped = true;
    }
}


package com.javarush.test.level27.lesson09.home01;

public class ConsumerTask implements Runnable {
    private TransferObject transferObject;
    protected volatile boolean stopped;

    public ConsumerTask(TransferObject transferObject) {
        this.transferObject = transferObject;
        new Thread(this, "ConsumerTask").start();
    }

    public void run() {

        while (!stopped) {
            if (transferObject != null)
            synchronized (transferObject) {
                while (!transferObject.isValuePresent) {
                    try {
                        transferObject.wait();
                    } catch (InterruptedException ignore) {

                    }
                }
                transferObject.get();
                transferObject.isValuePresent = false;
                transferObject.notifyAll();
            }
        }

    }

    public void stop() {
        stopped = true;
    }
}


package com.javarush.test.level27.lesson09.home01;

public class TransferObject {
    private int value;
    protected volatile boolean isValuePresent = false; //use this variable

    public synchronized int get() {
        System.out.println("Got: " + value);
        return value;
    }

    public synchronized void put(int value) {
        this.value = value;
        System.out.println("Put: " + value);
    }
}


Всё правильно выводит, по заданию. Почему-то не принимается сервером, что я упустил? Посмотрите пожалуйста!

Вопросы про synchronized

Возникло несколько вопросов по теме synchronized.


Вопрос 1
Предположим, у нас есть объект класса, в коде которого изменяется строка как предложено ниже.
public class Exxample1 extends Thread{
        public String stroka;

        public void run()
        {
            <...>
            stroka=stroka+" "; //(1)
            <...>
            synchronized(stroka)
            {
                stroka=stroka+" ";

            }
        }

    }

Объект этого класса одновременно используют сразу несколько нитей. Первая нить добралась до блока synchronized и заблокировала строку. Это значит, что все другие нити
  • не смогут зайти внутрь этого блока, пока первая нить не закончит свою работу в нем (заснут)
  • не смогут использовать строку stroka, где бы ее не вызвали(заснут). То есть команда (1) не будет выполнена пока первая нить не выйдет из блока.
  • оба пункта
  • другое
Вопрос 2
У нас есть объект класса, который используется одновременно несколькими нитями (код класса ниже)
public class Example2 extends Thread
    {
        public void run()
        {
            <...>
            synchronized(this)
            {
                <...>
            }
        }
    }

Какие утверждения, приведенные ниже верны.
Когда одна из нитей заходит в блок synchronized…
  • все остальные нити, которые уже работают с этим объектом заснут
  • нити, уже использующие этот объект продолжат свою работу, но новые будут ждать разблокировки объекта (заснут)
  • все нити, использующие этот объект или только собирающиеся его использовать заснут (объединение первых двух пунктов)
  • другое

Вопрос 3
Ниже приведен код класса, объект которого используется несколькими нитями одновременно.
public class Example3 extends Thread
    {   public String s;
        public int k;

        private String word;
        public void run()
        {
            <...>
            synchronized (s)
            {
                <...>
            }
            <...>


            synchronized (this)
            {
                <...>
            }
        }
    }

Одна из нитей заходит в первый блок synchronized и блокирует строку. В этот момент другая нить заходит во второй блок и пытается заблокировать весь объект, но строка s уже заблокирована. Что же произойдет?
  • вторая нить все равно заблокирует весь объект, первая — заснет
  • вторая нить все равно заблокирует весь объект, первая продолжит свою работу (а что тогда с s? будет использоваться обеими нитями или заблокирована для всех?)
  • вторая нить заснет, будет ждать выхода первой нити из блока
  • другое

Вопрос 4
Если использовать synchranized в описании метода, то когда нить вызовет этот метод, все треды, использующие объект, в котором описан данный метод, заснут? (Вроде ответ должен быть аналогичен ответу 2ого вопроса)
Спасибо за ваши ответы. Тема, действительно, сложная и несколько мутная. Надеюсь на ваше понимание. Кстати, можете подкинуть доп. ссылок или литературы на эту тему (коме уже указанных в JavaRush).

пришло время поразбирать synchronized, synchronized сам не поразбирается

Навеяно статьей Взаимодействие между потоками JAVA или задачка 'Робот'

Если в двух словах — то задача про то как с помощью синхронизации заставить два потока строго по-очереди выполнять свои обязанности. И будем разбирать как её нужно правильно делать через synchronized.

А сначала — лирическое отступление про то, что же делают wait() и notify()/notifyAll().

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

Открываем джавадоки и читаем описание оттуда:
notifyAll() — будит все потоки, находящиеся в состоянии [ожидания на мониторе данного объекта]. ...
wait() — при вызове без аргументов, переводит [текущий поток] в состояние waiting до тех пор пока какой-либо поток не вызовет notify() или notifyAll() на [объекте-мониторе, на котором синхронизация]. ...
Для базового понимания достаточно — теперь можем писать сам код:


class Leg implements Runnable{
    private final String name;
    private static final Object monitor  = new Object() ; //монитор на котором будем синхронизироваться сделаем статически-финальным и сразу проинициализируем
    public Leg(String name){
        this.name = name;
    }   
    public void run() {
        try{
            while (true) {    //зациклим до бесконечности пока "нога не сломается"
                synchronized (monitor) {
                    monitor.notifyAll();
                    monitor.wait();
                    System.out.println(name);//обязанностью каждой нити будет просто печатать своё имя в консоль
                }
            }
        } catch (InterruptedException e) {
            System.out.println("Leg error");
        }
    }
}

и вызов:
public class LegsSynchro
{
    public static void main(String[] args)
    {
        Leg lg1 =  new Leg("left");
        Leg lg2 =  new Leg("right");

        new Thread(lg1).start();
        new Thread(lg2).start();
    }
}


В первом приближении и при не очень неудачных стечениях обстоятельств такой код работать будет. Но при неудачных у нас могут появиться две проблемы:
spurious wakeups — описано в джавадоке внутри wait(). Поиск в гугле показал что если писать только под Windows то на нём их вроде как не бывает вообще, но на некоторых системах бывают. И раз об этом написано даже в джавадоке — то уж точно не фигня.
А вторая проблема(в конкретном примере пока не нужная, но мы ведь любим смотреть в будущее) — про возможность deadlock (и раз уж нам придется добавлять код для обработки spirious wakeups — то то состояние — можно будет использовать чтобы в будущем через него определять чей сейчас ход).

Итак поменяем код внутри synchronized на:
synchronized (monitor) {
                    monitor.notifyAll();
                    while (CONDITION) monitor.wait();
                    System.out.println(name);
                }


Что же мы выберем как CONDITION? Тот поток который отработал последним! Измененный код станет таким:
class Leg implements Runnable{
    private final String name;
    private static final Object monitor  = new Object() ;
    public static volatile Leg lastStepped; //состояние потока - последний отработавший поток

    public Leg(String name){
        this.name = name;
    }
    public void run() {
        try{
            while (true) {
                synchronized (monitor) {
                    monitor.notifyAll();
                    while (lastStepped==this) monitor.wait(); //проверка условия что последний отработавший - текущий поток(и если он вдруг сразу проснулся после того как его усыпили - то усыпим его опять)
                    lastStepped=this; //присваиваем что данный поток отработал последним
                    System.out.println(name);
                }
            }
        } catch (InterruptedException e) {
            System.out.println("Leg error");
        }
    }
}

А в мейне добавим установку начального состояния:
public class LegsSynchro
{
    public static void main(String[] args)
    {
        Leg lg1 =  new Leg("left");
        Leg lg2 =  new Leg("right");

        Leg.lastStepped = lg1; //выбираем любую - не важно

        new Thread(lg1).start();
        new Thread(lg2).start();
    }
}


Итог — наш код становится лучше, и более устойчивым к возможным косякам в будущем. Еще можно упомянуть про то что System.out.println() из разных потоков в консоль тоже вполне может начать работать не совсем корректно, потому как вывод у нас вообще говоря в основном потоке программы. Но в данном случае это неважно.

Всем спасибо за внимание — вопросы и предложения по дальнейшему улучшению — в комментарии