Panimula sa Java Memory Model

Inilalarawan ng Java Memory Model (JMM) ang pag-uugali ng mga thread sa Java runtime environment. Ang modelo ng memorya ay bahagi ng mga semantika ng wikang Java, at inilalarawan kung ano ang maaari at hindi dapat asahan ng isang programmer kapag bumubuo ng software hindi para sa isang partikular na Java machine, ngunit para sa Java sa kabuuan.

Ang orihinal na modelo ng memorya ng Java (na, sa partikular, ay tumutukoy sa "percolocal memory"), na binuo noong 1995, ay itinuturing na isang pagkabigo: maraming mga pag-optimize ay hindi maaaring gawin nang hindi nawawala ang garantiya ng kaligtasan ng code. Sa partikular, mayroong ilang mga opsyon para magsulat ng multi-threaded na "single":

  • alinman sa bawat pagkilos ng pag-access sa isang singleton (kahit na ang bagay ay nilikha nang matagal na ang nakalipas, at walang maaaring magbago) ay magdudulot ng inter-thread lock;
  • o sa ilalim ng isang tiyak na hanay ng mga pangyayari, ang sistema ay maglalabas ng hindi natapos na loner;
  • o sa ilalim ng isang tiyak na hanay ng mga pangyayari, ang sistema ay lilikha ng dalawang loner;
  • o ang disenyo ay depende sa pag-uugali ng isang partikular na makina.

Samakatuwid, ang mekanismo ng memorya ay muling idinisenyo. Noong 2005, sa paglabas ng Java 5, isang bagong diskarte ang ipinakita, na higit pang pinahusay sa paglabas ng Java 14.

Ang bagong modelo ay batay sa tatlong panuntunan:

Panuntunan #1 : Ang mga single-threaded na programa ay tumatakbo nang pseudo-sequentially. Nangangahulugan ito: sa katotohanan, ang processor ay maaaring magsagawa ng ilang mga operasyon sa bawat orasan, sa parehong oras na binabago ang kanilang pagkakasunud-sunod, gayunpaman, ang lahat ng mga dependency ng data ay nananatili, kaya ang pag-uugali ay hindi naiiba sa sunud-sunod.

Rule number 2 : walang out of nowhere na mga value. Ang pagbabasa ng anumang variable (maliban sa hindi pabagu-bagong haba at doble, kung saan ang panuntunang ito ay maaaring hindi hawak) ay magbabalik ng alinman sa default na halaga (zero) o isang bagay na nakasulat doon ng isa pang command.

At ang panuntunan bilang 3 : ang iba pang mga kaganapan ay isinasagawa sa pagkakasunud-sunod, kung ang mga ito ay konektado sa pamamagitan ng isang mahigpit na bahagyang pagkakasunud-sunod na relasyon "naisasagawa bago" ( mangyayari bago ).

Nangyayari dati

Nakaisip si Leslie Lamport ng konsepto ng Happens before . Ito ay isang mahigpit na partial order relation na ipinakilala sa pagitan ng mga atomic command (++ at -- ay hindi atomic) at hindi nangangahulugang "pisikal na dati".

Sinasabi nito na ang pangalawang koponan ay magiging "alam" sa mga pagbabagong ginawa ng una.

Nangyayari dati

Halimbawa, ang isa ay isinasagawa bago ang isa para sa mga naturang operasyon:

Pag-synchronize at mga monitor:

  • Kinukuha ang monitor ( lock method , synchronize start) at anuman ang mangyayari sa parehong thread pagkatapos nito.
  • Ang pagbabalik ng monitor (paraan ng pag-unlock , pagtatapos ng naka-synchronize) at anuman ang mangyayari sa parehong thread bago ito.
  • Ibinabalik ang monitor at pagkatapos ay kinukunan ito ng isa pang thread.

Pagsusulat at pagbabasa:

  • Pagsusulat sa anumang variable at pagkatapos ay basahin ito sa parehong stream.
  • Lahat sa parehong thread bago sumulat sa pabagu-bagong variable, at ang pagsulat mismo. pabagu-bago ng isip basahin at lahat ng bagay sa parehong thread pagkatapos nito.
  • Pagsusulat sa isang pabagu-bagong variable at pagkatapos ay basahin itong muli. Ang isang pabagu-bago ng isip na pagsulat ay nakikipag-ugnayan sa memorya sa parehong paraan tulad ng isang pagbabalik ng monitor, habang ang isang nabasa ay tulad ng isang pagkuha. Ito ay lumalabas na kung ang isang thread ay sumulat sa isang pabagu-bago ng isip na variable, at ang pangalawa ay natagpuan ito, ang lahat ng nauuna sa pagsulat ay isinasagawa bago ang lahat ng darating pagkatapos ng pagbasa; tingnan ang larawan.

Pagpapanatili ng bagay:

  • Static na pagsisimula at anumang mga aksyon na may anumang mga pagkakataon ng mga bagay.
  • Pagsusulat sa mga huling field sa constructor at lahat ng bagay pagkatapos ng constructor. Bilang eksepsiyon, ang happens-before na ugnayan ay hindi palipat-lipat na kumokonekta sa iba pang mga panuntunan at samakatuwid ay maaaring magdulot ng inter-thread race.
  • Any work with the object and finalize() .

Serbisyo ng stream:

  • Pagsisimula ng thread at anumang code sa thread.
  • Pag-zero ng mga variable na nauugnay sa thread at anumang code sa thread.
  • Code sa thread at join() ; code sa thread at isAlive() == false .
  • interrupt() ang thread at makita na huminto na ito.

Nangyayari bago ang mga nuances ng trabaho

Ang paglabas ng happens-before monitor ay maganap bago makuha ang parehong monitor. Ito ay nagkakahalaga na tandaan na ito ay ang paglabas, at hindi ang paglabas, iyon ay, hindi mo kailangang mag-alala tungkol sa kaligtasan kapag gumagamit ng paghihintay.

Tingnan natin kung paano makakatulong sa atin ang kaalamang ito na itama ang ating halimbawa. Sa kasong ito, ang lahat ay napaka-simple: alisin lamang ang panlabas na tseke at iwanan ang pag-synchronize kung ano ito. Ngayon ang pangalawang thread ay garantisadong makikita ang lahat ng mga pagbabago, dahil makukuha lamang nito ang monitor pagkatapos ilabas ito ng ibang thread. At dahil hindi niya ito ilalabas hanggang sa masimulan ang lahat, makikita natin ang lahat ng pagbabago nang sabay-sabay, at hindi hiwalay:

public class Keeper {
    private Data data = null;

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

        return data;
    }
}

Ang pagsusulat sa isang pabagu-bagong variable ay nangyayari-bago basahin mula sa parehong variable. Ang pagbabagong ginawa namin, siyempre, ay nag-aayos ng bug, ngunit inilalagay nito ang sinumang sumulat ng orihinal na code pabalik kung saan ito nanggaling - na humaharang sa bawat oras. Makakatipid ang pabagu-bagong keyword. Sa katunayan, ang pahayag na pinag-uusapan ay nangangahulugan na kapag binabasa ang lahat ng ipinahayag na pabagu-bago, palagi nating makukuha ang aktwal na halaga.

Bilang karagdagan, tulad ng sinabi ko kanina, para sa pabagu-bago ng isip na mga patlang, ang pagsulat ay palaging (kabilang ang mahaba at doble) isang atomic na operasyon. Isa pang mahalagang punto: kung mayroon kang isang pabagu-bago ng isip na entity na may mga reference sa iba pang mga entity (halimbawa, isang array, List o ilang iba pang klase), kung gayon ang isang reference lamang sa entity mismo ay palaging magiging "sariwa", ngunit hindi sa lahat ng nasa papasok na.

Kaya, bumalik sa aming Double-locking rams. Gamit ang volatile, maaari mong ayusin ang sitwasyon tulad nito:

public class Keeper {
    private volatile Data data = null;

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

Narito mayroon pa rin kaming lock, ngunit kung ang data == null. Sinasala namin ang natitirang mga kaso gamit ang volatile read. Ang katumpakan ay sinisiguro ng katotohanang nangyayari ang pabagu-bago ng isip na tindahan-bago ang pabagu-bagong pagbabasa, at lahat ng mga operasyong nagaganap sa constructor ay makikita ng sinumang magbabasa ng halaga ng field.