CodeGym /Blog Java /Ngẫu nhiên /Cùng nhau tốt hơn: Java và lớp Thread. Phần II - Đồng bộ ...
John Squirrels
Mức độ
San Francisco

Cùng nhau tốt hơn: Java và lớp Thread. Phần II - Đồng bộ hóa

Xuất bản trong nhóm

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 . Chủ đề là cần thiết để thực hiện công việc song song. Điều này làm cho khả năng cao là các luồng sẽ tương tác với nhau bằng cách nào đó. Hãy xem điều này diễn ra như thế nào và chúng ta có những công cụ cơ bản nào. Cùng nhau tốt hơn: Java và lớp Thread.  Phần II — Đồng bộ hóa - 1

năng suất

Thread.yield() khó hiểu và hiếm khi được sử dụng. Nó được mô tả theo nhiều cách khác nhau trên Internet. Bao gồm một số người viết rằng có một số chuỗi luồng, trong đó một luồng sẽ giảm dần dựa trên mức độ ưu tiên của luồng. Những người khác viết rằng một luồng sẽ thay đổi trạng thái của nó từ "Đang chạy" thành "Có thể chạy được" (mặc dù không có sự phân biệt giữa các trạng thái này, tức là Java không phân biệt giữa chúng). Thực tế là tất cả đều ít nổi tiếng hơn và đơn giản hơn theo một nghĩa nào đó. Cùng nhau tốt hơn: Java và lớp Thread.  Phần II — Đồng bộ hóa - 2Có một lỗi ( JDK-6416721: (spec thread) Fix Thread.yield() javadoc ) được ghi lại cho yield()tài liệu của phương pháp. Nếu bạn đọc nó, rõ ràng làyield()phương pháp thực sự chỉ cung cấp một số đề xuất cho bộ lập lịch luồng Java rằng luồng này có thể được cung cấp ít thời gian thực hiện hơn. Nhưng những gì thực sự xảy ra, tức là liệu bộ lập lịch có hành động theo đề xuất hay không và những gì nó làm nói chung, phụ thuộc vào việc triển khai của JVM và hệ điều hành. Và nó cũng có thể phụ thuộc vào một số yếu tố khác. Tất cả sự nhầm lẫn rất có thể là do đa luồng đã được suy nghĩ lại khi ngôn ngữ Java đã phát triển. Đọc thêm phần tổng quan tại đây: Giới thiệu tóm tắt về Java Thread.yield() .

Ngủ

Một luồng có thể chuyển sang chế độ ngủ trong khi thực hiện. Đây là kiểu tương tác dễ dàng nhất với các luồng khác. Hệ điều hành chạy máy ảo Java chạy mã Java của chúng tôi có bộ lập lịch luồng riêng . Nó quyết định chủ đề nào sẽ bắt đầu và khi nào. Một lập trình viên không thể tương tác trực tiếp với bộ lập lịch này từ mã Java mà chỉ thông qua JVM. Anh ấy hoặc cô ấy có thể yêu cầu bộ lập lịch tạm dừng chuỗi trong một thời gian, tức là đặt nó vào chế độ ngủ. Bạn có thể đọc thêm trong các bài viết này: Thread.sleep()How Multithreading works . Bạn cũng có thể kiểm tra cách các luồng hoạt động trong hệ điều hành Windows: Internals of Windows Thread . Và bây giờ hãy tận mắt chứng kiến. Lưu đoạn mã sau vào một tệp có tên HelloWorldApp.java:

class HelloWorldApp {
    public static void main(String []args) {
        Runnable task = () -> {
            try {
                int secToWait = 1000 * 60;
                Thread.currentThread().sleep(secToWait);
                System.out.println("Woke up");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        };
        Thread thread = new Thread(task);
        thread.start();
    }
}
Như bạn có thể thấy, chúng tôi có một số tác vụ đợi trong 60 giây, sau đó chương trình kết thúc. Chúng ta biên dịch bằng lệnh " javac HelloWorldApp.java" và sau đó chạy chương trình bằng lệnh " java HelloWorldApp". Tốt nhất là bắt đầu chương trình trong một cửa sổ riêng. Ví dụ, trên Windows, nó là như thế này: start java HelloWorldApp. Chúng tôi sử dụng lệnh jps để lấy PID (ID tiến trình) và chúng tôi mở danh sách các luồng bằng " jvisualvm --openpid pid: Cùng nhau tốt hơn: Java và lớp Thread.  Phần II — Đồng bộ hóa - 3Như bạn có thể thấy, luồng của chúng tôi hiện có trạng thái "Đang ngủ". Trên thực tế, có một cách hay hơn để trợ giúp chủ đề của chúng tôi có những giấc mơ ngọt ngào:

try {
	TimeUnit.SECONDS.sleep(60);
	System.out.println("Woke up");
} catch (InterruptedException e) {
	e.printStackTrace();
}
Bạn có nhận thấy rằng chúng tôi đang xử lý InterruptedExceptionở mọi nơi không? Hãy hiểu tại sao.

Thread.interrupt()

Vấn đề là trong khi một luồng đang chờ/ngủ, ai đó có thể muốn ngắt. Trong trường hợp này, chúng tôi xử lý một tệp InterruptedException. Cơ chế này được tạo sau khi Thread.stop()phương thức được tuyên bố là Không dùng nữa, nghĩa là đã lỗi thời và không được mong muốn. Lý do là khi stop()phương thức được gọi, luồng chỉ đơn giản là bị "giết", điều này rất khó đoán. Chúng tôi không thể biết khi nào chuỗi sẽ dừng và chúng tôi không thể đảm bảo tính nhất quán của dữ liệu. Hãy tưởng tượng rằng bạn đang ghi dữ liệu vào một tệp trong khi luồng bị hủy. Thay vì giết chết luồng, những người tạo ra Java đã quyết định rằng sẽ hợp lý hơn nếu nói với nó rằng nó nên bị gián đoạn. Làm thế nào để trả lời thông tin này là một vấn đề để chủ đề tự quyết định. Để biết thêm chi tiết, hãy đọc Tại sao Thread.stop không được dùng nữa?trên trang web của Oracle. Hãy xem xét một ví dụ:

public static void main(String []args) {
	Runnable task = () -> {
		try {
			TimeUnit.SECONDS.sleep(60);
		} catch (InterruptedException e) {
			System.out.println("Interrupted");
		}
	};
	Thread thread = new Thread(task);
	thread.start();
	thread.interrupt();
}
Trong ví dụ này, chúng tôi sẽ không đợi 60 giây. Thay vào đó, chúng tôi sẽ ngay lập tức hiển thị "Bị gián đoạn". Điều này là do chúng tôi đã gọi interrupt()phương thức trên luồng. Phương pháp này đặt một cờ nội bộ được gọi là "trạng thái ngắt". Nghĩa là, mỗi luồng có một cờ nội bộ không thể truy cập trực tiếp. Nhưng chúng tôi có các phương thức gốc để tương tác với cờ này. Nhưng đó không phải là cách duy nhất. Một luồng có thể đang chạy, không chờ đợi điều gì đó, chỉ đơn giản là thực hiện các hành động. Nhưng nó có thể dự đoán rằng những người khác sẽ muốn kết thúc công việc của nó vào một thời điểm cụ thể. Ví dụ:

public static void main(String []args) {
	Runnable task = () -> {
		while(!Thread.currentThread().isInterrupted()) {
			// Do some work
		}
		System.out.println("Finished");
	};
	Thread thread = new Thread(task);
	thread.start();
	thread.interrupt();
}
Trong ví dụ trên, whilevòng lặp sẽ được thực thi cho đến khi luồng bị ngắt ở bên ngoài. Đối với isInterruptedcờ, điều quan trọng cần biết là nếu chúng ta bắt được một InterruptedException, cờ isInterrupted sẽ được đặt lại và sau đó isInterrupted()sẽ trả về giá trị sai. Lớp Thread cũng có một phương thức Thread.interrupted() tĩnh chỉ áp dụng cho luồng hiện tại, nhưng phương thức này đặt lại cờ thành false! Đọc thêm trong chương này có tiêu đề Gián đoạn luồng .

Tham gia (Chờ chủ đề khác kết thúc)

Kiểu chờ đợi đơn giản nhất là đợi một luồng khác kết thúc.

public static void main(String []args) throws InterruptedException {
	Runnable task = () -> {
		try {
			TimeUnit.SECONDS.sleep(5);
		} catch (InterruptedException e) {
			System.out.println("Interrupted");
		}
	};
	Thread thread = new Thread(task);
	thread.start();
	thread.join();
	System.out.println("Finished");
}
Trong ví dụ này, luồng mới sẽ ở chế độ ngủ 5 giây. Đồng thời, luồng chính sẽ đợi cho đến khi luồng đang ngủ thức dậy và hoàn thành công việc của nó. Nếu bạn xem trạng thái của luồng trong JVisualVM, thì nó sẽ giống như sau: Cùng nhau tốt hơn: Java và lớp Thread.  Phần II — Đồng bộ hóa - 4Nhờ các công cụ giám sát, bạn có thể thấy điều gì đang xảy ra với luồng. Phương thức này joinkhá đơn giản, bởi vì nó chỉ là một phương thức có mã Java thực thi wait()miễn là luồng mà nó được gọi còn hoạt động. Ngay sau khi luồng chết (khi nó hoàn thành công việc), quá trình chờ sẽ bị gián đoạn. Và đó là tất cả sự kỳ diệu của join()phương pháp. Vì vậy, hãy chuyển sang điều thú vị nhất.

Màn hình

Đa luồng bao gồm khái niệm màn hình. Từ monitor xuất hiện trong tiếng Anh theo cách của tiếng Latinh thế kỷ 16 và có nghĩa là "một công cụ hoặc thiết bị được sử dụng để quan sát, kiểm tra hoặc lưu giữ bản ghi liên tục của một quy trình". Trong khuôn khổ bài viết này, chúng tôi sẽ cố gắng đề cập đến những điều cơ bản. Đối với bất kỳ ai muốn biết chi tiết, vui lòng đi sâu vào các tài liệu được liên kết. Chúng tôi bắt đầu hành trình của mình với Đặc tả ngôn ngữ Java (JLS): 17.1. đồng bộ hóa . Nó nói như sau: Cùng nhau tốt hơn: Java và lớp Thread.  Phần II — Đồng bộ hóa - 5Hóa ra Java sử dụng cơ chế "giám sát" để đồng bộ hóa giữa các luồng. Một màn hình được liên kết với từng đối tượng và các luồng có thể lấy lock()hoặc giải phóng nó bằng unlock(). Tiếp theo, chúng ta sẽ tìm hướng dẫn trên trang web của Oracle: Khóa nội tại và đồng bộ hóa. Hướng dẫn này nói rằng đồng bộ hóa của Java được xây dựng xung quanh một thực thể bên trong được gọi là khóa nội tại hoặc khóa màn hình . Khóa này thường được gọi đơn giản là " màn hình ". Một lần nữa chúng ta cũng thấy rằng mọi đối tượng trong Java đều có một khóa nội tại được liên kết với nó. Bạn có thể đọc Java - Khóa nội tại và đồng bộ hóa . Tiếp theo, điều quan trọng là phải hiểu cách một đối tượng trong Java có thể được liên kết với một màn hình. Trong Java, mỗi đối tượng có một tiêu đề lưu trữ siêu dữ liệu nội bộ không có sẵn cho lập trình viên từ mã nhưng máy ảo cần để hoạt động chính xác với các đối tượng. Tiêu đề đối tượng bao gồm một "từ đánh dấu", giống như sau: Cùng nhau tốt hơn: Java và lớp Thread.  Phần II — Đồng bộ hóa - 6

https://edu.netbeans.org/contrib/slides/java-overview-and-java-se6.pdf

Đây là một bài viết về JavaWorld rất hữu ích: Máy ảo Java thực hiện đồng bộ hóa luồng như thế nào . Bài viết này nên được kết hợp với mô tả từ phần "Tóm tắt" của vấn đề sau trong hệ thống theo dõi lỗi JDK: JDK-8183909 . Bạn có thể đọc điều tương tự ở đây: JEP-8183909 . Vì vậy, trong Java, một màn hình được liên kết với một đối tượng và được sử dụng để chặn một luồng khi luồng đó cố lấy (hoặc lấy) khóa. Đây là ví dụ đơn giản nhất:

public class HelloWorld{
    public static void main(String []args){
        Object object = new Object();
        synchronized(object) {
            System.out.println("Hello World");
        }
    }
}
Ở đây, luồng hiện tại (luồng mà các dòng mã này được thực thi) sử dụng từ synchronizedkhóa để cố gắng sử dụng màn hình được liên kết vớiobject"\biến để nhận/có được khóa. Nếu không ai khác tranh giành màn hình (nghĩa là không ai khác đang chạy mã được đồng bộ hóa bằng cùng một đối tượng), thì Java có thể cố gắng thực hiện một tối ưu hóa được gọi là "khóa thiên vị". Thẻ có liên quan và bản ghi về chuỗi nào sở hữu khóa của màn hình được thêm vào từ đánh dấu trong tiêu đề đối tượng. Điều này làm giảm chi phí cần thiết để khóa màn hình. Nếu màn hình trước đây thuộc sở hữu của một luồng khác, thì việc khóa như vậy là không đủ. JVM chuyển sang kiểu khóa tiếp theo: "khóa cơ bản". Nó sử dụng các hoạt động so sánh và hoán đổi (CAS). Hơn nữa, bản thân từ đánh dấu của tiêu đề đối tượng không còn lưu trữ từ đánh dấu nữa, mà thay vào đó là một tham chiếu đến nơi nó được lưu trữ và thẻ thay đổi để JVM hiểu rằng chúng ta đang sử dụng khóa cơ bản. Nếu nhiều luồng cạnh tranh (tranh giành) để giành được một màn hình (một luồng đã giành được khóa và luồng thứ hai đang đợi mở khóa), thì thẻ trong từ đánh dấu sẽ thay đổi và từ đánh dấu hiện lưu trữ tham chiếu đến màn hình như một đối tượng - một số thực thể nội bộ của JVM. Như đã nêu trong Đề xuất tăng cường JDK (JEP), tình huống này yêu cầu không gian trong vùng Heap gốc của bộ nhớ để lưu trữ thực thể này. Tham chiếu đến vị trí bộ nhớ của thực thể bên trong này sẽ được lưu trữ trong từ đánh dấu của tiêu đề đối tượng. Do đó, một màn hình thực sự là một cơ chế để đồng bộ hóa quyền truy cập vào các tài nguyên được chia sẻ giữa nhiều luồng. JVM chuyển đổi giữa một số cài đặt của cơ chế này. Vì vậy, để đơn giản, khi nói về màn hình, thực ra chúng ta đang nói về ổ khóa. và một giây đang đợi khóa được giải phóng), sau đó thẻ trong từ đánh dấu sẽ thay đổi và từ đánh dấu hiện lưu trữ một tham chiếu đến màn hình dưới dạng một đối tượng — một số thực thể bên trong của JVM. Như đã nêu trong Đề xuất tăng cường JDK (JEP), tình huống này yêu cầu không gian trong vùng Heap gốc của bộ nhớ để lưu trữ thực thể này. Tham chiếu đến vị trí bộ nhớ của thực thể bên trong này sẽ được lưu trữ trong từ đánh dấu của tiêu đề đối tượng. Do đó, một màn hình thực sự là một cơ chế để đồng bộ hóa quyền truy cập vào các tài nguyên được chia sẻ giữa nhiều luồng. JVM chuyển đổi giữa một số cài đặt của cơ chế này. Vì vậy, để đơn giản, khi nói về màn hình, thực ra chúng ta đang nói về ổ khóa. và một giây đang đợi khóa được giải phóng), sau đó thẻ trong từ đánh dấu sẽ thay đổi và từ đánh dấu hiện lưu trữ một tham chiếu đến màn hình dưới dạng một đối tượng — một số thực thể bên trong của JVM. Như đã nêu trong Đề xuất tăng cường JDK (JEP), tình huống này yêu cầu không gian trong vùng Heap gốc của bộ nhớ để lưu trữ thực thể này. Tham chiếu đến vị trí bộ nhớ của thực thể bên trong này sẽ được lưu trữ trong từ đánh dấu của tiêu đề đối tượng. Do đó, một màn hình thực sự là một cơ chế để đồng bộ hóa quyền truy cập vào các tài nguyên được chia sẻ giữa nhiều luồng. JVM chuyển đổi giữa một số cài đặt của cơ chế này. Vì vậy, để đơn giản, khi nói về màn hình, thực ra chúng ta đang nói về ổ khóa. và từ đánh dấu hiện lưu trữ một tham chiếu đến màn hình dưới dạng một đối tượng - một số thực thể nội bộ của JVM. Như đã nêu trong Đề xuất tăng cường JDK (JEP), tình huống này yêu cầu không gian trong vùng Heap gốc của bộ nhớ để lưu trữ thực thể này. Tham chiếu đến vị trí bộ nhớ của thực thể bên trong này sẽ được lưu trữ trong từ đánh dấu của tiêu đề đối tượng. Do đó, một màn hình thực sự là một cơ chế để đồng bộ hóa quyền truy cập vào các tài nguyên được chia sẻ giữa nhiều luồng. JVM chuyển đổi giữa một số cài đặt của cơ chế này. Vì vậy, để đơn giản, khi nói về màn hình, thực ra chúng ta đang nói về ổ khóa. và từ đánh dấu hiện lưu trữ một tham chiếu đến màn hình dưới dạng một đối tượng - một số thực thể nội bộ của JVM. Như đã nêu trong Đề xuất tăng cường JDK (JEP), tình huống này yêu cầu không gian trong vùng Heap gốc của bộ nhớ để lưu trữ thực thể này. Tham chiếu đến vị trí bộ nhớ của thực thể bên trong này sẽ được lưu trữ trong từ đánh dấu của tiêu đề đối tượng. Do đó, một màn hình thực sự là một cơ chế để đồng bộ hóa quyền truy cập vào các tài nguyên được chia sẻ giữa nhiều luồng. JVM chuyển đổi giữa một số cài đặt của cơ chế này. Vì vậy, để đơn giản, khi nói về màn hình, thực ra chúng ta đang nói về ổ khóa. Tham chiếu đến vị trí bộ nhớ của thực thể bên trong này sẽ được lưu trữ trong từ đánh dấu của tiêu đề đối tượng. Do đó, một màn hình thực sự là một cơ chế để đồng bộ hóa quyền truy cập vào các tài nguyên được chia sẻ giữa nhiều luồng. JVM chuyển đổi giữa một số cài đặt của cơ chế này. Vì vậy, để đơn giản, khi nói về màn hình, thực ra chúng ta đang nói về ổ khóa. Tham chiếu đến vị trí bộ nhớ của thực thể bên trong này sẽ được lưu trữ trong từ đánh dấu của tiêu đề đối tượng. Do đó, một màn hình thực sự là một cơ chế để đồng bộ hóa quyền truy cập vào các tài nguyên được chia sẻ giữa nhiều luồng. JVM chuyển đổi giữa một số cài đặt của cơ chế này. Vì vậy, để đơn giản, khi nói về màn hình, thực ra chúng ta đang nói về ổ khóa. Cùng nhau tốt hơn: Java và lớp Thread.  Phần II — Đồng bộ hóa - 7

Đã đồng bộ hóa (chờ khóa)

Như chúng ta đã thấy trước đó, khái niệm "khối đồng bộ" (hoặc "phần quan trọng") có liên quan chặt chẽ với khái niệm màn hình. Hãy xem một ví dụ:

public static void main(String[] args) throws InterruptedException {
	Object lock = new Object();

	Runnable task = () -> {
		synchronized(lock) {
			System.out.println("thread");
		}
	};

	Thread th1 = new Thread(task);
	th1.start();
	synchronized(lock) {
		for (int i = 0; i < 8; i++) {
			Thread.currentThread().sleep(1000);
			System.out.print(" " + i);
		}
		System.out.println(" ...");
	}
}
Ở đây, trước tiên, luồng chính chuyển đối tượng tác vụ sang luồng mới, sau đó ngay lập tức lấy khóa và thực hiện một thao tác dài với nó (8 giây). Tất cả thời gian này, nhiệm vụ không thể tiếp tục, vì nó không thể vào synchronizedkhối, vì khóa đã được mua. Nếu luồng không lấy được khóa, nó sẽ đợi màn hình. Ngay khi nhận được khóa, nó sẽ tiếp tục thực hiện. Khi một luồng thoát khỏi màn hình, nó sẽ giải phóng khóa. Trong JVisualVM, nó trông như thế này: Cùng nhau tốt hơn: Java và lớp Thread.  Phần II — Đồng bộ hóa - 8Như bạn có thể thấy trong JVisualVM, trạng thái là "Monitor", nghĩa là luồng bị chặn và không thể lấy màn hình. Bạn cũng có thể sử dụng mã để xác định trạng thái của luồng, nhưng tên của trạng thái được xác định theo cách này không khớp với tên được sử dụng trong JVisualVM, mặc dù chúng tương tự nhau. Trong trường hợp này, cácth1.getState()câu lệnh trong vòng lặp for sẽ trả về BLOCKED , vì chừng nào vòng lặp còn chạy, lockmàn hình của đối tượng bị chiếm giữ bởi mainluồng và th1luồng bị chặn và không thể tiếp tục cho đến khi khóa được giải phóng. Ngoài các khối được đồng bộ hóa, toàn bộ phương thức có thể được đồng bộ hóa. Ví dụ, đây là một phương thức từ lớp HashTable:

public synchronized int size() {
	return count;
}
Phương thức này sẽ chỉ được thực hiện bởi một luồng tại bất kỳ thời điểm nào. Chúng ta có thực sự cần khóa không? Vâng, chúng tôi cần nó. Trong trường hợp các phương thức thể hiện, đối tượng "this" (đối tượng hiện tại) hoạt động như một khóa. Có một cuộc thảo luận thú vị về chủ đề này tại đây: Có lợi thế nào khi sử dụng Phương thức được đồng bộ hóa thay vì Khối được đồng bộ hóa không? . Nếu phương thức là tĩnh, thì khóa sẽ không phải là đối tượng "this" (vì không có đối tượng "this" cho phương thức tĩnh), mà là đối tượng Class (ví dụ: ) Integer.class.

Chờ (đợi màn hình). phương pháp thông báo () và thông báo Tất cả ()

Lớp Thread có một phương thức chờ khác được liên kết với màn hình. Không giống như sleep()join(), phương thức này không thể được gọi một cách đơn giản. Tên của nó là wait(). Phương thức này waitđược gọi trên đối tượng được liên kết với màn hình mà chúng ta muốn đợi. Hãy xem một ví dụ:

public static void main(String []args) throws InterruptedException {
	    Object lock = new Object();
	    // The task object will wait until it is notified via lock
	    Runnable task = () -> {
	        synchronized(lock) {
	            try {
	                lock.wait();
	            } catch(InterruptedException e) {
	                System.out.println("interrupted");
	            }
	        }
	        // After we are notified, we will wait until we can acquire the lock
	        System.out.println("thread");
	    };
	    Thread taskThread = new Thread(task);
	    taskThread.start();
        // We sleep. Then we acquire the lock, notify, and release the lock
	    Thread.currentThread().sleep(3000);
	    System.out.println("main");
	    synchronized(lock) {
	        lock.notify();
	    }
}
Trong JVisualVM, nó trông như thế này: Cùng nhau tốt hơn: Java và lớp Thread.  Phần II — Đồng bộ hóa - 10Để hiểu cách thức hoạt động của nó, hãy nhớ rằng các phương thức wait()notify()được liên kết với java.lang.Object. Có vẻ lạ khi các phương thức liên quan đến luồng nằm trong Objectlớp. Nhưng lý do cho điều đó bây giờ mở ra. Bạn sẽ nhớ lại rằng mọi đối tượng trong Java đều có một tiêu đề. Tiêu đề chứa nhiều thông tin vệ sinh khác nhau, bao gồm thông tin về màn hình, tức là trạng thái của khóa. Hãy nhớ rằng, mỗi đối tượng hoặc thể hiện của một lớp được liên kết với một thực thể nội bộ trong JVM, được gọi là khóa hoặc màn hình nội tại. Trong ví dụ trên, mã cho đối tượng nhiệm vụ chỉ ra rằng chúng ta nhập khối được đồng bộ hóa cho màn hình được liên kết với đối locktượng. Nếu chúng tôi thành công trong việc lấy khóa cho màn hình này, thìwait()được gọi là. Luồng thực hiện tác vụ sẽ giải phóng lockmàn hình của đối tượng, nhưng sẽ vào hàng đợi các luồng đang chờ thông báo từ lockmàn hình của đối tượng. Hàng đợi các luồng này được gọi là WAIT SET, phản ánh chính xác hơn mục đích của nó. Đó là, nó giống một tập hợp hơn là một hàng đợi. Chuỗi maintạo một chuỗi mới với đối tượng tác vụ, khởi động nó và đợi 3 giây. Điều này làm cho luồng mới có khả năng cao nhận được khóa trước luồng mainvà vào hàng đợi của màn hình. Sau đó, mainluồng tự đi vào lockkhối đồng bộ của đối tượng và thực hiện thông báo luồng bằng màn hình. Sau khi thông báo được gửi đi, mainluồng sẽ giải phónglockmàn hình của đối tượng và luồng mới, trước đó đang đợi lockmàn hình của đối tượng được giải phóng, tiếp tục thực hiện. Có thể gửi thông báo tới chỉ một luồng ( notify()) hoặc đồng thời tới tất cả các luồng trong hàng đợi ( notifyAll()). Đọc thêm tại đây: Sự khác biệt giữa thông báo() và thông báoAll() trong Java . Điều quan trọng cần lưu ý là thứ tự thông báo phụ thuộc vào cách JVM được triển khai. Đọc thêm tại đây: Cách giải quyết tình trạng đói với thông báo và thông báo ALL? . Đồng bộ hóa có thể được thực hiện mà không cần chỉ định một đối tượng. Bạn có thể thực hiện việc này khi toàn bộ phương thức được đồng bộ hóa thay vì một khối mã đơn lẻ. Ví dụ: đối với các phương thức tĩnh, khóa sẽ là một đối tượng Lớp (có được qua .class):

public static synchronized void printA() {
	System.out.println("A");
}
public static void printB() {
	synchronized(HelloWorld.class) {
		System.out.println("B");
	}
}
Về cách sử dụng khóa, cả hai phương pháp đều giống nhau. Nếu một phương thức không phải là tĩnh, thì việc đồng bộ hóa sẽ được thực hiện bằng cách sử dụng phương thức hiện tại instance, nghĩa là sử dụng this. Nhân tiện, chúng tôi đã nói trước đó rằng bạn có thể sử dụng getState()phương thức này để lấy trạng thái của một chuỗi. Ví dụ: đối với một chuỗi trong hàng đợi màn hình, trạng thái sẽ là WAITING hoặc TIMED_WAITING, nếu phương wait()thức chỉ định thời gian chờ. Cùng nhau tốt hơn: Java và lớp Thread.  Phần II — Đồng bộ hóa - 11

https://stackoverflow.com/questions/36425942/what-is-the-lifecycle-of-thread-in-java

Vòng đời chủ đề

Trong suốt vòng đời của nó, trạng thái của một luồng sẽ thay đổi. Trên thực tế, những thay đổi này bao gồm vòng đời của luồng. Ngay sau khi một chủ đề được tạo, trạng thái của nó là MỚI. Ở trạng thái này, luồng mới chưa chạy và bộ lập lịch luồng Java chưa biết gì về nó. Để bộ lập lịch luồng tìm hiểu về luồng, bạn phải gọi thread.start()phương thức. Sau đó, luồng sẽ chuyển sang trạng thái RUNNABLE. Internet có rất nhiều sơ đồ không chính xác phân biệt giữa trạng thái "Có thể chạy được" và "Đang chạy". Nhưng đây là một sai lầm, vì Java không phân biệt giữa "sẵn sàng hoạt động" (runnable) và "đang hoạt động" (running). Khi một luồng đang hoạt động nhưng không hoạt động (không phải Runnable), nó ở một trong hai trạng thái:
  • BỊ CHẶN — đang chờ để vào phần quan trọng, tức là một synchronizedkhối.
  • WAITING - chờ đợi một luồng khác để đáp ứng một số điều kiện.
Nếu điều kiện được thỏa mãn, thì bộ lập lịch luồng sẽ bắt đầu luồng. Nếu chuỗi đang chờ đến một thời điểm đã chỉ định, thì trạng thái của chuỗi là TIMED_WAITING. Nếu luồng không còn chạy nữa (đã kết thúc hoặc đã ném ngoại lệ), thì luồng đó sẽ chuyển sang trạng thái ĐÃ KẾT THÚC. Để tìm hiểu trạng thái của luồng, hãy sử dụng getState()phương thức. Các luồng cũng có một isAlive()phương thức, phương thức này trả về giá trị true nếu luồng không bị KẾT THÚC.

LockSupport và chủ đề đậu xe

Bắt đầu với Java 1.6, một cơ chế thú vị được gọi là LockSupport đã xuất hiện. Cùng nhau tốt hơn: Java và lớp Thread.  Phần II — Đồng bộ hóa - 12Lớp này liên kết một "giấy phép" với mỗi luồng sử dụng nó. Một cuộc gọi đến park()phương thức sẽ trả về ngay lập tức nếu giấy phép có sẵn, sử dụng giấy phép trong quá trình này. Nếu không, nó sẽ chặn. Gọi unparkphương thức làm cho giấy phép có sẵn nếu nó chưa có sẵn. Chỉ có 1 giấy phép. Tài liệu Java dành cho LockSupportđề cập đến Semaphorelớp. Hãy xem xét một ví dụ đơn giản:

import java.util.concurrent.Semaphore;
public class HelloWorldApp{
    
    public static void main(String[] args) {
        Semaphore semaphore = new Semaphore(0);
        try {
            semaphore.acquire();
        } catch (InterruptedException e) {
            // Request the permit and wait until we get it
            e.printStackTrace();
        }
        System.out.println("Hello, World!");
    }
}
Mã này sẽ luôn đợi, vì bây giờ semaphore có 0 giấy phép. Và khi acquire()được gọi trong mã (tức là yêu cầu giấy phép), luồng sẽ đợi cho đến khi nhận được giấy phép. Vì chúng tôi đang chờ đợi, chúng tôi phải xử lý InterruptedException. Thật thú vị, semaphore có trạng thái luồng riêng biệt. Nếu chúng ta nhìn vào JVisualVM, chúng ta sẽ thấy rằng trạng thái không phải là "Chờ", mà là "Đỗ". Cùng nhau tốt hơn: Java và lớp Thread.  Phần II — Đồng bộ hóa - 13Hãy xem xét một ví dụ khác:

public static void main(String[] args) throws InterruptedException {
        Runnable task = () -> {
            // Park the current thread
            System.err.println("Will be Parked");
            LockSupport.park();
            // As soon as we are unparked, we will start to act
            System.err.println("Unparked");
        };
        Thread th = new Thread(task);
        th.start();
        Thread.currentThread().sleep(2000);
        System.err.println("Thread state: " + th.getState());
        
        LockSupport.unpark(th);
        Thread.currentThread().sleep(2000);
}
Trạng thái của chuỗi sẽ là CHỜ, nhưng JVisualVM phân biệt giữa waittừ synchronizedkhóa và parktừ LockSupportlớp. Tại sao điều này LockSupportrất quan trọng? Chúng tôi quay lại tài liệu Java và xem xét trạng thái luồng CHỜ ĐỢI . Như bạn có thể thấy, chỉ có ba cách để vào đó. Hai trong số những cách đó là wait()join(). Và thứ ba là LockSupport. Trong Java, khóa cũng có thể được xây dựng trên LockSupport và cung cấp các công cụ cấp cao hơn. Hãy thử sử dụng một cái. Ví dụ: hãy xem ReentrantLock:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class HelloWorld{

    public static void main(String []args) throws InterruptedException {
        Lock lock = new ReentrantLock();
        Runnable task = () -> {
            lock.lock();
            System.out.println("Thread");
            lock.unlock();
        };
        lock.lock();

        Thread th = new Thread(task);
        th.start();
        System.out.println("main");
        Thread.currentThread().sleep(2000);
        lock.unlock();
    }
}
Giống như trong các ví dụ trước, mọi thứ đều đơn giản ở đây. Đối locktượng đợi ai đó giải phóng tài nguyên được chia sẻ. Nếu chúng ta nhìn vào JVisualVM, chúng ta sẽ thấy rằng luồng mới sẽ được tạm dừng cho đến khi luồng maingiải phóng khóa cho nó. Bạn có thể đọc thêm về khóa tại đây: Java 8 StampedLocks so với ReadWriteLocks và API khóa và đồng bộ hóa trong Java. Để hiểu rõ hơn cách các khóa được triển khai, bạn nên đọc về Phaser trong bài viết này: Hướng dẫn về Phaser Java . Và nói về các bộ đồng bộ hóa khác nhau, bạn phải đọc bài viết DZone trên Bộ đồng bộ hóa Java.

Phần kết luận

Trong bài đánh giá này, chúng tôi đã kiểm tra các cách chính mà các luồng tương tác trong Java. Tài liệu bổ sung: Kết hợp tốt hơn: Java và lớp Thread. Phần I - Các luồng thực thi Cùng nhau tốt hơn: Java và lớp Thread. Phần III — Tương tác tốt hơn khi kết hợp với nhau: Java và lớp Thread. Phần IV - Có thể gọi được, Tương lai và bạn bè Tốt hơn cùng nhau: Java và lớp Chủ đề. Phần V — Executor, ThreadPool, Fork/Join Together tốt hơn: Java và lớp Thread. Phần VI — Bắn đi!
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION