Tổng quan ngắn gọn về các chi tiết cụ thể về cách các luồng tương tác. Trước đây, chúng ta đã xem cách các luồng được đồng bộ hóa với nhau. Lần này chúng ta sẽ đi sâu vào các vấn đề có thể phát sinh khi các luồng tương tác với nhau và chúng ta sẽ nói về cách tránh chúng. Chúng tôi cũng sẽ cung cấp một số liên kết hữu ích để nghiên cứu sâu hơn.
Với plugin JVisualVM được cài đặt (thông qua Công cụ -> Plugin), chúng ta có thể thấy nơi xảy ra bế tắc:
Theo JVisualVM, chúng ta thấy các khoảng thời gian ngủ và khoảng thời gian dừng (đây là khi một luồng cố gắng đạt được khóa — nó chuyển sang trạng thái dừng, như chúng ta đã thảo luận trước đó khi nói về đồng bộ hóa luồng ) . Bạn có thể xem một ví dụ về livelock tại đây: Java - Thread Livelock .
Bạn có thể xem một siêu ví dụ ở đây: Java - Thread Starvation and Fairness . Ví dụ này cho thấy điều gì sẽ xảy ra với các luồng trong quá trình bỏ đói và cách một thay đổi nhỏ từ
Kiểm tra này đã được thêm vào IntelliJ IDEA như một phần của vấn đề IDEA-61117 , được liệt kê trong Ghi chú phát hành vào năm 2010.

Giới thiệu
Vì vậy, chúng ta biết rằng Java có luồng. Bạn có thể đọc về điều đó trong bài đánh giá có tựa đề Cùng nhau tốt hơn: Java và lớp Chủ đề. Phần I - Chủ đề thực hiện . Và chúng tôi đã khám phá ra thực tế rằng các luồng có thể đồng bộ hóa với nhau trong bài đánh giá có tiêu đề Cùng nhau tốt hơn: Java và lớp Chủ đề. Phần II — Đồng bộ hóa . Đã đến lúc nói về cách các luồng tương tác với nhau. Làm thế nào để họ chia sẻ tài nguyên được chia sẻ? Những vấn đề gì có thể phát sinh ở đây?
Bế tắc
Vấn đề đáng sợ nhất là bế tắc. Bế tắc là khi hai hoặc nhiều luồng đang chờ luồng kia mãi mãi. Chúng tôi sẽ lấy một ví dụ từ trang web Oracle mô tả bế tắc :
public class Deadlock {
static class Friend {
private final String name;
public Friend(String name) {
this.name = name;
}
public String getName() {
return this.name;
}
public synchronized void bow(Friend bower) {
System.out.format("%s: %s bowed to me!%n",
this.name, bower.getName());
bower.bowBack(this);
}
public synchronized void bowBack(Friend bower) {
System.out.format("%s: %s bowed back to me!%n",
this.name, bower.getName());
}
}
public static void main(String[] args) {
final Friend alphonse = new Friend("Alphonse");
final Friend gaston = new Friend("Gaston");
new Thread(() -> alphonse.bow(gaston)).start();
new Thread(() -> gaston.bow(alphonse)).start();
}
}
Bế tắc có thể không xảy ra ở đây lần đầu tiên, nhưng nếu chương trình của bạn bị treo, thì đã đến lúc chạy jvisualvm
: 
"Thread-1" - Thread t@12
java.lang.Thread.State: BLOCKED
at Deadlock$Friend.bowBack(Deadlock.java:16)
- waiting to lock <33a78231> (a Deadlock$Friend) owned by "Thread-0" t@11
Chủ đề 1 đang chờ khóa từ chủ đề 0. Tại sao điều đó lại xảy ra? Thread-1
bắt đầu chạy và thực thi Friend#bow
phương thức. Nó được đánh dấu bằng synchronized
từ khóa, có nghĩa là chúng tôi đang lấy màn hình cho this
(đối tượng hiện tại). Đầu vào của phương thức là một tham chiếu đến Friend
đối tượng khác. Bây giờ, Thread-1
muốn thực thi phương thức trên phương thức kia Friend
và phải lấy khóa của nó để thực hiện. Nhưng nếu luồng khác (trong trường hợp này Thread-0
) quản lý để nhập bow()
phương thức, thì khóa đã được lấy và Thread-1
đợiThread-0
, và ngược lại. Đây là bế tắc không thể giải quyết được, và chúng tôi gọi nó là bế tắc. Giống như một cái kìm chết chóc không thể thoát ra, bế tắc là sự ngăn chặn lẫn nhau không thể phá vỡ. Để biết cách giải thích khác về bế tắc, bạn có thể xem video này: Giải thích về bế tắc và bế tắc .
ổ khóa
Nếu có bế tắc, thì cũng có livelock? Vâng, có :) Livelock xảy ra khi các chủ đề bên ngoài dường như vẫn còn sống, nhưng chúng không thể làm bất cứ điều gì, bởi vì (các) điều kiện cần thiết để chúng tiếp tục công việc của mình không thể được đáp ứng. Về cơ bản, livelock tương tự như bế tắc, nhưng các luồng không "treo" chờ màn hình. Thay vào đó, họ mãi mãi làm một cái gì đó. Ví dụ:
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class App {
public static final String ANSI_BLUE = "\u001B[34m";
public static final String ANSI_PURPLE = "\u001B[35m";
public static void log(String text) {
String name = Thread.currentThread().getName(); // Like "Thread-1" or "Thread-0"
String color = ANSI_BLUE;
int val = Integer.valueOf(name.substring(name.lastIndexOf("-") + 1)) + 1;
if (val != 0) {
color = ANSI_PURPLE;
}
System.out.println(color + name + ": " + text + color);
try {
System.out.println(color + name + ": wait for " + val + " sec" + color);
Thread.currentThread().sleep(val * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Lock first = new ReentrantLock();
Lock second = new ReentrantLock();
Runnable locker = () -> {
boolean firstLocked = false;
boolean secondLocked = false;
try {
while (!firstLocked || !secondLocked) {
firstLocked = first.tryLock(100, TimeUnit.MILLISECONDS);
log("First Locked: " + firstLocked);
secondLocked = second.tryLock(100, TimeUnit.MILLISECONDS);
log("Second Locked: " + secondLocked);
}
first.unlock();
second.unlock();
} catch (InterruptedException e) {
e.printStackTrace();
}
};
new Thread(locker).start();
new Thread(locker).start();
}
}
Sự thành công của mã này phụ thuộc vào thứ tự mà bộ lập lịch luồng Java bắt đầu các luồng. Nếu Thead-1
bắt đầu trước, thì chúng tôi nhận được livelock:
Thread-1: First Locked: true
Thread-1: wait for 2 sec
Thread-0: First Locked: false
Thread-0: wait for 1 sec
Thread-0: Second Locked: true
Thread-0: wait for 1 sec
Thread-1: Second Locked: false
Thread-1: wait for 2 sec
Thread-0: First Locked: false
Thread-0: wait for 1 sec
...
Như bạn có thể thấy từ ví dụ, cả hai luồng đều cố gắng lấy cả hai khóa lần lượt, nhưng chúng không thành công. Nhưng, họ không bế tắc. Bề ngoài, mọi thứ đều ổn và họ đang làm công việc của mình. 
chết đói
Ngoài bế tắc và bế tắc, có một vấn đề khác có thể xảy ra trong quá trình đa luồng: chết đói. Hiện tượng này khác với các hình thức chặn trước đó ở chỗ các luồng không bị chặn — đơn giản là chúng không có đủ tài nguyên. Kết quả là, trong khi một số luồng chiếm toàn bộ thời gian thực hiện, thì một số luồng khác không thể chạy:
https://www.logicbig.com/
Thread.sleep()
việc Thread.wait()
cho phép bạn phân phối tải đồng đều. 
điều kiện cuộc đua
Trong đa luồng, có một thứ gọi là "điều kiện chủng tộc". Hiện tượng này xảy ra khi các luồng chia sẻ tài nguyên, nhưng mã được viết theo cách không đảm bảo chia sẻ chính xác. Hãy xem một ví dụ:
public class App {
public static int value = 0;
public static void main(String[] args) {
Runnable task = () -> {
for (int i = 0; i < 10000; i++) {
int oldValue = value;
int newValue = ++value;
if (oldValue + 1 != newValue) {
throw new IllegalStateException(oldValue + " + 1 = " + newValue);
}
}
};
new Thread(task).start();
new Thread(task).start();
new Thread(task).start();
}
}
Mã này có thể không tạo ra lỗi lần đầu tiên. Khi nó xảy ra, nó có thể trông như thế này:
Exception in thread "Thread-1" java.lang.IllegalStateException: 7899 + 1 = 7901
at App.lambda$main$0(App.java:13)
at java.lang.Thread.run(Thread.java:745)
Như bạn có thể thấy, đã xảy ra sự cố khi newValue
được gán một giá trị. newValue
to quá. Do điều kiện tương tranh, một trong các chuỗi đã quản lý để thay đổi biến value
giữa hai câu lệnh. Nó chỉ ra rằng có một cuộc chạy đua giữa các chủ đề. Bây giờ hãy nghĩ về tầm quan trọng của việc không mắc các lỗi tương tự với các giao dịch tiền tệ... Bạn cũng có thể xem các ví dụ và sơ đồ tại đây: Mã mô phỏng điều kiện chủng tộc trong chuỗi Java .
Bay hơi
Nói về sự tương tác của các chủ đề, từvolatile
khóa đáng nói. Hãy xem xét một ví dụ đơn giản:
public class App {
public static boolean flag = false;
public static void main(String[] args) throws InterruptedException {
Runnable whileFlagFalse = () -> {
while(!flag) {
}
System.out.println("Flag is now TRUE");
};
new Thread(whileFlagFalse).start();
Thread.sleep(1000);
flag = true;
}
}
Thú vị nhất, điều này rất có thể không hoạt động. Chủ đề mới sẽ không thấy sự thay đổi trong flag
trường. Để khắc phục điều này cho flag
trường, chúng ta cần sử dụng volatile
từ khóa. Như thế nào và tại sao? Bộ xử lý thực hiện tất cả các hành động. Nhưng kết quả tính toán phải được lưu trữ ở đâu đó. Đối với điều này, có bộ nhớ chính và bộ nhớ cache của bộ xử lý. Bộ nhớ cache của bộ xử lý giống như một đoạn bộ nhớ nhỏ được sử dụng để truy cập dữ liệu nhanh hơn so với khi truy cập bộ nhớ chính. Nhưng mọi thứ đều có nhược điểm: dữ liệu trong bộ đệm có thể không được cập nhật (như trong ví dụ trên, khi giá trị của trường cờ không được cập nhật). Nênvolatile
từ khóa cho JVM biết rằng chúng tôi không muốn lưu trữ biến của mình. Điều này cho phép nhìn thấy kết quả cập nhật trên tất cả các luồng. Đây là một lời giải thích rất đơn giản. Đối với volatile
từ khóa, tôi thực sự khuyên bạn nên đọc bài viết này . Để biết thêm thông tin, tôi cũng khuyên bạn nên đọc Java Memory Model và Java Volatile Keyword . Ngoài ra, điều quan trọng cần nhớ volatile
là về khả năng hiển thị chứ không phải về tính nguyên tử của các thay đổi. Xem mã trong phần "Điều kiện chủng tộc", chúng ta sẽ thấy chú giải công cụ trong IntelliJ IDEA: 
nguyên tử
Phép toán nguyên tử là phép toán không thể chia nhỏ. Ví dụ, thao tác gán giá trị cho một biến phải là nguyên tử. Thật không may, thao tác gia tăng không phải là nguyên tử, bởi vì gia số yêu cầu tới ba thao tác CPU: lấy giá trị cũ, thêm một giá trị vào đó, sau đó lưu giá trị. Tại sao tính nguyên tử lại quan trọng? Với thao tác gia tăng, nếu xảy ra tình trạng chạy đua thì tài nguyên dùng chung (tức là giá trị dùng chung) có thể đột ngột thay đổi bất cứ lúc nào. Ngoài ra, các hoạt động liên quan đến cấu trúc 64 bit, chẳng hạn nhưlong
và double
, không phải là nguyên tử. Bạn có thể đọc thêm chi tiết tại đây: Đảm bảo tính nguyên tử khi đọc và ghi các giá trị 64 bit . Các vấn đề liên quan đến tính nguyên tử có thể được nhìn thấy trong ví dụ này:
public class App {
public static int value = 0;
public static AtomicInteger atomic = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
Runnable task = () -> {
for (int i = 0; i < 10000; i++) {
value++;
atomic.incrementAndGet();
}
};
for (int i = 0; i < 3; i++) {
new Thread(task).start();
}
Thread.sleep(300);
System.out.println(value);
System.out.println(atomic.get());
}
}
Lớp học đặc biệt AtomicInteger
sẽ luôn cho chúng tôi 30.000, nhưng sẽ value
thay đổi theo thời gian. Có một tổng quan ngắn về chủ đề này: Giới thiệu về các biến nguyên tử trong Java . Thuật toán "so sánh và hoán đổi" nằm ở trung tâm của các lớp nguyên tử. Bạn có thể đọc thêm về nó ở đây trong So sánh các thuật toán không khóa - CAS và FAA trên ví dụ về JDK 7 và 8 hoặc trong bài viết So sánh và hoán đổi trên Wikipedia. 
http://jeremymanson.blogspot.com/2008/11/what-volatile-means-in-java.html
xảy ra trước
Có một khái niệm thú vị và bí ẩn được gọi là "xảy ra trước". Là một phần của nghiên cứu của bạn về chủ đề, bạn nên đọc về nó. Mối quan hệ xảy ra trước hiển thị thứ tự các hành động giữa các luồng sẽ được nhìn thấy. Có nhiều cách giải thích và bình luận. Đây là một trong những bài thuyết trình gần đây nhất về chủ đề này: Các mối quan hệ "Xảy ra trước" của Java .Bản tóm tắt
Trong bài đánh giá này, chúng tôi đã khám phá một số chi tiết cụ thể về cách các luồng tương tác. Chúng tôi đã thảo luận các vấn đề có thể phát sinh, cũng như các cách để xác định và loại bỏ chúng. Danh sách các tài liệu bổ sung về chủ đề:- Kiểm tra khóa hai lần
- Câu hỏi thường gặp về JSR 133 (Mô hình bộ nhớ Java)
- IQ 35: Làm thế nào để ngăn chặn bế tắc?
- Các khái niệm tương tranh trong Java của Douglas Hawkins (2017)
GO TO FULL VERSION