Синхронизация в Java

В многопоточном программировании синхронизация (synchronization) играет ключевую роль для обеспечения безопасной работы нескольких потоков с общими ресурсами. Без синхронизации данные могут стать неконсистентными или повреждёнными, если несколько потоков одновременно обращаются и изменяют общие переменные. В Java синхронизация — это механизм, который гарантирует, что в любой момент времени доступ к ресурсу имеет только один поток. Этот процесс помогает избежать таких проблем, как несогласованность данных и состояния гонки (race conditions), возникающих при взаимодействии нескольких потоков с общими ресурсами.

Пример

Ниже представлен пример программы на Java, демонстрирующий синхронизацию.


// Java Program to demonstrate synchronization in Java
class Counter {
    private int c = 0; // Общая переменная

    // Синхронизированный метод для увеличения счетчика
    public synchronized void inc() {
        c++;
    }

    // Синхронизированный метод для получения значения счетчика
    public synchronized int get() {
        return c;
    }
}

public class Geeks {
    public static void main(String[] args) {
        Counter cnt = new Counter(); // Общий ресурс

        // Поток 1 увеличивает счетчик
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                cnt.inc();
            }
        });

        // Поток 2 увеличивает счетчик
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                cnt.inc();
            }
        });

        // Запускаем оба потока
        t1.start();
        t2.start();

        // Ожидаем завершения потоков
        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // Вывод итогового значения счетчика
        System.out.println("Counter: " + cnt.get());
    }
}

Вывод программы:

Counter: 2000

Объяснение:
Два потока — t1 и t2 — одновременно увеличивают общий счетчик. Методы inc() и get() помечены как синхронизированные, то есть в один момент времени только один поток может выполнить эти методы, что предотвращает состояние гонки. Благодаря этому итоговое значение счетчика всегда будет корректным, обновлённым обоими потоками.


Необходимость синхронизации

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


Синхронизированные блоки в Java

Java предоставляет возможность создавать потоки и синхронизировать их задачи с помощью синхронизированных блоков (synchronized blocks).

Синхронизированный блок в Java синхронизируется на определённом объекте и выделяется ключевым словом synchronized. Все синхронизированные блоки, которые синхронизируются на одном и том же объекте, допускают выполнение только одного потока внутри блока одновременно. Остальные потоки, которые попытаются войти в этот блок, будут заблокированы, пока текущий поток не выйдет из блока.

Если вы хотите в совершенстве освоить многопоточность и понять, как избегать ошибок, связанных с параллельностью, синхронизированные блоки — важная часть этого процесса.

Общая форма синхронизированного блока


synchronized(sync_object)
{
   // Доступ к общим переменным и другим ресурсам
}

В Java эта синхронизация реализована через понятия монитор (monitor) или замок (lock). В данный момент времени лишь один поток может владеть монитором. Когда поток захватывает замок, говорят, что он вошёл в монитор. Остальные потоки, пытающиеся войти в заблокированный монитор, приостанавливаются до выхода первого потока.


Пример

Ниже показан пример синхронизации с использованием синхронизированного блока.


// Java Program to demonstrate synchronization block in Java

class Counter {
    private int c = 0; // Общая переменная

    // Метод с синхронизированным блоком
    public void inc() {
        synchronized(this) { // Синхронизируем только этот блок
            c++;
        }
    }

    // Метод для получения значения счетчика
    public int get() {
        return c;
    }
}

public class Geeks {
    public static void main(String[] args) {
        Counter cnt = new Counter(); // Общий ресурс

        // Поток 1 увеличивает счетчик
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                cnt.inc();
            }
        });

        // Поток 2 увеличивает счетчик
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                cnt.inc();
            }
        });

        // Запускаем оба потока
        t1.start();
        t2.start();

        // Ожидаем завершения потоков
        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // Вывод итогового значения счетчика
        System.out.println("Counter: " + cnt.get());
    }
}

Вывод программы:

Counter: 2000

Виды синхронизации

В Java можно выделить два основных вида синхронизации:

  1. Синхронизация процессов (Process Synchronization)
  2. Синхронизация потоков (Thread Synchronization)

1. Синхронизация процессов в Java

Синхронизация процессов — это техника координации выполнения нескольких процессов. Она гарантирует сохранность и упорядоченность общих ресурсов.

Пример

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


// Java Program to demonstrate Process Synchronization
class BankAccount {
    private int balance = 1000; // Общий ресурс (баланс банка)

    // Синхронизированный метод для внесения депозита
    public synchronized void deposit(int amount) {
        balance += amount;
        System.out.println("Deposited: " + amount + ", Balance: " + balance);
    }

    // Синхронизированный метод для снятия средств
    public synchronized void withdraw(int amount) {
        if (balance >= amount) {
            balance -= amount;
            System.out.println("Withdrawn: " + amount + ", Balance: " + balance);
        } else {
            System.out.println("Insufficient balance to withdraw: " + amount);
        }
    }

    public int getBalance() {
        return balance;
    }
}

// Основной класс
public class Geeks {
    public static void main(String[] args) {
        BankAccount account = new BankAccount(); // Общий ресурс

        // Поток 1 для внесения депозита
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 3; i++) {
                account.deposit(200);
                try {
                    Thread.sleep(50); // Имитация задержки
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        // Поток 2 для снятия средств
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 3; i++) {
                account.withdraw(100);
                try {
                    Thread.sleep(100); // Имитация задержки
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        // Запускаем оба потока
        t1.start();
        t2.start();

        // Ожидаем завершения потоков
        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // Вывод окончательного баланса
        System.out.println("Final Balance: " + account.getBalance());
    }
}

Вывод программы:

Withdrawn: 100, Balance: 900
Deposited: 200, Balance: 1100
Deposited: 200, Balance: 1300
Withdrawn: 100, Balance: 1200
Deposited: 200, Balance: 1400
Withdrawn: 100, Balance: 1300
Final Balance: 1300

Объяснение:
Демонстрируется синхронизация процессов на примере банковского счёта с операциями внесения и снятия средств. Два потока, один осуществляет депозит, другой — снятие. Методы deposit() и withdraw() синхронизированы для обеспечения безопасности потоков и предотвращения состояния гонки при одновременном доступе к балансу.


2. Синхронизация потоков в Java

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

  1. Взаимное исключение (Mutual Exclusion)
  2. Взаимодействие (Cooperation) [межпоточное взаимодействие в Java]

Пример

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


// Java Program to demonstrate thread synchronization for Ticket Booking System
class TicketBooking {
    private int availableTickets = 10; // Общий ресурс (количество билетов)

    // Синхронизированный метод бронирования билетов
    public synchronized void bookTicket(int tickets) {
        if (availableTickets >= tickets) {
            availableTickets -= tickets;
            System.out.println("Booked " + tickets + " tickets, Remaining tickets: " + availableTickets);
        } else {
            System.out.println("Not enough tickets available to book " + tickets);
        }
    }

    public int getAvailableTickets() {
        return availableTickets;
    }
}

public class Geeks {
    public static void main(String[] args) {
        TicketBooking booking = new TicketBooking(); // Общий ресурс

        // Поток 1 бронирует билеты
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 2; i++) {
                booking.bookTicket(2); // Пытается забронировать по 2 билета
                try {
                    Thread.sleep(50); // Имитация задержки
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        // Поток 2 бронирует билеты
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 2; i++) {
                booking.bookTicket(3); // Пытается забронировать по 3 билета
                try {
                    Thread.sleep(40); // Имитация задержки
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        // Запускаем оба потока
        t1.start();
        t2.start();

        // Ожидаем завершения потоков
        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // Вывод оставшегося количества билетов
        System.out.println("Final Available Tickets: " + booking.getAvailableTickets());
    }
}

Вывод программы:

Booked 2 tickets, Remaining tickets: 8
Booked 3 tickets, Remaining tickets: 5
Booked 3 tickets, Remaining tickets: 2
Booked 2 tickets, Remaining tickets: 0
Final Available Tickets: 0

Объяснение:
Класс TicketBooking содержит синхронизированный метод bookTicket(), который гарантирует, что билеты бронирует только один поток за раз. Это предотвращает конфликт и перепродажу билетов. Каждый поток пытается забронировать определённое количество билетов с задержкой между попытками. Синхронизация обеспечивает безопасный доступ и обновление переменной availableTickets.


Взаимное исключение (Mutual Exclusion)

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

  1. Синхронизированные методы
  2. Синхронизированные блоки
  3. Статическая синхронизация

Пример

Ниже демонстрируется работа синхронизации на примере отправки сообщений.


// A Java program to demonstrate working of synchronized.
import java.io.*;

// Класс для отправки сообщения
class Sender {
    public void send(String msg) {
        System.out.println("Sending " + msg);  // Вывод сообщения
        try {
            Thread.sleep(100);
        } catch (Exception e) {
            System.out.println("Thread  interrupted.");
        }
        System.out.println(msg + " Sent");  // Сообщение отправлено
    }
}

// Класс отправки сообщения с использованием потоков
class ThreadedSend extends Thread {
    private String msg;
    Sender sender;

    // Конструктор принимает сообщение и объект Sender
    ThreadedSend(String m, Sender obj) {
        msg = m;
        sender = obj;
    }

    public void run() {
        // Синхронизация на объекте sender — только один поток шлёт сообщение одновременно
        synchronized(sender) {
            sender.send(msg);
        }
    }
}

// Класс для запуска программы
class Geeks {
    public static void main(String args[]) {
        Sender send = new Sender();
        ThreadedSend S1 = new ThreadedSend("Hi ", send);
        ThreadedSend S2 = new ThreadedSend("Bye ", send);

        // Запускаем два потока ThreadedSend
        S1.start();
        S2.start();

        // Ожидаем завершения потоков
        try {
            S1.join();
            S2.join();
        } catch (Exception e) {
            System.out.println("Interrupted");
        }
    }
}

Вывод программы:

Sending Hi 
Hi  Sent
Sending Bye 
Bye  Sent

Объяснение:
В примере объект Sender синхронизируется внутри метода run() класса ThreadedSend. Альтернативно можно синхронизировать весь метод send(), что даст тот же результат и позволит не синхронизировать объект внутри run().

Иногда не нужно синхронизировать весь метод. Часто достаточно синхронизировать только часть метода — синхронизированные блоки позволяют делать это.


Пример синхронизированного метода с использованием анонимного класса


// Java Program to synchronized method by
// using an anonymous class
import java.io.*;

class Test {
    synchronized void test_func(int n) {
        // Синхронизированный метод
        for (int i = 1; i <= 3; i++) {
            System.out.println(n + i);
            try {
                Thread.sleep(100);
            } catch (Exception e) {
                System.out.println(e);
            }
        }
    }
}

public class Geeks {
    // Главная функция
    public static void main(String args[]) {
        // Только один объект
        final Test O = new Test();

        Thread a = new Thread() {
            public void run() {
                O.test_func(15);
            }
        };

        Thread b = new Thread() {
            public void run() {
                O.test_func(30);
            }
        };

        a.start();
        b.start();
    }
}

Вывод программы:

16
17
18
31
32
33

Объяснение:
В классе Test есть синхронизированный метод test_func(), который выводит последовательность чисел с небольшой задержкой, обеспечивая безопасность при параллельном доступе из нескольких потоков. Создаются два потока через анонимные классы, каждый из которых вызывает test_func() с разными параметрами. Ключевое слово synchronized гарантирует, что метод выполняется одним потоком за раз.


🔑 Ключевые моменты:

  • Синхронизация обеспечивает безопасность при работе нескольких потоков с общими ресурсами, предотвращая состояния гонки и некорректное изменение данных.
  • В Java синхронизация реализуется через синхронизированные методы и блоки, используя монитор (lock) для последовательного доступа.
  • Синхронизированные блоки позволяют ограничить область синхронизации только нужным участком кода, повышая производительность.
  • Существуют два основных типа синхронизации — процессов и потоков, каждый из которых решает свои задачи в многозадачности.
  • Взаимное исключение — один из способов синхронизации, реализуемый через синхронизированные методы, блоки и статическую синхронизацию.

Ответить

Ваш адрес email не будет опубликован. Обязательные поля помечены *