Въведение в модела на паметта на 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 и всички операции, които се случват в конструктора, са видими за всеки, който чете стойността на полето.
GO TO FULL VERSION