Architektura sprzętowa pamięci

Nowoczesna sprzętowa architektura pamięci różni się od wewnętrznego modelu pamięci Javy. Dlatego musisz zrozumieć architekturę sprzętową, aby wiedzieć, jak działa z nią model Java. W tej sekcji opisano ogólną sprzętową architekturę pamięci, aw następnej opisano sposób, w jaki Java z nią współpracuje.

Oto uproszczony schemat architektury sprzętowej nowoczesnego komputera:

Architektura sprzętowa pamięci

We współczesnym świecie komputer ma 2 lub więcej procesorów i jest to już normą. Niektóre z tych procesorów mogą mieć również wiele rdzeni. Na takich komputerach możliwe jest jednoczesne uruchamianie wielu wątków. Każdy rdzeń procesora jest w stanie wykonać jeden wątek w dowolnym momencie. Oznacza to, że każda aplikacja Java jest a priori wielowątkowa, aw twoim programie w danym momencie może działać jeden wątek na rdzeń procesora.

Rdzeń procesora zawiera zestaw rejestrów, które znajdują się w jego pamięci (wewnątrz rdzenia). Wykonuje operacje na danych rejestrowych znacznie szybciej niż na danych znajdujących się w pamięci głównej komputera (RAM). Dzieje się tak, ponieważ procesor może uzyskać dostęp do tych rejestrów znacznie szybciej.

Każdy procesor może mieć również własną warstwę pamięci podręcznej. Większość nowoczesnych procesorów to ma. Procesor może uzyskać dostęp do swojej pamięci podręcznej znacznie szybciej niż pamięć główna, ale nie tak szybko, jak jego wewnętrzne rejestry. Wartość szybkości dostępu do pamięci podręcznej mieści się w przybliżeniu pomiędzy szybkościami dostępu do pamięci głównej i rejestrów wewnętrznych.

Ponadto procesory mają miejsce na wielopoziomową pamięć podręczną. Nie jest to jednak tak ważne, aby zrozumieć, w jaki sposób model pamięci Java współdziała z pamięcią sprzętową. Ważne jest, aby wiedzieć, że procesory mogą mieć pewien poziom pamięci podręcznej.

Każdy komputer zawiera również pamięć RAM (główny obszar pamięci) w ten sam sposób. Wszystkie rdzenie mają dostęp do pamięci głównej. Obszar pamięci głównej jest zwykle znacznie większy niż pamięć podręczna rdzeni procesora.

W momencie, gdy procesor potrzebuje dostępu do pamięci głównej, wczytuje jej część do swojej pamięci podręcznej. Może również odczytywać niektóre dane z pamięci podręcznej do swoich wewnętrznych rejestrów, a następnie wykonywać na nich operacje. Kiedy procesor musi zapisać wynik z powrotem do pamięci głównej, opróżnia dane ze swojego wewnętrznego rejestru do pamięci podręcznej, aw pewnym momencie do pamięci głównej.

Dane przechowywane w pamięci podręcznej są zwykle usuwane z powrotem do pamięci głównej, gdy procesor musi zapisać coś innego w pamięci podręcznej. Pamięć podręczna ma możliwość jednoczesnego czyszczenia pamięci i zapisu danych. Procesor nie musi odczytywać ani zapisywać pełnej pamięci podręcznej za każdym razem podczas aktualizacji. Zwykle pamięć podręczna jest aktualizowana w małych blokach pamięci, które nazywane są „linią pamięci podręcznej”. Jedna lub więcej „linii pamięci podręcznej” może zostać wczytana do pamięci podręcznej, a jedna lub więcej linii pamięci podręcznej może zostać wyczyszczona z powrotem do pamięci głównej.

Połączenie modelu pamięci Java i sprzętowej architektury pamięci

Jak już wspomniano, model pamięci Java i architektura sprzętowa pamięci są różne. Architektura sprzętowa nie rozróżnia stosów wątków i stert. Na sprzęcie stos wątków i HEAP (sterta) znajdują się w pamięci głównej.

Części stosów i stosów wątków mogą czasami znajdować się w pamięciach podręcznych i rejestrach wewnętrznych procesora. Pokazano to na schemacie:

stos wątków i HEAP

Gdy obiekty i zmienne mogą być przechowywane w różnych obszarach pamięci komputera, mogą pojawić się pewne problemy. Oto dwa główne:

  • Widoczność zmian wprowadzonych przez wątek do zmiennych udostępnionych.
  • Wyścig podczas odczytu, sprawdzania i zapisywania wspólnych zmiennych.

Obie te kwestie zostaną wyjaśnione poniżej.

Widoczność obiektów udostępnionych

Jeśli co najmniej dwa wątki współużytkują obiekt bez odpowiedniego użycia deklaracji ulotnej lub synchronizacji, zmiany we współdzielonym obiekcie dokonane przez jeden wątek mogą nie być widoczne dla innych wątków.

Wyobraź sobie, że udostępniony obiekt jest początkowo przechowywany w pamięci głównej. Wątek działający na procesorze wczytuje udostępniony obiekt do pamięci podręcznej tego samego procesora. Tam dokonuje zmian w obiekcie. Dopóki pamięć podręczna procesora nie zostanie wyczyszczona do pamięci głównej, zmodyfikowana wersja udostępnionego obiektu nie jest widoczna dla wątków działających na innych procesorach. W ten sposób każdy wątek może uzyskać własną kopię współdzielonego obiektu, każda kopia będzie znajdować się w osobnej pamięci podręcznej procesora.

Poniższy diagram ilustruje zarys tej sytuacji. Jeden wątek uruchomiony na lewym procesorze kopiuje współdzielony obiekt do swojej pamięci podręcznej i zmienia wartość licznika na 2. Ta zmiana jest niewidoczna dla innych wątków działających na prawym procesorze, ponieważ aktualizacja licznika nie została jeszcze przeniesiona z powrotem do pamięci głównej.

Aby rozwiązać ten problem, możesz użyć słowa kluczowego volatile podczas deklarowania zmiennej. Może zapewnić, że dana zmienna jest odczytywana bezpośrednio z pamięci głównej i jest zawsze zapisywana z powrotem do pamięci głównej podczas aktualizacji.

Warunki wyścigu

Jeśli dwa lub więcej wątków współdzieli ten sam obiekt i więcej niż jeden wątek aktualizuje zmienne w tym udostępnionym obiekcie, może wystąpić sytuacja wyścigu.

Wyobraź sobie, że wątek A wczytuje zmienną licznika współdzielonego obiektu do pamięci podręcznej swojego procesora. Wyobraź sobie również, że wątek B robi to samo, ale w pamięci podręcznej innego procesora. Teraz wątek A dodaje 1 do wartości count, a wątek B robi to samo. Teraz zmienna została zwiększona dwukrotnie - osobno o +1 w pamięci podręcznej każdego procesora.

Gdyby te przyrosty były wykonywane sekwencyjnie, zmienna zliczająca zostałaby podwojona i zapisana z powrotem do pamięci głównej (pierwotna wartość + 2).

Jednak dwa przyrosty zostały wykonane w tym samym czasie bez odpowiedniej synchronizacji. Niezależnie od tego, który wątek (A czy B) zapisuje zaktualizowaną wersję licznika do pamięci głównej, nowa wartość będzie tylko o 1 większa niż pierwotna wartość, pomimo dwóch przyrostów.

Ten diagram ilustruje występowanie opisanego powyżej problemu wyścigu:

Aby rozwiązać ten problem, możesz użyć zsynchronizowanego bloku Java. Zsynchronizowany blok zapewnia, że ​​tylko jeden wątek może wprowadzić daną krytyczną sekcję kodu w danym momencie.

Zsynchronizowane bloki gwarantują również, że wszystkie zmienne, do których uzyskano dostęp w zsynchronizowanym bloku, zostaną odczytane z pamięci głównej, a gdy wątek opuści zsynchronizowany blok, wszystkie zaktualizowane zmienne zostaną przeniesione z powrotem do pamięci głównej, niezależnie od tego, czy zmienna jest zadeklarowana jako ulotna, czy nie.