Отправляйтесь в волнующую экспедицию по кодированию с нашей серией «Java Journeys», предназначенной исключительно для начинающих учеников, стремящихся разгадать магию программирования на Java.

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

class BankAccount {
    private double balance;

    public BankAccount(double initialBalance) {
        this.balance = initialBalance;
    }

    // Synchronized method to withdraw funds
    public synchronized void withdraw(double amount) {
        if (amount <= balance) {
            System.out.println(Thread.currentThread().getName() + " is withdrawing $" + amount);
            balance -= amount;
        } else {
            System.out.println(Thread.currentThread().getName() + " tried to withdraw $" + amount + " but has insufficient balance.");
        }
    }

    public double getBalance() {
        return balance;
    }
}

class CustomerThread extends Thread {
    private BankAccount account;

    public CustomerThread(BankAccount account) {
        this.account = account;
    }

    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            double withdrawalAmount = Math.random() * 500;
            account.withdraw(withdrawalAmount);
            try {
                Thread.sleep(100); // Simulating some processing time
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

public class SynchronizedMethodExample {
    public static void main(String[] args) {
        BankAccount account = new BankAccount(1000);

        CustomerThread customer1 = new CustomerThread(account);
        CustomerThread customer2 = new CustomerThread(account);

        customer1.setName("Customer 1");
        customer2.setName("Customer 2");

        customer1.start();
        customer2.start();

        try {
            customer1.join();
            customer2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("Final balance: $" + account.getBalance());
    }
}

В этом примере:

  • Класс BankAccount представляет банковский счет с синхронизированным методом withdraw.
  • Класс CustomerThread имитирует клиентов, снимающих деньги с банковского счета.
  • Класс SynchronizedMethodExample устанавливает клиентские потоки и демонстрирует синхронизацию.

Ключевые моменты, на которые следует обратить внимание:

  • Метод withdraw помечен как синхронизированный. Это гарантирует, что только один поток может одновременно выполнять этот метод, предотвращая проблемы параллелизма, такие как перерисовка.
  • Несколько потоков клиентов (CustomerThread) пытаются снять средства одновременно.
  • Метод join используется для ожидания завершения обоих клиентских потоков перед печатью окончательного баланса.

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

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

class Printer {
    // Synchronized block to control access to the printer
    public void printDocument(String document, String threadName) {
        synchronized (this) {
            System.out.println("Printing document: " + document + " by " + threadName);
            try {
                Thread.sleep(100); // Simulating printing time
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("Document " + document + " printed by " + threadName);
        }
    }
}

class PrintingThread extends Thread {
    private Printer printer;
    private String document;

    public PrintingThread(Printer printer, String document) {
        this.printer = printer;
        this.document = document;
    }

    @Override
    public void run() {
        printer.printDocument(document, Thread.currentThread().getName());
    }
}

public class SynchronizedBlockExample {
    public static void main(String[] args) {
        Printer printer = new Printer();

        PrintingThread thread1 = new PrintingThread(printer, "Document 1");
        PrintingThread thread2 = new PrintingThread(printer, "Document 2");
        PrintingThread thread3 = new PrintingThread(printer, "Document 3");

        thread1.setName("Thread 1");
        thread2.setName("Thread 2");
        thread3.setName("Thread 3");

        thread1.start();
        thread2.start();
        thread3.start();

        try {
            thread1.join();
            thread2.join();
            thread3.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

В этом расширенном примере:

  • Класс Printer содержит синхронизированный блок, обеспечивающий доступ к принтеру одновременно только одному потоку.
  • Класс PrintingThread имитирует печать документа с помощью принтера.
  • Класс SynchronizedBlockExample настраивает потоки печати и демонстрирует синхронизацию с использованием синхронизированных блоков.

Ключевые моменты, на которые следует обратить внимание:

  • Синхронизированный блок в методе printDocument блокирует экземпляр объекта Printer, обеспечивая эксклюзивный доступ к принтеру для текущего потока.
  • Несколько потоков PrintingThread пытаются распечатать документы одновременно.
  • Метод join используется для ожидания завершения всех потоков до завершения программы.

В этом примере демонстрируется использование синхронизированных блоков для управления доступом к общему ресурсу и предотвращения проблем параллелизма.

Давайте разберем концепции в расширенном примере с использованием синхронизированных блоков в упрощенном виде:

1. Синхронизированный блок. В классе Printer синхронизированный блок похож на запертую комнату. Только один поток может войти в эту комнату одновременно. Когда поток входит в комнату, он может безопасно использовать принтер, не мешая другим потокам.

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

3. Тема печати. Каждый PrintingThread представляет человека, который хочет распечатать документ. Эти люди не могут пользоваться принтером одновременно, потому что могут испортить распечатки друг друга. Итак, они ждут своей очереди, чтобы войти в комнату с принтером (синхронизированный блок) для печати.

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

5. Смоделированная печать. Чтобы сделать ее более реалистичной, мы моделируем время печати с помощью Thread.sleep(100). Это похоже на то, что принтеру нужно время, чтобы распечатать документ. Пока один человек печатает, другие ждут за пределами комнаты с принтером.

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

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