JavaRush /Blog Java /Random-VI /Quản lý biến động
lexmirnov
Mức độ
Москва

Quản lý biến động

Xuất bản trong nhóm

Hướng dẫn sử dụng các biến dễ bay hơi

Bởi Brian Goetz Ngày 19 tháng 6 năm 2007 Bản gốc: Quản lý tính biến động Các biến có thể biến động trong Java có thể được gọi là "ánh sáng đồng bộ hóa"; Chúng yêu cầu sử dụng ít mã hơn các khối được đồng bộ hóa, thường chạy nhanh hơn nhưng chỉ có thể thực hiện một phần nhỏ những gì khối được đồng bộ hóa thực hiện. Bài viết này trình bày một số mẫu để sử dụng biến động hiệu quả—và một số cảnh báo về những chỗ không nên sử dụng nó. Khóa có hai tính năng chính: loại trừ lẫn nhau (mutex) và khả năng hiển thị. Loại trừ lẫn nhau có nghĩa là một khóa chỉ có thể được giữ bởi một luồng tại một thời điểm và thuộc tính này có thể được sử dụng để triển khai các giao thức kiểm soát truy cập cho các tài nguyên được chia sẻ để mỗi lần chỉ có một luồng sử dụng chúng. Khả năng hiển thị là một vấn đề tế nhị hơn, mục đích của nó là đảm bảo rằng những thay đổi được thực hiện đối với tài nguyên công cộng trước khi khóa được giải phóng sẽ hiển thị đối với luồng tiếp theo đảm nhận khóa đó. Nếu đồng bộ hóa không đảm bảo khả năng hiển thị, các luồng có thể nhận được giá trị cũ hoặc không chính xác cho các biến công khai, điều này sẽ dẫn đến một số vấn đề nghiêm trọng.
Biến động
Các biến dễ bay hơi có các thuộc tính hiển thị của các biến được đồng bộ hóa, nhưng thiếu tính nguyên tử của chúng. Điều này có nghĩa là các luồng sẽ tự động sử dụng các giá trị mới nhất của các biến dễ bay hơi. Chúng có thể được sử dụng để đảm bảo an toàn cho luồng , nhưng trong một số trường hợp rất hạn chế: những trường hợp không đưa ra mối quan hệ giữa nhiều biến hoặc giữa giá trị hiện tại và tương lai của một biến. Do đó, chỉ biến động thôi là không đủ để triển khai bộ đếm, mutex hoặc bất kỳ lớp nào có các phần bất biến được liên kết với nhiều biến (ví dụ: "bắt đầu <= kết thúc"). Bạn có thể chọn khóa dễ thay đổi vì một trong hai lý do chính: tính đơn giản hoặc khả năng mở rộng. Một số cấu trúc ngôn ngữ sẽ dễ viết hơn dưới dạng mã chương trình và sau này dễ đọc và hiểu hơn khi chúng sử dụng các biến dễ thay đổi thay vì khóa. Ngoài ra, không giống như khóa, chúng không thể chặn một luồng và do đó ít gặp phải các vấn đề về khả năng mở rộng hơn. Trong trường hợp có nhiều lượt đọc hơn ghi, các biến dễ thay đổi có thể mang lại lợi ích về hiệu suất so với khóa.
Điều kiện sử dụng đúng chất dễ bay hơi
Bạn có thể thay thế ổ khóa bằng ổ khóa dễ bay hơi trong một số trường hợp hạn chế. Để đảm bảo an toàn cho luồng, cả hai tiêu chí phải được đáp ứng:
  1. Những gì được ghi vào một biến không phụ thuộc vào giá trị hiện tại của nó.
  2. Biến không tham gia bất biến với các biến khác.
Nói một cách đơn giản, những điều kiện này có nghĩa là các giá trị hợp lệ có thể được ghi vào một biến dễ bay hơi độc lập với bất kỳ trạng thái nào khác của chương trình, bao gồm cả trạng thái hiện tại của biến. Điều kiện đầu tiên loại trừ việc sử dụng các biến dễ bay hơi làm bộ đếm an toàn cho luồng. Mặc dù phần tăng dần (x++) trông giống như một thao tác đơn lẻ, nhưng thực tế nó là một chuỗi toàn bộ các thao tác đọc-sửa-ghi phải được thực hiện một cách nguyên tử, điều mà biến động không cung cấp. Một hoạt động hợp lệ sẽ yêu cầu giá trị của x giữ nguyên trong suốt hoạt động, điều này không thể đạt được bằng cách sử dụng biến động. (Tuy nhiên, nếu bạn có thể đảm bảo rằng giá trị chỉ được ghi từ một luồng thì có thể bỏ qua điều kiện đầu tiên.) Trong hầu hết các trường hợp, điều kiện thứ nhất hoặc thứ hai sẽ bị vi phạm, khiến các biến dễ bay hơi trở thành phương pháp ít được sử dụng hơn để đạt được sự an toàn của luồng so với các biến được đồng bộ hóa. Liệt kê 1 cho thấy một lớp không an toàn luồng với một dãy số. Nó chứa một bất biến - giới hạn dưới luôn nhỏ hơn hoặc bằng giới hạn trên. @NotThreadSafe public class NumberRange { private int lower, upper; public int getLower() { return lower; } public int getUpper() { return upper; } public void setLower(int value) { if (value > upper) throw new IllegalArgumentException(...); lower = value; } public void setUpper(int value) { if (value < lower) throw new IllegalArgumentException(...); upper = value; } } Vì các biến trạng thái phạm vi bị giới hạn theo cách này, nên sẽ không đủ để làm cho các trường trên và dưới biến động để đảm bảo lớp an toàn cho luồng; vẫn cần phải đồng bộ hóa. Nếu không, sớm hay muộn bạn cũng sẽ gặp xui xẻo và hai luồng thực hiện setLower() và setUpper() với các giá trị không phù hợp có thể dẫn phạm vi đến trạng thái không nhất quán. Ví dụ: nếu giá trị ban đầu là (0, 5), luồng A gọi setLower(4) và đồng thời luồng B gọi setUpper(3), thì các thao tác xen kẽ này sẽ dẫn đến lỗi, mặc dù cả hai đều vượt qua kiểm tra điều đó được cho là để bảo vệ sự bất biến. Kết quả là phạm vi sẽ là (4, 3) - giá trị không chính xác. Chúng ta cần biến setLower() và setUpper() thành nguyên tử cho các hoạt động phạm vi khác - và việc tạo các trường dễ bay hơi sẽ không làm được điều đó.
Cân nhắc về hiệu suất
Lý do đầu tiên để sử dụng dễ bay hơi là sự đơn giản. Trong một số trường hợp, việc sử dụng một biến như vậy đơn giản là dễ dàng hơn việc sử dụng khóa liên kết với nó. Lý do thứ hai là hiệu suất, đôi khi biến động sẽ hoạt động nhanh hơn khóa. Rất khó để đưa ra các tuyên bố chính xác, toàn diện như "X luôn nhanh hơn Y", đặc biệt khi nói đến các hoạt động nội bộ của Máy ảo Java. (Ví dụ: JVM có thể giải phóng hoàn toàn khóa trong một số trường hợp, gây khó khăn cho việc thảo luận về chi phí biến động so với đồng bộ hóa một cách trừu tượng). Tuy nhiên, trên hầu hết các kiến ​​trúc bộ xử lý hiện đại, chi phí đọc biến động không khác nhiều so với chi phí đọc các biến thông thường. Chi phí ghi biến động cao hơn đáng kể so với ghi biến thông thường do cần có hàng rào bộ nhớ để đảm bảo khả năng hiển thị, nhưng nhìn chung rẻ hơn so với cài đặt khóa.
Các mẫu để sử dụng hợp lý chất dễ bay hơi
Nhiều chuyên gia về đồng thời có xu hướng tránh sử dụng hoàn toàn các biến dễ bay hơi vì chúng khó sử dụng chính xác hơn so với khóa. Tuy nhiên, có một số mẫu được xác định rõ ràng mà nếu tuân thủ cẩn thận thì có thể được sử dụng một cách an toàn trong nhiều tình huống khác nhau. Luôn tôn trọng các giới hạn của tính dễ bay hơi - chỉ sử dụng các chất dễ bay hơi độc lập với bất kỳ thứ gì khác trong chương trình và điều này sẽ giúp bạn không đi vào vùng nguy hiểm với các mẫu này.
Mẫu số 1: Cờ trạng thái
Có lẽ việc sử dụng chính tắc các biến có thể thay đổi là các cờ trạng thái boolean đơn giản cho biết rằng một sự kiện quan trọng trong vòng đời một lần đã xảy ra, chẳng hạn như hoàn thành khởi tạo hoặc yêu cầu tắt máy. Nhiều ứng dụng bao gồm một cấu trúc điều khiển có dạng: "cho đến khi chúng ta sẵn sàng tắt, hãy tiếp tục chạy" như trong Liệt kê 2: Có khả năng là volatile boolean shutdownRequested; ... public void shutdown() { shutdownRequested = true; } public void doWork() { while (!shutdownRequested) { // do stuff } } phương thức tắt máy() sẽ được gọi từ một nơi nào đó bên ngoài vòng lặp - trên một luồng khác - vì vậy cần phải đồng bộ hóa để đảm bảo khả năng hiển thị biến chính xác được yêu cầu tắt máy. (Nó có thể được gọi từ trình nghe JMX, trình nghe hành động trong chuỗi sự kiện GUI, qua RMI, qua dịch vụ web, v.v.). Tuy nhiên, một vòng lặp có các khối được đồng bộ hóa sẽ cồng kềnh hơn nhiều so với một vòng lặp có cờ trạng thái dễ bay hơi như trong Liệt kê 2. Bởi vì độ biến động làm cho việc viết mã dễ dàng hơn và cờ trạng thái không phụ thuộc vào bất kỳ trạng thái chương trình nào khác, đây là một ví dụ về một sử dụng tốt dễ bay hơi. Đặc điểm của các cờ trạng thái như vậy là thường chỉ có một trạng thái chuyển tiếp; cờ yêu cầu tắt máy chuyển từ sai sang đúng và sau đó chương trình sẽ tắt. Mẫu này có thể được mở rộng thành các cờ trạng thái có thể thay đổi qua lại, nhưng chỉ khi chu kỳ chuyển đổi (từ sai sang đúng sang sai) xảy ra mà không có sự can thiệp từ bên ngoài. Mặt khác, một số loại cơ chế chuyển tiếp nguyên tử, chẳng hạn như các biến nguyên tử, là cần thiết.
Mẫu số 2: Xuất bản an toàn một lần
Các lỗi hiển thị có thể xảy ra khi không có sự đồng bộ hóa có thể trở thành một vấn đề khó khăn hơn khi viết các tham chiếu đối tượng thay vì các giá trị nguyên thủy. Nếu không đồng bộ hóa, bạn có thể thấy giá trị hiện tại của một tham chiếu đối tượng được viết bởi một luồng khác và vẫn thấy các giá trị trạng thái cũ cho đối tượng đó. (Mối đe dọa này là gốc rễ của vấn đề với khóa kiểm tra kép khét tiếng, trong đó tham chiếu đối tượng được đọc mà không đồng bộ hóa và bạn có nguy cơ nhìn thấy tham chiếu thực tế nhưng lại nhận được một đối tượng được xây dựng một phần thông qua nó.) object là tạo một tham chiếu đến một đối tượng dễ bay hơi. Liệt kê 3 cho thấy một ví dụ trong đó, trong khi khởi động, một luồng nền sẽ tải một số dữ liệu từ cơ sở dữ liệu. Mã khác có thể thử sử dụng dữ liệu này sẽ kiểm tra xem nó đã được xuất bản chưa trước khi thử sử dụng nó. public class BackgroundFloobleLoader { public volatile Flooble theFlooble; public void initInBackground() { // делаем много всякого theFlooble = new Flooble(); // единственная запись в theFlooble } } public class SomeOtherClass { public void doWork() { while (true) { // чё-то там делаем... // используем theFolooble, но только если она готова if (floobleLoader.theFlooble != null) doSomething(floobleLoader.theFlooble); } } } Nếu tham chiếu đến theFlooble không thay đổi, mã trong doWork() sẽ có nguy cơ nhìn thấy Flooble được xây dựng một phần khi cố gắng tham chiếu theFlooble. Yêu cầu chính đối với mẫu này là đối tượng được xuất bản phải an toàn theo luồng hoặc không thể thay đổi một cách hiệu quả (bất biến một cách hiệu quả có nghĩa là trạng thái của nó không bao giờ thay đổi sau khi được xuất bản). Liên kết dễ bay hơi có thể đảm bảo rằng một đối tượng được hiển thị ở dạng được xuất bản, nhưng nếu trạng thái của đối tượng thay đổi sau khi xuất bản thì cần phải đồng bộ hóa bổ sung.
Mẫu số 3: Quan sát độc lập
Một ví dụ đơn giản khác về việc sử dụng an toàn chất dễ bay hơi là khi các quan sát được “xuất bản” định kỳ để sử dụng trong một chương trình. Ví dụ, có một cảm biến môi trường phát hiện nhiệt độ hiện tại. Luồng nền có thể đọc cảm biến này cứ sau vài giây và cập nhật một biến dễ bay hơi chứa nhiệt độ hiện tại. Sau đó, các luồng khác có thể đọc biến này và biết rằng giá trị trong đó luôn được cập nhật. Một cách sử dụng khác của mẫu này là thu thập số liệu thống kê về chương trình. Liệt kê 4 cho thấy cơ chế xác thực có thể nhớ tên của người dùng đăng nhập lần cuối như thế nào. Tham chiếu LastUser sẽ được sử dụng lại để đăng giá trị cho phần còn lại của chương trình sử dụng. public class UserManager { public volatile String lastUser; public boolean authenticate(String user, String password) { boolean valid = passwordIsValid(user, password); if (valid) { User u = new User(); activeUsers.add(u); lastUser = user; } return valid; } } Mẫu này mở rộng trên mẫu trước đó; giá trị được xuất bản để sử dụng ở nơi khác trong chương trình, nhưng việc xuất bản không phải là sự kiện diễn ra một lần mà là một loạt các sự kiện độc lập. Mẫu này yêu cầu giá trị được xuất bản phải bất biến một cách hiệu quả - trạng thái của nó không thay đổi sau khi xuất bản. Mã sử ​​dụng giá trị phải lưu ý rằng nó có thể thay đổi bất kỳ lúc nào.
Mẫu số 4: mẫu “đậu dễ ​​bay hơi”
Mẫu “đậu dễ ​​bay hơi” có thể áp dụng trong các khung công tác sử dụng JavaBeans làm “cấu trúc được tôn vinh”. Mẫu “đậu dễ ​​bay hơi” sử dụng JavaBean làm vùng chứa cho một nhóm thuộc tính độc lập với getters và/hoặc setters. Lý do cơ bản cho mẫu "đậu dễ ​​bay hơi" là nhiều khung cung cấp các thùng chứa cho các chủ sở hữu dữ liệu có thể thay đổi (chẳng hạn như HttpSession), nhưng các đối tượng được đặt trong các thùng chứa này phải an toàn theo luồng. Trong mẫu đậu dễ ​​bay hơi, tất cả các phần tử dữ liệu JavaBean đều dễ bay hơi và các getter và setters phải tầm thường - chúng không được chứa bất kỳ logic nào ngoài việc nhận hoặc thiết lập thuộc tính tương ứng. Ngoài ra, đối với các thành viên dữ liệu là tham chiếu đối tượng, các đối tượng nói trên thực tế phải bất biến. (Điều này không cho phép các trường tham chiếu mảng, vì khi một tham chiếu mảng được khai báo là không ổn định, chỉ tham chiếu đó chứ không phải bản thân các phần tử mới có thuộc tính dễ bay hơi.) Giống như bất kỳ biến dễ bay hơi nào, không thể có bất biến hoặc hạn chế nào liên quan đến các thuộc tính của JavaBeans . Một ví dụ về JavaBean được viết bằng mẫu “volatile Bean” được hiển thị trong Liệt kê 5: @ThreadSafe public class Person { private volatile String firstName; private volatile String lastName; private volatile int age; public String getFirstName() { return firstName; } public String getLastName() { return lastName; } public int getAge() { return age; } public void setFirstName(String firstName) { this.firstName = firstName; } public void setLastName(String lastName) { this.lastName = lastName; } public void setAge(int age) { this.age = age; } }
Các mô hình biến động phức tạp hơn
Các mẫu trong phần trước bao gồm hầu hết các trường hợp phổ biến trong đó việc sử dụng biến động là hợp lý và rõ ràng. Phần này xem xét một mẫu phức tạp hơn trong đó tính dễ bay hơi có thể mang lại lợi ích về hiệu suất hoặc khả năng mở rộng. Các mẫu dễ bay hơi cao cấp hơn có thể cực kỳ dễ vỡ. Điều quan trọng là các giả định của bạn phải được ghi lại cẩn thận và các mẫu này được gói gọn chặt chẽ, bởi vì ngay cả những thay đổi nhỏ nhất cũng có thể phá vỡ mã của bạn! Ngoài ra, vì lý do chính dẫn đến các trường hợp sử dụng không ổn định phức tạp hơn là hiệu suất, hãy đảm bảo rằng bạn thực sự có nhu cầu rõ ràng về mức tăng hiệu suất dự định trước khi sử dụng chúng. Những mẫu này là sự thỏa hiệp hy sinh khả năng đọc hoặc dễ bảo trì để có thể đạt được hiệu suất - nếu bạn không cần cải thiện hiệu suất (hoặc không thể chứng minh rằng bạn cần nó bằng một chương trình đo lường nghiêm ngặt), thì đó có thể là một thỏa thuận tồi vì điều đó bạn đang từ bỏ một thứ có giá trị và nhận lại một thứ ít giá trị hơn.
Mẫu số 5: Khóa đọc ghi giá rẻ
Đến bây giờ bạn nên biết rõ rằng độ biến động quá yếu để thực hiện bộ đếm. Vì ++x về cơ bản là rút gọn ba thao tác (đọc, nối thêm, lưu trữ), nếu có sự cố xảy ra, bạn sẽ mất giá trị cập nhật nếu nhiều luồng cố gắng tăng bộ đếm dễ bay hơi cùng một lúc. Tuy nhiên, nếu số lần đọc nhiều hơn đáng kể so với số thay đổi, bạn có thể kết hợp các biến khóa nội tại và các biến dễ thay đổi để giảm chi phí chung cho đường dẫn mã. Liệt kê 6 cho thấy một bộ đếm an toàn theo luồng sử dụng được đồng bộ hóa để đảm bảo rằng thao tác tăng dần là nguyên tử và sử dụng tính dễ bay hơi để đảm bảo rằng kết quả hiện tại được hiển thị. Nếu cập nhật không thường xuyên, phương pháp này có thể cải thiện hiệu suất vì chi phí đọc được giới hạn ở các lần đọc không ổn định, thường rẻ hơn so với việc mua một khóa không xung đột. @ThreadSafe public class CheesyCounter { // Employs the cheap read-write lock trick // All mutative operations MUST be done with the 'this' lock held @GuardedBy("this") private volatile int value; public int getValue() { return value; } public synchronized int increment() { return value++; } } Lý do phương pháp này được gọi là "khóa đọc-ghi giá rẻ" là do bạn sử dụng các cơ chế định thời gian khác nhau để đọc và ghi. Bởi vì các thao tác ghi trong trường hợp này vi phạm điều kiện đầu tiên của việc sử dụng biến động, bạn không thể sử dụng biến động để triển khai bộ đếm một cách an toàn - bạn phải sử dụng khóa. Tuy nhiên, bạn có thể sử dụng biến động để hiển thị giá trị hiện tại khi đọc, do đó, bạn sử dụng khóa cho tất cả các thao tác sửa đổi và biến động cho các thao tác chỉ đọc. Nếu khóa chỉ cho phép một luồng tại một thời điểm truy cập vào một giá trị, thì các lần đọc dễ bay hơi cho phép nhiều hơn một giá trị, vì vậy khi bạn sử dụng tính dễ bay hơi để bảo vệ việc đọc, bạn sẽ nhận được mức trao đổi cao hơn so với khi bạn sử dụng khóa trên tất cả các mã: và đọc và ghi lại. Tuy nhiên, hãy lưu ý đến sự mong manh của mẫu này: với hai cơ chế đồng bộ hóa cạnh tranh, nó có thể trở nên rất phức tạp nếu bạn vượt ra ngoài ứng dụng cơ bản nhất của mẫu này.
Bản tóm tắt
Biến dễ bay hơi là một hình thức đồng bộ hóa đơn giản hơn nhưng yếu hơn so với khóa, trong một số trường hợp mang lại hiệu suất hoặc khả năng mở rộng tốt hơn so với khóa nội tại. Nếu bạn đáp ứng các điều kiện để sử dụng an toàn biến động - một biến thực sự độc lập với cả các biến khác và các giá trị trước đó của chính nó - đôi khi bạn có thể đơn giản hóa mã bằng cách thay thế đồng bộ hóa bằng biến động. Tuy nhiên, mã sử dụng tính dễ thay đổi thường dễ hỏng hơn mã sử dụng khóa. Các mẫu được đề xuất ở đây bao gồm các trường hợp phổ biến nhất trong đó sự biến động là một giải pháp thay thế hợp lý cho việc đồng bộ hóa. Bằng cách làm theo các mẫu này - và chú ý không đẩy chúng vượt quá giới hạn của chính chúng - bạn có thể sử dụng biến động một cách an toàn trong trường hợp chúng mang lại lợi ích.
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION