Kiến trúc phần cứng bộ nhớ
Kiến trúc phần cứng bộ nhớ hiện đại khác với mô hình bộ nhớ trong của Java. Do đó, bạn cần phải hiểu kiến trúc phần cứng để biết mô hình Java hoạt động với nó như thế nào. Phần này mô tả kiến trúc phần cứng bộ nhớ chung và phần tiếp theo mô tả cách Java làm việc với nó.
Đây là một sơ đồ đơn giản hóa về kiến trúc phần cứng của một máy tính hiện đại:
![Kiến trúc phần cứng bộ nhớ](https://cdn.codegym.cc/images/article/0a28b119-0299-42d8-aab3-fba135d16288/512.jpeg)
Trong thế giới hiện đại, một máy tính có 2 bộ xử lý trở lên và đây đã là tiêu chuẩn. Một số bộ xử lý này cũng có thể có nhiều lõi. Trên những máy tính như vậy, có thể chạy nhiều luồng cùng một lúc. Mỗi lõi bộ xử lý có khả năng thực thi một luồng tại bất kỳ thời điểm nào. Điều này có nghĩa là bất kỳ ứng dụng Java nào cũng là đa luồng tiên nghiệm và trong chương trình của bạn, một luồng trên mỗi lõi bộ xử lý có thể chạy tại một thời điểm.
Lõi bộ xử lý chứa một tập hợp các thanh ghi nằm trong bộ nhớ của nó (bên trong lõi). Nó thực hiện các thao tác trên dữ liệu thanh ghi nhanh hơn nhiều so với dữ liệu nằm trong bộ nhớ chính của máy tính (RAM). Điều này là do bộ xử lý có thể truy cập các thanh ghi này nhanh hơn nhiều.
Mỗi CPU cũng có thể có lớp bộ đệm riêng. Hầu hết các bộ xử lý hiện đại đều có nó. Bộ xử lý có thể truy cập bộ đệm của nó nhanh hơn nhiều so với bộ nhớ chính, nhưng không nhanh bằng các thanh ghi bên trong của nó. Giá trị của tốc độ truy cập bộ đệm xấp xỉ giữa tốc độ truy cập của bộ nhớ chính và các thanh ghi bên trong.
Hơn nữa, bộ xử lý có một nơi để có bộ đệm đa cấp. Nhưng điều này không quá quan trọng để biết để hiểu mô hình bộ nhớ Java tương tác với bộ nhớ phần cứng như thế nào. Điều quan trọng cần biết là bộ xử lý có thể có một số mức bộ đệm.
Bất kỳ máy tính nào cũng chứa RAM (vùng bộ nhớ chính) theo cách tương tự. Tất cả các lõi có thể truy cập bộ nhớ chính. Vùng bộ nhớ chính thường lớn hơn nhiều so với bộ nhớ đệm của các nhân xử lý.
Tại thời điểm bộ xử lý cần truy cập vào bộ nhớ chính, nó sẽ đọc một phần của nó vào bộ nhớ cache. Nó cũng có thể đọc một số dữ liệu từ bộ đệm vào các thanh ghi bên trong của nó và sau đó thực hiện các thao tác trên chúng. Khi CPU cần ghi kết quả trở lại bộ nhớ chính, nó sẽ xóa dữ liệu từ thanh ghi bên trong của nó vào bộ đệm và tại một số điểm, vào bộ nhớ chính.
Dữ liệu được lưu trữ trong bộ đệm thường được đưa trở lại bộ nhớ chính khi bộ xử lý cần lưu trữ thứ gì đó khác trong bộ đệm. Bộ đệm có khả năng xóa bộ nhớ và ghi dữ liệu cùng một lúc. Bộ xử lý không cần phải đọc hoặc ghi toàn bộ bộ nhớ cache mỗi lần trong quá trình cập nhật. Thông thường bộ đệm được cập nhật trong các khối bộ nhớ nhỏ, chúng được gọi là "dòng bộ đệm". Một hoặc nhiều "dòng bộ đệm" có thể được đọc vào bộ nhớ đệm và một hoặc nhiều dòng bộ đệm có thể được chuyển trở lại bộ nhớ chính.
Kết hợp mô hình bộ nhớ Java và kiến trúc phần cứng bộ nhớ
Như đã đề cập, mô hình bộ nhớ Java và kiến trúc phần cứng bộ nhớ là khác nhau. Kiến trúc phần cứng không phân biệt giữa ngăn xếp luồng và đống. Trên phần cứng, ngăn xếp luồng và HEAP (đống) nằm trong bộ nhớ chính.
Các bộ phận của ngăn xếp và đống luồng đôi khi có thể xuất hiện trong bộ đệm và thanh ghi bên trong của CPU. Điều này được thể hiện trong sơ đồ:
![ngăn xếp chủ đề và HEAP](https://cdn.codegym.cc/images/article/7b653255-07de-4c9b-b6c0-9c47c7e6cd44/1024.jpeg)
Khi các đối tượng và biến có thể được lưu trữ trong các vùng khác nhau của bộ nhớ máy tính, một số vấn đề có thể phát sinh. Đây là hai cái chính:
- Khả năng hiển thị các thay đổi mà chuỗi đã thực hiện đối với các biến được chia sẻ.
- Điều kiện chạy đua khi đọc, kiểm tra và ghi các biến dùng chung.
Cả hai vấn đề này sẽ được giải thích dưới đây.
Khả năng hiển thị của các đối tượng được chia sẻ
Nếu hai hoặc nhiều luồng chia sẻ một đối tượng mà không sử dụng đúng cách khai báo hoặc đồng bộ hóa khả biến, thì các thay đổi đối với đối tượng được chia sẻ bởi một luồng có thể không hiển thị đối với các luồng khác.
Hãy tưởng tượng rằng một đối tượng được chia sẻ ban đầu được lưu trữ trong bộ nhớ chính. Một luồng chạy trên CPU đọc đối tượng được chia sẻ vào bộ đệm của cùng một CPU. Ở đó, anh ta thực hiện các thay đổi đối với đối tượng. Cho đến khi bộ đệm của CPU được xóa vào bộ nhớ chính, phiên bản sửa đổi của đối tượng được chia sẻ sẽ không hiển thị đối với các luồng chạy trên các CPU khác. Do đó, mỗi luồng có thể nhận bản sao riêng của đối tượng được chia sẻ, mỗi bản sao sẽ nằm trong bộ đệm CPU riêng biệt.
Sơ đồ sau đây minh họa một phác thảo của tình huống này. Một luồng đang chạy trên CPU bên trái sao chép đối tượng được chia sẻ vào bộ nhớ cache của nó và thay đổi giá trị của số đếm thành 2. Thay đổi này là vô hình đối với các luồng khác đang chạy trên CPU bên phải vì bản cập nhật cho số đếm vẫn chưa được chuyển trở lại bộ nhớ chính.
![](https://cdn.codegym.cc/images/article/c4ba107f-a25e-496d-af1a-4e7128d00d0e/800.jpeg)
Để giải quyết vấn đề này, bạn có thể sử dụng từ khóa dễ bay hơi khi khai báo một biến. Nó có thể đảm bảo rằng một biến nhất định được đọc trực tiếp từ bộ nhớ chính và luôn được ghi trở lại bộ nhớ chính khi được cập nhật.
Điều kiện của cuộc đua
Nếu hai hoặc nhiều luồng chia sẻ cùng một đối tượng và nhiều hơn một luồng cập nhật các biến trong đối tượng được chia sẻ đó thì có thể xảy ra tình trạng tranh đua.
Hãy tưởng tượng rằng luồng A đọc biến đếm của đối tượng được chia sẻ vào bộ đệm của bộ xử lý. Cũng hãy tưởng tượng rằng luồng B cũng làm điều tương tự, nhưng trong bộ đệm của bộ xử lý khác. Bây giờ luồng A thêm 1 vào giá trị của số đếm và luồng B cũng làm như vậy. Giờ đây, biến đã được tăng lên hai lần - riêng biệt bằng +1 trong bộ đệm của mỗi bộ xử lý.
Nếu các bước tăng này được thực hiện tuần tự, biến đếm sẽ được nhân đôi và ghi trở lại bộ nhớ chính (giá trị ban đầu + 2).
Tuy nhiên, hai gia số đã được thực hiện cùng một lúc mà không có sự đồng bộ hóa thích hợp. Bất kể luồng nào (A hoặc B) ghi phiên bản đếm được cập nhật của nó vào bộ nhớ chính, giá trị mới sẽ chỉ nhiều hơn 1 giá trị ban đầu, mặc dù có hai lần tăng.
Sơ đồ này minh họa sự xuất hiện của vấn đề điều kiện dồn đuổi được mô tả ở trên:
![](https://cdn.codegym.cc/images/article/37347ace-af3d-4648-a218-18f8c1576289/800.jpeg)
Để giải quyết vấn đề này, bạn có thể sử dụng khối đồng bộ hóa Java. Một khối được đồng bộ hóa đảm bảo rằng chỉ một luồng có thể nhập một phần mã quan trọng nhất định tại bất kỳ thời điểm nào.
Các khối được đồng bộ hóa cũng đảm bảo rằng tất cả các biến được truy cập bên trong khối được đồng bộ hóa sẽ được đọc từ bộ nhớ chính và khi luồng thoát khỏi khối được đồng bộ hóa, tất cả các biến được cập nhật sẽ được đưa trở lại bộ nhớ chính, bất kể biến đó được khai báo là biến động hay Không.