Механизм взаимодействия между потоками

Java задняя часть GitHub товар

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

Но на самом деле бывает и такая ситуация: если поток получает блокировку, но в процессе выполнения отсутствуют какие-то условия, например ресурс для запроса к БД еще не пришел, данные команды чтения с диска не возвращены, и т. д. В этом случае это пустая трата ресурсов, чтобы позволить потоку все еще занимать ожидание ЦП.

Следовательно, на каждом объекте также есть очередь ожидания, которая блокирует все потоки, которые получают блокировки и не выполняют определенные условия во время работы, поэтому состояние блокировки и очереди всего объекта такое.

image

Entry Set блокирует все потоки, которым не удалось получить блокировку текущего объекта, а Wait Set блокирует все потоки, которые отказываются от ЦП из-за невыполнения определенных условий во время получения блокировки.

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

метод ожидания/уведомления

В классе Object есть несколько методов, которые мы редко используем, но они являются основными методами взаимодействия потоков.Мы используем эти методы для управления взаимодействием между потоками.

public final native void wait(long timeout)

public final void wait()

public final native void notify();

public final native void notify();

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

Следовательно, эти два метода в основном предназначены для работы с очередью ожидания объекта, то есть для блокировки и освобождения потоков, которые получают блокировку, но не имеют условий для продолжения выполнения во время работы.

Но есть посылка, на которую все должны обратить внимание: Wait and notify действуют в очереди ожидания встроенной блокировки объекта, то есть поток в очереди ожидания может быть заблокирован и освобожден только предпосылка получения встроенного замка объекта. Проще говоря,Эти два метода можно вызывать только внутри синхронизированных измененных блоков кода..

Давайте посмотрим на кусок кода:

public class Test {
    private static Object lock = new Object();

    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(){
            @Override
            public void run(){
                synchronized (lock){
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        Thread thread2 = new Thread(){
            @Override
            public void run(){
                synchronized (lock){
                    System.out.println("hello");
                }
            }
        };
        thread1.start();
        thread2.start();

        Thread.sleep(2000);

        System.out.println(thread1.getState());
        System.out.println(thread2.getState());
    }
}

результат операции:

image

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

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

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

Какой смысл демонстрировать такую ​​программу? Я просто хочу сказать вам, что хотя потоки в очереди блокировки и очереди ожидания не могут заставить ЦП нормально выполнять инструкции, они принадлежат к двум разным состояниям.Потоки в очереди блокировки будут честно конкурировать за ресурсы блокировки, узнав, что блокировка была освобождена.Потоки в очереди ожидания должны быть уведомлены другими потоками, вызвав метод уведомления, и перейти из очереди ожидания в очередь блокировки, чтобы повторно конкурировать за ресурсы блокировки.

Реализация связанных методов

1. Метод сна

Метод сна используется для блокировки текущего потока на указанный период времени, и состояние потока становится TIMED_WAITING, но это отличается от метода ожидания. Оба уступают ЦП, но метод сна не освобождает текущую блокировку.

Другими словами, метод сна не является методом синхронизации и взаимодействия между потоками. Он только позволяет потоку временно передать ЦП и приостановить работу на определенный период времени. Когда время истекло, ЦП будет выделен. системой для продолжения выполнения.

2. Способ объединения

Метод соединения используется для реализации операции ожидания двух потоков друг друга, см. код:

public void testJoin() throws InterruptedException {
    Thread thread = new Thread(){
        @Override
        public void run(){
            for (int i=0; i<1000; i++)
                System.out.println(i);
        }
    };
    thread.start();

    thread.join();
    System.out.println("main thread finished.....");
}

Помимо метода соединения, сначала должен быть выполнен метод печати в основном потоке, но на самом деле эта программа выполнит метод печати основного потока после завершения выполнения потока потока.

Механизм реализации отличается от метода сна, давайте посмотрим:

image

image

Суть метода заключается в вызове wait(delay) для блокировки текущего потока.Когда поток пробуждается, вычисляется, сколько времени прошло с момента входа в метод до текущего момента времени.

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

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

Типичная проблема синхронизации потоков

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

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

public class Repository {
    private List<Integer> list = new ArrayList<>();
    private int limit = 10;  //设置仓库容量上限

    public synchronized void addGoods(int count) throws InterruptedException {
        while(list.size() == limit){
            //达到仓库上限,不能继续生产
            wait();
        }
        list.add(count);
        System.out.println("生产者生产产品:" + count);
        //通知所有的消费者
        notifyAll();
    }

    public synchronized void removeGoods() throws InterruptedException {
        while(list.size() <= 0){
            //仓库中没有产品
            wait();
        }

        int res = list.get(0);
        list.remove(0);
        System.out.println("消费者消费产品:" + res);
        //通知所有的生产者
        notifyAll();
    }
}

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

public class Producer extends Thread{
    Repository repository = null;

    public Producer(Repository p){
        this.repository = p;
    }

    @Override
    public void run(){
        int count = 1;
        while(true){
            try {
                Thread.sleep((long) (Math.random() * 500));
                repository.addGoods(count++);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

Определите класс производителя, который случайным образом добавляет товары на склад. Если нет успешного добавления, оно будет заблокировано в цикле.

public class Customer extends Thread{
    Repository repository = null;

    public Customer(Repository p){
        this.repository = p;
    }

    @Override
    public void run(){
        while(true){
            try {
                Thread.sleep((long) (Math.random() * 500));
                repository.removeGoods();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

Определите класс потребителей, который случайным образом выбирает продукт со склада. Если продукт не будет успешно извлечен, он также будет заблокирован в цикле.

public void testProducerAndCustomer() {
    Repository repository = new Repository();
    Thread producer = new Producer(repository);
    Thread consumer = new Customer(repository);

    producer.start();
    consumer.start();

    producer.join();
    consumer.join();
    System.out.println("main thread finished..");
}

Основной поток запускает эти два потока, и программа работает примерно так:

生产者生产产品:1
消费者消费产品:1
生产者生产产品:2
消费者消费产品:2
生产者生产产品:3
消费者消费产品:3
。。。。。
。。。。。
消费者消费产品:17
生产者生产产品:21
消费者消费产品:18
生产者生产产品:22
消费者消费产品:19
生产者生产产品:23
消费者消费产品:20
生产者生产产品:24
生产者生产产品:25
生产者生产产品:26
消费者消费产品:21
生产者生产产品:27
生产者生产产品:28
消费者消费产品:22
消费者消费产品:23
生产者生产产品:29
生产者生产产品:30
。。。。。。
。。。。。。

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

Скорость выполнения потока-производителя может превышать скорость потока-потребителя, и если скорость выполнения потока-потребителя превышает скорость производителя, емкость хранилища будет пустой и он будет заблокирован.

Подводя итог, блок кода, измененный с помощью synchronized, представляет собой очередь блокировки встроенной блокировки объекта, который используется непосредственно, поток не может получить блокировку и, естественно, блокируется в очереди, а ожидание/уведомление — это то, что мы вручную управлять входом и выходом из очереди ожидания. Но по сути это две очереди, использующие встроенную блокировку объекта.

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


Весь код, изображения, файлы в статье хранятся в облаке на моем GitHub:

(GitHub.com/один батат/о…)

Добро пожаловать в официальную учетную запись WeChat: OneJavaCoder, все статьи будут синхронизированы в официальной учетной записи.

image