JavaRush /Blog Java /Random-VI /Bạn không thể phá hỏng Java bằng một luồng: Phần II - đồn...
Viacheslav
Mức độ

Bạn không thể phá hỏng Java bằng một luồng: Phần II - đồng bộ hóa

Xuất bản trong nhóm

Giới thiệu

Vì vậy, chúng tôi biết rằng có những luồng trong Java mà bạn có thể đọc trong bài đánh giá “ Bạn không thể làm hỏng Java bằng một luồng: Phần I - Chủ đề ”. Chủ đề là cần thiết để thực hiện công việc đồng thời. Vì vậy, rất có thể các thread sẽ tương tác với nhau bằng cách nào đó. Hãy hiểu điều này xảy ra như thế nào và chúng ta có những biện pháp kiểm soát cơ bản nào. Bạn không thể hủy hoại Java chỉ bằng một luồng: Phần II - đồng bộ hóa - 1

Năng suất

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

Ngủ - Chủ đề ngủ quên

Một thread có thể rơi vào trạng thái ngủ trong quá trình thực thi. Đây là loại tương tác đơn giản nhất với các chủ đề khác. Hệ điều hành mà máy ảo Java được cài đặt, nơi mã Java được thực thi, có bộ lập lịch luồng riêng, được gọi là Bộ lập lịch luồng. Chính anh ta là người quyết định chủ đề nào sẽ chạy khi nào. 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, nhưng anh ta có thể, thông qua JVM, yêu cầu bộ lập lịch tạm dừng luồng một lúc để “đưa nó vào chế độ ngủ”. Bạn có thể đọc thêm ở bài viết " Thread.sleep() " và " How Multithreading works ". Hơn nữa, bạn có thể tìm hiểu cách hoạt động của các luồng trong hệ điều hành Windows: " Internals of Windows Thread ". Bây giờ chúng ta sẽ nhìn thấy nó bằng chính mắt mình. Hãy lưu đoạn mã sau vào một tập tin HelloWorldApp.java:
class HelloWorldApp {
    public static void main(String []args) {
        Runnable task = () -> {
            try {
                int secToWait = 1000 * 60;
                Thread.currentThread().sleep(secToWait);
                System.out.println("Waked up");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        };
        Thread thread = new Thread(task);
        thread.start();
    }
}
Như bạn có thể thấy, chúng ta có một tác vụ đợi 60 giây, sau đó chương trình sẽ kết thúc. Chúng tôi biên dịch javac HelloWorldApp.javavà chạy java HelloWorldApp. Tốt hơn là khởi chạy trong một cửa sổ riêng. Ví dụ: trên Windows nó sẽ như thế này: start java HelloWorldApp. Sử dụng lệnh jps, chúng tôi tìm ra PID của quy trình và mở danh sách các luồng bằng cách sử dụng jvisualvm --openpid pidПроцесса: Bạn không thể phá hỏng Java chỉ bằng một luồng: Phần II - đồng bộ hóa - 3Như bạn có thể thấy, luồng của chúng tôi đã chuyển sang trạng thái Ngủ. Trong thực tế, việc ngủ luồng hiện tại có thể được thực hiện đẹp hơn:
try {
	TimeUnit.SECONDS.sleep(60);
	System.out.println("Waked up");
} catch (InterruptedException e) {
	e.printStackTrace();
}
Có lẽ bạn đã nhận thấy rằng chúng tôi xử lý ở mọi nơi InterruptedException? Hãy hiểu tại sao.

Làm gián đoạn một chủ đề hoặc Thread.interrupt

Vấn đề là trong khi sợi dây đang chờ đợi trong giấc mơ, ai đó có thể muốn làm gián đoạn sự chờ đợi này. Trong trường hợp này, chúng tôi xử lý một ngoại lệ như vậy. Việc này được thực hiện sau khi phương thức Thread.stopđược khai báo là Không dùng nữa, tức là đã lỗi thời và không được sử dụng nữa. Lý do cho điều này là khi phương thức này được gọi, stopthread 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 luồng sẽ dừng lại, 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 và sau đó luồng bị hủy. Vì vậy, chúng tôi quyết định rằng sẽ hợp lý hơn nếu không tắt luồng mà thông báo cho nó rằng nó nên bị gián đoạn. Làm thế nào để phản ứng với điều này là tùy thuộc vào dòng chảy. Bạn có thể tìm thêm thông tin chi tiết trong phần " Tại sao Thread.stop không được dùng nữa? " Hãy xem 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 mà sẽ in ngay lập tức 'Interrupted'. Điều này là do chúng tôi đã gọi phương thức của luồng interrupt. Phương thức này đặ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 pháp riêng để tương tác với cờ này. Nhưng đây không phải là cách duy nhất. Một luồng có thể đang trong quá trình thực thi, không chờ đợi điều gì đó mà chỉ thực hiện các hành động. Nhưng nó có thể đảm bảo rằng họ sẽ muốn hoàn thành nó ở một thời điểm nhất định trong công việc của mình. 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, bạn có thể thấy vòng lặp whilesẽ chạy cho đến khi luồng bị gián đoạn từ bên ngoài. Điều quan trọng cần biết về cờ isInterrupted là nếu chúng ta bắt được cờ này InterruptedException, cờ đó isInterruptedsẽ được đặt lại và sau đó isInterruptednó sẽ trả về sai. Ngoài ra còn có một phương thức tĩnh trong lớp Thread chỉ áp dụng cho luồng hiện tại - Thread.interrupted() , nhưng phương thức này đặt lại cờ thành sai! Bạn có thể đọc thêm ở chương " Gián đoạn luồng ".

Tham gia - Đang chờ một chủ đề khác hoàn thành

Kiểu chờ đơn giản nhất là chờ một luồng khác hoàn thành.
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ẽ ngủ trong 5 giây. Đồng thời, main thread sẽ đợi cho đến khi thread ngủ thức dậy và hoàn thành công việc của mình. Nếu bạn xem qua JVisualVM, trạng thái của luồng sẽ như thế này: Bạn không thể phá hỏng Java chỉ bằng một luồng: 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, vì nó đơn giản là một phương thức có mã java thực thi waittrong khi luồng mà nó được gọi vẫn còn hoạt động. Khi luồng chết (khi kết thúc), việc chờ đợi sẽ chấm dứt. Đó là toàn bộ sự kỳ diệu của phương pháp này join. Vì vậy, hãy chuyển sang phần thú vị nhất.

Giám sát khái niệm

Trong đa luồng có một thứ như Monitor. Nói chung, từ giám sát được dịch từ tiếng Latin là “người giám sát” hoặc “người giám sát”. Trong khuôn khổ bài viết này, chúng tôi sẽ cố gắng ghi nhớ bản chất và đối với những ai muốn, tôi yêu cầu bạn đi sâu vào tài liệu từ các liên kết để biết chi tiết. Hãy bắt đầu hành trình của chúng ta với đặc tả ngôn ngữ Java, tức là với JLS: " 17.1. Synchronization ". Nó nói như sau: Bạn không thể phá hỏng Java chỉ bằng một luồng: Phần II - đồng bộ hóa - 5Hóa ra với mục đích đồng bộ hóa giữa các luồng, Java sử dụng một cơ chế nhất định gọi là “Màn hình”. Mỗi đối tượng có một màn hình được liên kết với nó và các luồng có thể khóa hoặc mở khóa nó. Tiếp theo, chúng ta sẽ tìm thấy hướng dẫn đào tạo trên trang web của Oracle: “ Khóa nội tại và đồng bộ hóa ”. Hướng dẫn này giải thích rằng đồng bộ hóa trong 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. Thông thường, khóa như vậy đượ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àn hình. Mỗi đối tượng trong Java có một tiêu đề - một loại 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 MarkWord trông như thế này: Bạn không thể phá hỏng Java chỉ bằng một luồng: Phần II - đồng bộ hóa - 6

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

Một bài viết của Habr rất hữu ích ở đây: " Nhưng đa luồng hoạt động như thế nào? Phần I: đồng bộ hóa ." Đối với bài viết này, đáng để thêm một mô tả từ Tóm tắt khối tác vụ từ trình sửa lỗi JDK: “ JDK-8183909 ”. Bạn có thể đọc điều tương tự trong " 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à luồng có thể chặn luồng này hoặc người ta cũng nói “lấy khóa”. 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");
        }
    }
}
Vì vậy, bằng cách sử dụng từ khóa, synchronizedluồng hiện tại (trong đó các dòng mã này được thực thi) sẽ cố gắng sử dụng màn hình được liên kết với đối tượng objectvà “lấy khóa” hoặc “chụp màn hình” (tùy chọn thứ hai thậm chí còn thích hợp hơn). Nếu không có sự tranh chấp nào về màn hình (tức là không ai khác muốn đồng bộ hóa trên cùng một đối tượng), Java có thể thử thực hiện một tối ưu hóa được gọi là "khóa thiên vị". Tiêu đề của đối tượng trong Mark Word sẽ chứa thẻ tương ứng và bản ghi về chủ đề mà màn hình được gắn vào. Điều này làm giảm chi phí khi chụp màn hình. Nếu màn hình đã được buộc vào một luồng khác trước đó thì việc khóa này là không đủ. JVM chuyển sang loại khóa tiếp theo - khóa cơ bản. Nó sử dụng các hoạt động so sánh và trao đổi (CAS). Đồng thời, tiêu đề trong Mark Word không còn lưu trữ chính Mark Word nữa mà liên kết tới bộ lưu trữ của nó + thẻ được thay đổi để JVM hiểu rằng chúng ta đang sử dụng khóa cơ bản. Nếu có sự tranh chấp về màn hình của một số luồng (một luồng đã chụp màn hình và luồng thứ hai đang chờ màn hình được giải phóng), thì thẻ trong Mark Word sẽ thay đổi và Mark Word bắt đầu lưu trữ 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 JEP, trong trường hợp này, cần có không gian trong vùng bộ nhớ Heap gốc để lưu trữ thực thể này. Liên kết đến vị trí lưu trữ của thực thể bên trong này sẽ nằm trong đối tượng Mark Word. Vì vậy, như chúng ta thấy, màn hình thực sự là một cơ chế đảm bảo đồng bộ hóa quyền truy cập của nhiều luồng vào các tài nguyên được chia sẻ. Có một số cách triển khai cơ chế này mà JVM chuyển đổi giữa chúng. Vì vậy, để đơn giản, khi nói đến màn hình, thực chất chúng ta đang nói đến ổ khóa. Bạn không thể phá hỏng Java bằng một luồng: Phần II - đồng bộ hóa - 7

Đồng bộ hóa và chờ bằng khóa

Khái niệm màn hình, như chúng ta đã thấy trước đây, có liên quan chặt chẽ với khái niệm “khối đồng bộ hóa” (hoặc, như nó còn được gọi là phần quan trọng). 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, luồng chính trước tiên sẽ gửi tác vụ đến một luồng mới, sau đó ngay lập tức “bắt” khóa và thực hiện một thao tác dài với nó (8 giây). Trong suốt thời gian này, tác vụ không thể vào khối để thực thi nó synchronized, bởi vì ổ khóa đã có người sử dụng. Nếu một thread không thể lấy được khóa, nó sẽ đợi ở màn hình. Ngay sau khi nhận được nó, nó sẽ tiếp tục thực hiện. Khi một sợi dây rời khỏi màn hình, nó sẽ nhả khóa. Trong JVisualVM nó sẽ trông như thế này: Bạn không thể phá hỏng Java chỉ bằng một luồng: Phần II - đồng bộ hóa - 8Như bạn có thể thấy, trạng thái trong JVisualVM được gọi là "Màn hình" vì luồng bị chặn và không thể chiếm màn hình. Bạn cũng có thể tìm hiểu trạng thái của luồng trong mã, nhưng tên của trạng thái này không trùng với các thuật ngữ JVisualVM, mặc dù chúng tương tự nhau. Trong trường hợp này, th1.getState()vòng lặp forsẽ trả về BLOCKED , bởi vì Trong khi vòng lặp đang chạy, màn hình lockbị chiếm mainbởi luồng và luồng th1bị chặn và không thể tiếp tục hoạt động cho đến khi khóa được trả lại. Ngoài các khối đồng bộ hóa, toàn bộ phương thức có thể được đồng bộ hóa. Ví dụ: một phương thức từ lớp HashTable:
public synchronized int size() {
	return count;
}
Trong một đơn vị thời gian, phương thức này sẽ chỉ được thực thi bởi một luồng. Nhưng chúng ta cần một cái khóa, phải không? Vâng tôi cần nó. Trong trường hợp phương thức đối tượng, khóa sẽ là this. Có một cuộc thảo luận thú vị về chủ đề nà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 này là tĩnh thì khóa sẽ không phải là this(vì đối với phương thức tĩnh thì không thể là this), mà là đối tượng lớp (Ví dụ: Integer.class).

Đợi và chờ đợi trên màn hình. Các phương thức thông báo và thông báoTất cả

Thread có một phương thức chờ khác được kết nối với màn hình. Không giống như sleepand join, nó không thể được gọi. Và tên anh ấy là wait. Phương thức này được thực thi waittrên đối tượng mà chúng ta muốn đợi trên màn hình. Hãy xem một ví dụ:
public static void main(String []args) throws InterruptedException {
	    Object lock = new Object();
	    // task будет ждать, пока его не оповестят через lock
	    Runnable task = () -> {
	        synchronized(lock) {
	            try {
	                lock.wait();
	            } catch(InterruptedException e) {
	                System.out.println("interrupted");
	            }
	        }
	        // После оповещения нас мы будем ждать, пока сможем взять лок
	        System.out.println("thread");
	    };
	    Thread taskThread = new Thread(task);
	    taskThread.start();
        // Ждём и после этого забираем себе лок, оповещаем и отдаём лок
	    Thread.currentThread().sleep(3000);
	    System.out.println("main");
	    synchronized(lock) {
	        lock.notify();
	    }
}
Trong JVisualVM nó sẽ trông như thế này: Bạn không thể hủy hoại Java chỉ bằng một luồng: Phần II - đồng bộ hóa - 10Để hiểu cách hoạt động của nó, bạn nên nhớ rằng các phương thức waittham notifykhảo java.lang.Object. Có vẻ lạ khi các phương thức liên quan đến luồng nằm trong tệp Object. Nhưng đây là câu trả lời. Như chúng ta nhớ, mọi đối tượng trong Java đều có tiêu đề. Tiêu đề chứa nhiều thông tin dịch vụ khác nhau, bao gồm thông tin về màn hình—dữ liệu về trạng thái khóa. Và như chúng ta nhớ, mỗi đối tượng (tức là mỗi phiên bản) có một liên kết với một thực thể JVM bên trong được gọi là khóa nội tại, còn được gọi là màn hình. Trong ví dụ trên, tác vụ mô tả rằng chúng ta nhập khối đồng bộ hóa trên màn hình được liên kết với lock. Nếu có thể lấy được khóa trên màn hình này thì wait. Luồng thực thi tác vụ này sẽ giải phóng màn hình locknhưng sẽ tham gia vào hàng đợi các luồng đang chờ thông báo trên màn hình lock. Hàng chủ đề này được gọi là WAIT-SET, phản ánh bản chất chính xác hơn. Nó giống một bộ hơn là một hàng đợi. Chuỗi maintạo một chuỗi mới với tác vụ tác vụ, khởi động nó và đợi trong 3 giây. Điều này cho phép, với xác suất cao, một luồng mới lấy khóa trước luồng mainvà xếp hàng trên màn hình. Sau đó, luồng maintự đi vào khối đồng bộ hóa lockvà thực hiện thông báo về luồng trên màn hình. Sau khi thông báo được gửi, luồng mainsẽ giải phóng màn hình lockvà luồng mới (đã chờ trước đó) locktiếp tục thực thi sau khi chờ màn hình được giải phóng. Có thể gửi thông báo tới chỉ một trong các luồng ( notify) hoặc tới tất cả các luồng trong hàng đợi cùng một lúc ( notifyAll). Bạn có thể đọc thêm trong phần " 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 việc triển khai JVM. Bạn có thể đọc thêm trong phần " Làm thế nào để giải quyết tình trạng đói bằng thông báo và thông báo tất cả? ". Đồng bộ hóa có thể được thực hiện mà không cần chỉ định đối tượng. Điều này có thể được thực hiện khi không phải một phần mã riêng biệt được đồng bộ hóa mà là toàn bộ phương thức. Ví dụ: đối với các phương thức tĩnh, khóa sẽ là đối tượng lớp (thu đượ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 phương thức không tĩnh thì việc đồng bộ hóa sẽ được thực hiện theo hiện tại instance, tức là theo this. Nhân tiện, trước đó chúng tôi đã nói rằng bằng cách sử dụng phương thức này, getStatebạn có thể nhận được trạng thái của một chuỗi. Vì vậy, đây là một luồng được màn hình xếp hàng đợi, trạng thái sẽ là WAITING hoặc TIMED_WAITING nếu phương thức waitchỉ định giới hạn thời gian chờ. Bạn không thể phá hỏng Java chỉ bằng một luồng: Phần II - đồng bộ hóa - 11

Vòng đời của một thread

Như chúng ta đã thấy, dòng chảy thay đổi trạng thái của nó trong quá trình sống. Về bản chất, những thay đổi này chính là vòng đời của thread. Khi một chủ đề vừa được tạo, nó có trạng thái MỚI. Ở vị trí này, nó vẫn chưa bắt đầu và Bộ lập lịch luồng Java chưa biết gì về luồng mới. Để bộ lập lịch luồng biết về một luồng, bạn phải gọi phương thức thread.start(). Khi đó thread sẽ chuyển sang trạng thái RUNNABLE. Có nhiều sơ đồ không chính xác trên Internet trong đó trạng thái Runnable và Running được tách biệt. Nhưng đây là một sai lầm, bởi vì... Java không phân biệt giữa trạng thái "sẵn sàng chạy" và "đang chạy". Khi một thread vẫn còn hoạt động nhưng không hoạt động (không thể chạy được), nó sẽ ở một trong hai trạng thái:
  • BLOCKED - đang chờ vào phần được bảo vệ, tức là vào synchonizedkhối.
  • WAITING - chờ một luồng khác dựa trên một điều kiện. Nếu điều kiện là đúng, bộ lập lịch luồng sẽ khởi động luồng.
Nếu một luồng đang chờ theo thời gian thì nó ở trạng thái TIMED_WAITING. Nếu luồng không còn chạy nữa (hoàn thành thành công hoặc có ngoại lệ), nó sẽ chuyển sang trạng thái KẾT THÚC. Để tìm trạng thái của một luồng (trạng thái của nó), phương thức này được sử dụng getState. Các luồng cũng có một phương thức isAlivetrả về true nếu luồng không bị kết thúc.

Hỗ trợ khóa và đỗ luồng

Kể từ Java 1.6 đã có một cơ chế thú vị được gọi là LockSupport . Bạn không thể phá hỏng Java chỉ bằng một luồng: Phần II - đồng bộ hóa - 12Lớp này liên kết một "giấy phép" hoặc quyền với mỗi luồng sử dụng nó. Cuộc gọi phương thức parksẽ trả về ngay lập tức nếu có giấy phép, sử dụng cùng giấy phép đó trong suốt cuộc gọi. Nếu không nó sẽ bị chặn. Việc gọi phương thức này unparksẽ cung cấp giấy phép nếu nó chưa có sẵn. Chỉ có 1 Giấy phép. Trong API Java, LockSupportmột tệp Semaphore. Hãy xem 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) {
            // Просим разрешение и ждём, пока не получим его
            e.printStackTrace();
        }
        System.out.println("Hello, World!");
    }
}
Mã này sẽ đợi mãi vì semaphore hiện có 0 giấy phép. Và khi được gọi bằng mã acquire(tức là yêu cầu quyền), luồng sẽ đợi cho đến khi nhận được quyền. Vì chúng tôi đang chờ đợi nên chúng tôi có nghĩa vụ phải xử lý nó InterruptedException. Điều thú vị là một semaphore thực hiện một trạng thái luồng riêng biệt. Nếu nhìn vào JVisualVM, chúng ta sẽ thấy trạng thái của chúng ta không phải là Chờ mà là Park. Bạn không thể phá hỏng Java chỉ bằng một luồng: Phần II - đồng bộ hóa - 13Hãy xem một ví dụ khác:
public static void main(String[] args) throws InterruptedException {
        Runnable task = () -> {
            //Запаркуем текущий поток
            System.err.println("Will be Parked");
            LockSupport.park();
            // Как только нас распаркуют - начнём действовать
            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 luồng sẽ là ĐANG CHỜ, nhưng JVisualVM phân biệt waitgiữa synchronizedparkvới LockSupport. Tại sao điều này lại quan trọng LockSupport? Hãy quay lại API Java và xem Trạng thái luồng WAITING . Như bạn có thể thấy, chỉ có ba cách để vào được nó. 2 cách - cách này waitjoin. Và cái thứ ba là LockSupport. Các khóa trong Java được xây dựng trên cùng các nguyên tắc LockSupportvà đại diện cho các công cụ cấp cao hơn. Hãy thử sử dụng một cái. Ví dụ, chúng ta hãy xem tại 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();
    }
}
Như trong các ví dụ trước, mọi thứ ở đây đều đơn giản. lockchờ ai đó giải phóng tài nguyên. Nếu nhìn vào JVisualVM, chúng ta sẽ thấy luồng mới sẽ được giữ cho đến khi mainluồng đó khóa nó. Bạn có thể đọc thêm về các khóa tại đây: " Lập trình đa luồng trong Java 8. Phần hai. Đồng bộ hóa quyền truy cập vào các đối tượng có thể thay đổi " và " Java Lock API. Lý thuyết và ví dụ về sử dụng ." Để hiểu rõ hơn cách triển khai khóa, sẽ rất hữu ích khi đọc về Phazer trong phần tổng quan " Phaser Class ". Và nói về các trình đồng bộ hóa khác nhau, bạn phải đọc bài viết trên Habré “ Java.util.concurrent.* Synchronizers Reference ”.

Tổng cộng

Trong bài đánh giá này, chúng ta đã xem xét các cách chính mà các luồng tương tác trong Java. Tài liệu bổ sung: #Viacheslav
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION