Въведение в модела на паметта на Java

Моделът на паметта на Java (JMM) описва поведението на нишките в среда за изпълнение на Java. Моделът на паметта е част от семантиката на езика Java и описва Howво може и Howво не трябва да очаква програмистът, когато разработва софтуер не за конкретна Java машина, а за Java като цяло.

Оригиналният модел на паметта на Java (който по-специално се отнася до "percolocal memory"), разработен през 1995 г., се счита за провал: много оптимизации не могат да бъдат напequalsи, без да се загуби гаранцията за безопасност на codeа. По-специално, има няколко опции за писане на многонишков "единичен":

  • or всеки акт на достъп до сингълтон (дори когато обектът е създаден преди много време и нищо не може да се промени) ще предизвика заключване между нишки;
  • or при определен набор от обстоятелства системата ще издаде незавършен самотник;
  • or при определен набор от обстоятелства системата ще създаде двама самотници;
  • or дизайнът ще зависи от поведението на конкретна машина.

Следователно механизмът на паметта е преработен. През 2005 г., с пускането на Java 5, беше представен нов подход, който беше допълнително подобрен с пускането на Java 14.

Новият модел се основава на три правила:

Правило #1 : Програмите с една нишка се изпълняват псевдопоследователно. Това означава: в действителност процесорът може да изпълнява няколко операции на часовник, като в същото време променя техния ред, но всички зависимости на данните остават, така че поведението не се различава от последователното.

Правило номер 2 : няма стойности от нищото. Четенето на която и да е променлива (с изключение на енергонезависимите long и double, за които това правило може да не важи) ще върне or стойността по подразбиране (нула), or нещо, записано там от друга команда.

И правило номер 3 : останалите събития се изпълняват по ред, ако са свързани със строга частична връзка на реда „изпълнява се преди“ ( се случва преди ).

Случва се преди

Лесли Лампорт излезе с концепцията за Случва се преди . Това е стриктна връзка на частичен ред, въведена между атомарните команди (++ и -- не са атомарни) и не означава "физически преди".

В него се казва, че вторият отбор ще бъде "в течение" за промените, напequalsи от първия.

Случва се преди

Например, едното се изпълнява преди другото за такива операции:

Синхронизация и монитори:

  • Заснемане на монитора ( метод на заключване , синхронизирано стартиране) и всичко, което се случва в същата нишка след него.
  • Връщането на монитора ( отключване на метод , край на синхронизирано) и всичко, което се случва в същата нишка преди него.
  • Връщане на монитора и след това заснемането му от друга нишка.

Писане и четене:

  • Писане във всяка променлива и след това четене в същия поток.
  • Всичко в една и съща нишка преди запис в променливата volatile и самото писане. volatile read и всичко в същата тема след него.
  • Записване в променлива променлива и след това четене отново. Нестабилният запис взаимодейства с паметта по същия начин като връщането на монитора, докато четенето е като улавяне. Оказва се, че ако една нишка пише в volatile променлива и втората я намери, всичко, което предхожда записа, се изпълнява преди всичко, което идва след четенето; виж снимката.

Поддръжка на обекта:

  • Статична инициализация и всяHowви действия с всяHowви екземпляри на обекти.
  • Писане в крайните полета в конструктора и всичко след конструктора. По изключение релацията случва-преди не се свързва транзитивно с други правила и следователно може да предизвика надпревара между нишките.
  • Всяка работа с обекта и finalize() .

Услуга за поточно предаване:

  • Стартиране на нишка и всеки code в нишката.
  • Нулиране на променливи, свързани с нишката и всеки code в нишката.
  • Код в нишка и join() ; code в нишката и isAlive() == false .
  • interrupt() нишката и открива, че тя е спряла.

Случва се преди работа нюанси

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

Нека видим How това знание ще ни помогне да коригираме нашия пример. В този случай всичко е много просто: просто премахнете външната проверка и оставете синхронизацията такава, Howвато е. Сега втората нишка е гарантирана, че ще види всички промени, защото ще получи монитора едва след като другата нишка го освободи. И тъй като той няма да го пусне, докато всичко не бъде инициализирано, ще видим всички промени наведнъж, а не поотделно:

public class Keeper {
    private Data data = null;

    public Data getData() {
        synchronized(this) {
            if(data == null) {
                data = new Data();
            }
        }

        return data;
    }
}

Записването в променлива променлива се случва - преди четене от същата променлива. Промяната, която направихме, разбира се, поправя грешката, но връща този, който е написал оригиналния code, там, откъдето е дошъл - блокира всеки път. Ключовата дума volatile може да спаси. Всъщност въпросното твърдение означава, че когато четем всичко, което е обявено за volatile, винаги ще получим действителната стойност.

Освен това, Howто казах по-рано, за летливи полета писането винаги е (включително дълго и двойно) атомарна операция. Друг важен момент: ако имате променлив обект, който има препратки към други обекти (например масив, списък or няHowъв друг клас), тогава само препратката към самия обект винаги ще бъде „свежа“, но не и към всичко в то входящо.

И така, да се върнем към нашите овни с двойно заключване. Използвайки volatile, можете да коригирате ситуацията по следния начин:

public class Keeper {
    private volatile Data data = null;

    public Data getData() {
        if(data == null) {
            synchronized(this) {
                if(data == null) {
                    data = new Data();
                }
            }
        }
        return data;
    }
}

Тук все още имаме заключване, но само ако data == null. Ние филтрираме останалите случаи с помощта на непостоянно четене. Коректността се гарантира от факта, че volatile store се случва - преди volatile read и всички операции, които се случват в конструктора, са видими за всеки, който чете стойността на полето.