CodeGym /Blog Java /Ngẫu nhiên /Quản lý chủ đề. Từ khóa dễ bay hơi và phương thức yield()...

Quản lý chủ đề. Từ khóa dễ bay hơi và phương thức yield()

Xuất bản trong nhóm
CHÀO! Chúng tôi tiếp tục nghiên cứu về đa luồng. Hôm nay chúng ta sẽ làm quen với volatiletừ khóa và yield()phương pháp. Hãy đi sâu vào :)

Từ khóa biến động

Khi tạo các ứng dụng đa luồng, chúng ta có thể gặp phải hai vấn đề nghiêm trọng. Đầu tiên, khi một ứng dụng đa luồng đang chạy, các luồng khác nhau có thể lưu vào bộ nhớ đệm các giá trị của biến (chúng ta đã nói về điều này trong bài học có tựa đề 'Sử dụng dễ bay hơi' ). Bạn có thể gặp tình huống trong đó một luồng thay đổi giá trị của một biến, nhưng luồng thứ hai không nhận thấy sự thay đổi đó vì luồng này đang hoạt động với bản sao biến được lưu trong bộ nhớ cache. Đương nhiên, hậu quả có thể nghiêm trọng. Giả sử rằng đó không chỉ là bất kỳ biến cũ nào mà là số dư tài khoản ngân hàng của bạn, đột nhiên bắt đầu nhảy lên nhảy xuống một cách ngẫu nhiên :) Điều đó nghe có vẻ không thú vị phải không? Thứ hai, trong Java, các thao tác đọc và ghi tất cả các kiểu nguyên thủy,longdouble, là nguyên tử. Chà, ví dụ: nếu bạn thay đổi giá trị của một intbiến trên một luồng và trên một luồng khác, bạn đọc giá trị của biến đó, thì bạn sẽ nhận được giá trị cũ hoặc giá trị mới của nó, tức là giá trị do thay đổi tạo ra trong chủ đề 1. Không có 'giá trị trung gian'. Tuy nhiên, điều này không hoạt động với longs và doubles. Tại sao? Vì hỗ trợ đa nền tảng. Hãy nhớ rằng ở những cấp độ đầu tiên, chúng tôi đã nói rằng nguyên tắc hướng dẫn của Java là 'viết một lần, chạy mọi nơi'? Điều đó có nghĩa là hỗ trợ đa nền tảng. Nói cách khác, một ứng dụng Java chạy trên tất cả các loại nền tảng khác nhau. Ví dụ: trên hệ điều hành Windows, các phiên bản Linux hoặc MacOS khác nhau. Nó sẽ chạy mà không gặp trở ngại nào trên tất cả chúng. Cân bằng 64 bit,longdoublelà những nguyên thủy 'nặng nhất' trong Java. Và một số nền tảng 32 bit nhất định không thực hiện đọc và ghi nguyên tử các biến 64 bit. Các biến như vậy được đọc và ghi trong hai thao tác. Đầu tiên, 32 bit đầu tiên được ghi vào biến, sau đó 32 bit khác được ghi. Kết quả là, một vấn đề có thể phát sinh. Một luồng ghi một số giá trị 64-bit vào một Xbiến và thực hiện như vậy trong hai thao tác. Đồng thời, một luồng thứ hai cố gắng đọc giá trị của biến và thực hiện như vậy ở giữa hai thao tác đó — khi 32 bit đầu tiên đã được ghi, nhưng 32 bit thứ hai thì chưa. Kết quả là nó đọc một giá trị trung gian, không chính xác và chúng tôi gặp lỗi. Ví dụ: nếu trên một nền tảng như vậy, chúng tôi cố gắng ghi số vào 9223372036854775809 đến một biến, nó sẽ chiếm 64 bit. Ở dạng nhị phân, nó có dạng như sau: 100000000000000000000000000000000000000000000000000000001 Chuỗi đầu tiên bắt đầu ghi số vào biến. Lúc đầu, nó ghi 32 bit đầu tiên (1000000000000000000000000000000) và sau đó là 32 bit thứ hai (0000000000000000000000000000001) Và luồng thứ hai có thể được kết hợp giữa các hoạt động này, đọc giá trị trung gian của biến (10000000000000000000000000000000), là 32 bit đầu tiên đã được ghi. Trong hệ thập phân, con số này là 2.147.483.648. Nói cách khác, chúng tôi chỉ muốn viết số 9223372036854775809 vào một biến, nhưng do thực tế là thao tác này không phải là nguyên tử trên một số nền tảng, chúng tôi có số 2.147.483.648, không biết từ đâu xuất hiện và sẽ có tác dụng không xác định. chương trình. Luồng thứ hai chỉ cần đọc giá trị của biến trước khi nó được ghi xong, tức là luồng nhìn thấy 32 bit đầu tiên, nhưng không nhìn thấy 32 bit thứ hai. Tất nhiên, những vấn đề này đã không phát sinh ngày hôm qua. Java giải quyết chúng bằng một từ khóa duy nhất: volatile. Nếu chúng ta sử dụngvolatiletừ khóa khi khai báo một số biến trong chương trình của chúng tôi…

public class Main {

   public volatile long x = 2222222222222222222L;

   public static void main(String[] args) {

   }
}
…nó có nghĩa là:
  1. Nó sẽ luôn được đọc và viết nguyên tử. Ngay cả khi đó là 64-bit doublehoặc long.
  2. Máy Java sẽ không lưu trữ nó. Vì vậy, bạn sẽ không gặp phải trường hợp 10 luồng đang hoạt động với các bản sao cục bộ của chính chúng.
Do đó, hai vấn đề rất nghiêm trọng được giải quyết chỉ bằng một từ :)

Phương thức năng suất ()

Chúng ta đã xem xét nhiều Threadphương thức của lớp, nhưng có một phương thức quan trọng sẽ mới đối với bạn. Đó là yield()phương pháp . Và nó làm chính xác những gì tên của nó ngụ ý! Quản lý chủ đề.  Từ khóa dễ bay hơi và phương thức yield() - 2Khi chúng ta gọi yieldphương thức trên một luồng, nó thực sự nói chuyện với các luồng khác: 'Này, các bạn. Tôi không đặc biệt vội vàng đi đâu cả, vì vậy nếu điều quan trọng đối với bất kỳ ai trong số các bạn là có thời gian xử lý, hãy cứ làm đi — tôi có thể đợi được'. Đây là một ví dụ đơn giản về cách thức hoạt động của nó:

public class ThreadExample extends Thread {

   public ThreadExample() {
       this.start();
   }

   public void run() {

       System.out.println(Thread.currentThread().getName() + " yields its place to others");
       Thread.yield();
       System.out.println(Thread.currentThread().getName() + " has finished executing.");
   }

   public static void main(String[] args) {
       new ThreadExample();
       new ThreadExample();
       new ThreadExample();
   }
}
Chúng tôi tuần tự tạo và bắt đầu ba luồng: Thread-0, Thread-1, và Thread-2. Thread-0bắt đầu trước và ngay lập tức nhường chỗ cho những người khác. Sau đó Thread-1được bắt đầu và cũng mang lại lợi nhuận. Sau đó Thread-2là bắt đầu, mà cũng mang lại. Chúng tôi không còn chủ đề nào nữa và sau khi Thread-2giành được vị trí cuối cùng, bộ lập lịch chủ đề nói, 'Hừm, không còn chủ đề mới nào nữa. Chúng ta có ai trong hàng đợi? Ai nhường vị trí của nó trước Thread-2? Có vẻ như nó đã được Thread-1. Được rồi, điều đó có nghĩa là chúng ta sẽ để nó chạy'. Thread-1hoàn thành công việc của nó và sau đó bộ lập lịch luồng tiếp tục điều phối: 'Được rồi, Thread-1đã hoàn thành. Chúng ta có ai khác trong hàng đợi không?'. Chủ đề-0 nằm trong hàng đợi: nó nhường chỗ ngay trướcThread-1. Bây giờ nó đến lượt của nó và chạy đến khi hoàn thành. Sau đó, bộ lập lịch kết thúc việc điều phối các chủ đề: 'Được rồi, Thread-2bạn đã nhường quyền cho các chủ đề khác và giờ chúng đã hoàn tất. Bạn là người cuối cùng đầu hàng, vì vậy bây giờ đến lượt bạn'. Sau đó Thread-2chạy đến khi hoàn thành. Đầu ra của giao diện điều khiển sẽ giống như sau: Chủ đề-0 nhường vị trí của nó cho người khác Chủ đề-1 nhường vị trí của mình cho người khác Chủ đề-2 nhường vị trí của mình cho người khác Chủ đề-1 đã thực hiện xong. Thread-0 đã thực hiện xong. Chủ đề-2 đã thực hiện xong. Tất nhiên, bộ lập lịch luồng có thể bắt đầu các luồng theo thứ tự khác (ví dụ: 2-1-0 thay vì 0-1-2), nhưng nguyên tắc vẫn giữ nguyên.

Quy tắc xảy ra trước

Điều cuối cùng chúng ta sẽ đề cập hôm nay là khái niệm ' xảy ra trước đó '. Như bạn đã biết, trong Java, bộ lập lịch luồng thực hiện phần lớn công việc liên quan đến việc phân bổ thời gian và tài nguyên cho các luồng để thực hiện nhiệm vụ của chúng. Bạn cũng đã nhiều lần thấy cách các luồng được thực thi theo một thứ tự ngẫu nhiên thường không thể dự đoán được. Và nói chung, sau khi lập trình 'tuần tự' mà chúng ta đã làm trước đây, lập trình đa luồng trông giống như một thứ gì đó ngẫu nhiên. Bạn đã tin rằng bạn có thể sử dụng nhiều phương pháp để kiểm soát luồng của một chương trình đa luồng. Nhưng đa luồng trong Java có một trụ cột nữa — quy tắc 4 ' xảy ra trước '. Hiểu các quy tắc này khá đơn giản. Hãy tưởng tượng rằng chúng ta có hai chủ đề - AB. Mỗi luồng này có thể thực hiện các thao tác 12. Trong mỗi quy tắc, khi chúng tôi nói ' A xảy ra trước B ', chúng tôi muốn nói rằng tất cả các thay đổi do luồng thực Ahiện trước khi thao tác 1và các thay đổi do thao tác này sẽ hiển thị đối với luồng Bkhi thao tác 2được thực hiện và sau đó. Mỗi quy tắc đảm bảo rằng khi bạn viết một chương trình đa luồng, một số sự kiện nhất định sẽ xảy ra trước các sự kiện khác 100% thời gian và tại thời điểm hoạt động, 2luồng Bsẽ luôn nhận thức được những thay đổi mà luồng Ađã thực hiện trong quá trình hoạt động 1. Hãy xem xét chúng.

Quy tắc 1.

Việc giải phóng một mutex xảy ra trước khi một luồng khác thu được cùng một màn hình. Tôi nghĩ rằng bạn hiểu tất cả mọi thứ ở đây. Nếu một mutex của một đối tượng hoặc lớp được một luồng thu được. Ví dụ: bởi luồng A, một luồng khác (luồng B) không thể có được nó cùng một lúc. Nó phải đợi cho đến khi mutex được giải phóng.

Quy tắc 2.

Phương Thread.start()pháp xảy ra trước Thread.run() . Một lần nữa, không có gì khó khăn ở đây. Bạn đã biết rằng để bắt đầu chạy mã bên trong run()phương thức, bạn phải gọi start()phương thức đó trên luồng. Cụ thể, phương thức bắt đầu, không phải run()chính phương thức đó! Quy tắc này đảm bảo rằng các giá trị của tất cả các biến được đặt trước khi Thread.start()được gọi sẽ hiển thị bên trong run()phương thức sau khi bắt đầu.

Quy tắc 3.

Sự kết thúc của run()phương thức xảy ra trước khi phương thức trả về join(). Hãy quay lại hai chủ đề của chúng ta: AB. Chúng tôi gọi join()phương thức để luồng Bđược đảm bảo chờ hoàn thành luồng Atrước khi nó thực hiện công việc của mình. Điều này có nghĩa là run()phương thức của đối tượng A được đảm bảo chạy đến cùng. Và tất cả các thay đổi đối với dữ liệu xảy ra trong run()phương thức của luồng Ađều được đảm bảo một trăm phần trăm sẽ hiển thị trong luồng Bsau khi thực hiện xong, đợi luồng Ahoàn thành công việc của nó để nó có thể bắt đầu công việc của chính nó.

Quy tắc 4.

Ghi vào một volatilebiến xảy ra trước khi đọc từ cùng một biến đó. Khi chúng tôi sử dụng volatiletừ khóa, chúng tôi thực sự luôn nhận được giá trị hiện tại. Ngay cả với longhoặc double(chúng tôi đã nói trước đó về các vấn đề có thể xảy ra ở đây). Như bạn đã hiểu, những thay đổi được thực hiện trên một số luồng không phải lúc nào cũng hiển thị đối với các luồng khác. Nhưng, tất nhiên, có những tình huống rất thường xảy ra khi hành vi đó không phù hợp với chúng ta. Giả sử rằng chúng ta gán một giá trị cho một biến trên luồng A:

int z;

….

z = 555;
Nếu Bluồng của chúng tôi hiển thị giá trị của zbiến trên bảng điều khiển, thì nó có thể dễ dàng hiển thị 0 vì nó không biết về giá trị được gán. Nhưng Quy tắc 4 đảm bảo rằng nếu chúng ta khai báo zbiến là volatile, thì những thay đổi đối với giá trị của nó trên một luồng sẽ luôn hiển thị trên một luồng khác. Nếu chúng ta thêm từ volatilevào mã trước đó...

volatile int z;

….

z = 555;
... thì chúng tôi ngăn chặn tình huống mà luồng Bcó thể hiển thị 0. Việc ghi vào volatilecác biến xảy ra trước khi đọc từ chúng.
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION