Рубрики
Без рубрики

Многопоточность Java. Основы

В этой статье я попытаюсь объяснить, как работать с многопоточностью в java. Помечено как java, многопоточность, программное обеспечение, программирование.

Зачем вам нужна многопоточность

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

С появлением операционных систем стало возможным запускать сразу несколько программ. Программы запускались в процессе, то есть в изолированной, независимой среде, для которой операционная система выделяла такие ресурсы, как память, файловые дескрипторы.

Необходимость разработки операционных систем с поддержкой многопоточности была обусловлена следующими факторами:

  • Использование ресурсов. Программам иногда приходится ждать какой-то сторонней операции, такой как ввод и вывод, и пока мы ждали, не было возможности выполнить какую-то полезную работу.
  • Справедливость в распределении ресурсов. Многие пользователи и программы могут иметь одинаковые права на компьютерные ресурсы. Предпочтительнее распределять все ресурсы между ними.
  • Удобство. Часто проще и проще написать несколько программ, каждая из которых выполняет разные задачи, а затем координировать их, чем написать одну программу, которая выполняет все задачи.

Простой потокобезопасный счетчик

public class Sequence {
   private int value;

   public synchronized int getNext() {
       return value++;
   }
}

Этот счетчик потокобезопасен, поскольку метод GetNext () содержит ключевое слово synchronized, которое используется для обеспечения потокобезопасности.

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

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

  • Ограничить доступ к таким переменным
  • Сделайте состояния неизменяемыми
  • Используйте синхронизацию для доступа к переменным состояния

Атомарность

public class Counter {
    private int long value;

    public int getNext() {
         return value++;
    }
}

Условия гонки

Представьте, что вы встречаетесь со своим другом в Starbucks рядом с университетом в 12 часов дня. Но когда вы приехали, оказалось, что есть два Старбакса, и вы не уверены, какая встреча состоится. В 12:10 вы не увидели своего друга в Starbucks A, поэтому решили зайти в Starbucks B, чтобы проверить, там ли ваш друг, но, как оказалось, его там не было. Возможны следующие результаты

  • Ваш друг опаздывает и его нет ни в одном из Старбаксов
  • Ваш друг пришел в Старбакс А, когда вы ушли в Старбакс, Но
  • Ваш друг был в Starbucks И искал вас, но он не нашел его и пошел в Starbucks A

Давайте представим себе наихудшую ситуацию. В 12:15 вечера вы посетили оба Старбакса. Что ты будешь делать?

Ты вернешься в Старбакс А? Сколько раз ты собираешься туда возвращаться?

До тех пор, пока не будет разработан протокол согласованности, вы оба можете весь день ходить между Starbucks.

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

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

public class LazyInitRace {
    private ExpensiveObject instance = null;

    public ExpensiveObject getInstance() {
        if (instance == null) {
           instance = new ExpensiveObject();
        }
        return instance;
    }
}
if (!vector.contains(element)) {
    vector.add(element);
}

Считаете ли вы, что этот код потокобезопасен?

Ответ: Нет. Эта попытка поместить элемент в коллекцию, если такого элемента нет, имеет условие гонки, даже если оба метода add & contains являются атомарными. Методы с ключевым словом synchronized в их подписи могут выполнять отдельные операции атомарно, но требуется дополнительная синхронизация, когда оба таких метода объединяются в каком-либо сложном действии. Не злоупотребляйте ключевым словом synchronized, так как чрезмерная синхронизация может привести к проблемам с производительностью.

Изменчивые переменные

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

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

Используйте volatile только в следующих случаях

  • Для описания переменных, которые не зависят от их текущего значения
  • Эта переменная не участвует в инвариантах с другими переменными состояния
  • При доступе к переменной блокировка не требуется по какой-либо другой причине

Тупик

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

public class TestDeadlockExample1 {  
  public static void main(String[] args) {  
    final String resource1 = "dev.to";  
    final String resource2 = "dev.architecture";  
    // t1 tries to lock resource1 then resource2  
    Thread t1 = new Thread() {  
      public void run() {  
          synchronized (resource1) {  
           System.out.println("Thread 1: locked resource 1");  

           try { Thread.sleep(150);} catch (Exception e) {}  

           synchronized (resource2) {  
            System.out.println("Thread 1: locked resource 2");  
           }  
         }  
      }  
    };  

    // t2 tries to lock resource2 then resource1  
    Thread t2 = new Thread() {  
      public void run() {  
        synchronized (resource2) {  
          System.out.println("Thread 2: locked resource 2");  

          try { Thread.sleep(150);} catch (Exception e) {}  

          synchronized (resource1) {  
            System.out.println("Thread 2: locked resource 1");  
          }  
        }  
      }  
    };  


    t1.start();  
    t2.start();  
  }  
}

Голод

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

Живая блокировка

Поток часто действует в ответ на действие другого потока. Если действие другого потока также является ответом на действие другого потока, то может возникнуть оперативный блок. Как и в случае взаимоблокировки, заблокированные потоки не могут двигаться дальше. Однако потоки не заблокированы – они просто слишком заняты, отвечая друг другу, чтобы возобновить работу. Это сравнимо с двумя людьми, пытающимися пройти друг мимо друга в коридоре: Андрей двигается влево, чтобы пропустить Григория, а Григорий двигается вправо, чтобы пропустить Андрея. Видя, что они все еще блокируют друг друга, Андрей двигается вправо, а Григорий – влево.

Синхронизация потоков и ее применение

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

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

Оригинал: “https://dev.to/vrnsky/java-multithreading-basics-4cdc”