Этот пост был первоначально опубликован в Этот пост был первоначально опубликован в
В этой статье основное внимание будет уделено тому, что делает ключевое слово synchronized в Java и когда его следует использовать.
Чтобы лучше понять эту статью, пожалуйста, ознакомьтесь с моей статьей Введение в многопоточность на случай, если вы новичок в многопоточности в Java.
Давайте возьмем следующий фрагмент кода:
public void doSomething(){
//Execute Logic 1
//Execute Logic 2
......
}
Допустим, что 2 потока t1 и t2 будут вызывать метод doSomething() .
Здесь главное, о чем мы заботимся, это то, что если один поток вызывает doSomething(), то ему нужно завершить все в методе, прежде чем другой поток вызовет doSomething() .
В основном что-то вроде этого:
t1 executes Logic 1 t1 executes Logic 2 t2 executes Logic 1 t2 executes Logic 2
или
t2 executes Logic 1 t2 executes Logic 2 t1 executes Logic 1 t1 executes Logic 2
Но реальность такова, что они могут выполняться в любом порядке. . Приведенный ниже сценарий весьма вероятен:
t1 executes Logic 1 t2 executes Logic 1 t1 executes Logic 2 t2 executes Logic 2
Но что, если мы хотим убедиться, что t1 полностью завершит do Something() до того, как начнется t2 do Something() ?
Вот тут-то и вступает в дело синхронизация. Синхронизация гарантирует, что любой синхронизированный метод или фрагмент кода должны быть полностью завершены одним потоком, прежде чем другой поток сможет получить доступ к этому блоку.
Если мы сделаем doSomething() синхронизированным, как показано ниже, то только один поток может получить доступ к do Something() одновременно
public synchronized void doSomething(){
//Execute Logic 1
//Execute Logic 2
......
}
Обычный Синхронизированный Метод
В этом разделе мы создадим 3 потока, которые будут печатать числа от 0 до 5.
Сначала давайте создадим простую утилиту, которая будет печатать числа от 0 до 5:
class PrintUtil {
public synchronized void printNumbers() {
for (int i = 0; i <= 5; i++) {
System.out.println(Thread.currentThread().getName() + ": " + i);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
Эта утилита имеет метод, называемый print Numbers() . Этот метод печатает числа от 0 до 5 и ожидает 100 мс после каждой печати. Метод помечен как синхронизированный так что в любой момент только один поток может получить доступ к методу printNumbers()
Далее давайте создадим рабочий, который будет реализовывать управляемый интерфейс:
class RunnableWorker implements Runnable {
PrintUtil pu;
public RunnableWorker(PrintUtil pu) {
this.pu = pu;
}
@Override
public void run() {
pu.printNumbers();
}
}
Это очень простой рабочий процесс, который вызывает метод printNumbers() в классе Print Util .
Наконец давайте создадим основной класс который свяжет все это воедино:
public class SynchronizedMethodDemo {
public static void main(String[] args) {
PrintUtil pu = new PrintUtil();
Runnable r = new RunnableWorker(pu);
Thread t1 = new Thread(r);
Thread t2 = new Thread(r);
Thread t3 = new Thread(r);
t1.start();
t2.start();
t3.start();
}
}
Основной класс создает экземпляр PrintUtil.
Затем он создает 3 потока t1, t2 и t3, передавая Runnable Worker в качестве аргумента, а затем запускает 3 потока.
Выходные данные вышеупомянутой программы приведены ниже:
Thread-0: 0 Thread-0: 1 Thread-0: 2 Thread-0: 3 Thread-0: 4 Thread-0: 5 Thread-2: 0 Thread-2: 1 Thread-2: 2 Thread-2: 3 Thread-2: 4 Thread-2: 5 Thread-1: 0 Thread-1: 1 Thread-1: 2 Thread-1: 3 Thread-1: 4 Thread-1: 5
Как видно выше, Synchronized гарантирует, что только тогда, когда один поток завершает печать от 0 до 5, другой поток начинает его выполнение.
Как это работает?
В этом примере экземпляр объекта Print Util /имеет блокировку. Когда один поток выполняет метод printNumbers() , он получает блокировку от pu. Этот поток снимает блокировку только тогда, когда выполнение print Numbers() полностью завершено. Другие потоки могут выполнять print Numbers() только тогда, когда они получают блокировку.
Поскольку каждый поток удерживает блокировку до тех пор, пока printNumbers() не будет полностью завершен, мы получаем результат, показанный выше.
В общем случае блокировка удерживается объектом, который вызывает синхронизированный метод. В этом случае этот объект был помещен
Синхронизировано со статическими методами
В этом примере мы используем статический метод для печати чисел от 0 до 5 вместо метода экземпляра.
Код утилиты принтера показан ниже:
class PrintUtilExample2 {
public static synchronized void printNumbers() {
for (int i = 0; i <= 5; i++) {
System.out.println(Thread.currentThread().getName() + ": " + i);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
Здесь метод помечен как статический и синхронизированный.
Далее давайте создадим рабочего. Код для рабочего показан ниже:
class RunnableWorkerExample2 implements Runnable {
@Override
public void run() {
PrintUtilExample2.printNumbers();
}
}
В этом случае рабочий вызывает print Numbers() напрямую, используя имя класса, поскольку это статический метод.
Наконец, у нас есть основной класс, который показан ниже:
public class SynchronizedStaticMethodDemo {
public static void main(String[] args) {
Runnable r = new RunnableWorkerExample2();
Thread t1 = new Thread(r);
Thread t2 = new Thread(r);
Thread t3 = new Thread(r);
t1.start();
t2.start();
t3.start();
}
}
Здесь также основной метод создает три потока t1, t2 и t3 с аргументом в качестве выполняемого рабочего примера 2 и запускает потоки.
Выходные данные этого кода приведены ниже:
Thread-2: 0 Thread-2: 1 Thread-2: 2 Thread-2: 3 Thread-2: 4 Thread-2: 5 Thread-0: 0 Thread-0: 1 Thread-0: 2 Thread-0: 3 Thread-0: 4 Thread-0: 5 Thread-1: 0 Thread-1: 1 Thread-1: 2 Thread-1: 3 Thread-1: 4 Thread-1: 5
Снова мы видим, что каждый поток завершает печать от 0 до 5, прежде чем следующий поток начнет печать.
Как это работает
В случае статических методов блокировка поддерживается самим классом PrintUtilExample2. Остальная часть механизма блокировки точно такая же, как упоминалось в предыдущем разделе.
Создание синхронизированных блоков с пользовательскими объектами
В этом разделе мы увидим, как синхронизировать блок кода с помощью пользовательского объекта.
Класс утилиты печати показан ниже:
class PrintUtilExample3 {
final Object lockObject = new Object();
public void printNumbers() {
synchronized(lockObject) {
for (int i = 0; i <= 5; i++) {
System.out.println(Thread.currentThread().getName() + ": " + i);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
Здесь метод printNumbers() не помечен как синхронизированный.
Мы создаем объект с именем lock Object . Затем мы создаем синхронизированный блок кода, используя synchronized(lockObject){}
Преимущество использования этого подхода заключается в том, что мы можем пометить любой пользовательский блок кода как синхронизированный. (Это может быть целый метод, или это может быть даже одна строка во всем методе. )
Рабочий код показан ниже:
class RunnableWorkerExample3 implements Runnable {
PrintUtilExample3 pu3;
public RunnableWorkerExample3(PrintUtilExample3 pu3){
this.pu3 = pu3;
}
@Override
public void run() {
pu3.printNumbers();
}
}
Основной класс показан ниже:
public class SynchronizedCustomObjectDemo {
public static void main(String[] args) {
PrintUtilExample3 pu3 = new PrintUtilExample3();
Runnable r = new RunnableWorkerExample3(pu3);
Thread t1 = new Thread(r);
Thread t2 = new Thread(r);
Thread t3 = new Thread(r);
t1.start();
t2.start();
t3.start();
}
}
Результат вышеупомянутой программы показан ниже:
Thread-0: 0 Thread-0: 1 Thread-0: 2 Thread-0: 3 Thread-0: 4 Thread-0: 5 Thread-2: 0 Thread-2: 1 Thread-2: 2 Thread-2: 3 Thread-2: 4 Thread-2: 5 Thread-1: 0 Thread-1: 1 Thread-1: 2 Thread-1: 3 Thread-1: 4 Thread-1: 5
В этом подходе мы также видим, что каждый поток завершает печать от 0 до 5, прежде чем следующий поток начнет печатать числа.
Как это работает
В этом случае пользовательский объект, созданный lock Object , удерживает блокировку. Остальная часть механизма блокировки такая же, как упоминалось в предыдущих разделах.
Также в предыдущих разделах весь метод был помечен как синхронизированный. Но при таком подходе любой блок кода может быть помечен как синхронизированный, что обеспечивает большую гибкость.
Весь код, обсуждаемый в этой статье, можно найти в этом репозитории GitHub .
Синхронизация действительно сопряжена с некоторыми проблемами. Могут быть сценарии, в которых мы хотим, чтобы блок кода синхронизировался только для операций записи. Но для операций чтения должно быть разрешено любое количество одновременных потоков. Для того, чтобы справиться с этими сценариями, в Java существуют другие лучшие подходы, о которых я расскажу в одной из будущих статей.
Теперь вы знаете, что такое синхронизация и как ее использовать в Java.
Не стесняйтесь связаться со мной в LinkedIn или подписаться на меня в Twitter.
Если вам понравился этот пост, вы можете зайти на мой сайт https://adityasridhar.com для других подобных сообщений
Оригинал: “https://dev.to/adityasridhar/what-is-the-use-of-synchronized-keyword-in-java-5628”