CFSDN nhấn mạnh vào giá trị tạo ra nguồn mở và chúng tôi cam kết xây dựng nền tảng chia sẻ tài nguyên để mọi nhân viên CNTT có thể tìm thấy thế giới tuyệt vời của bạn tại đây.
Bài viết trên blog CFSDN này nói về cách sử dụng và ý tưởng triển khai chính của StampedLock. Nó được tác giả sưu tầm và biên soạn. Nếu bạn quan tâm đến bài viết này, hãy nhớ like nhé.

Lời nói đầu.
Trong phát triển đa luồng, để kiểm soát việc đồng bộ hóa luồng, các từ khóa được sử dụng phổ biến nhất là từ khóa được đồng bộ hóa và khóa đăng nhập lại. Trong JDK8, một loại vũ khí mới StampedLock đã được giới thiệu. Đây là gì? Từ tiếng Anh Stamp có nghĩa là dấu bưu điện. Vậy nó có ý nghĩa gì ở đây, ngốc à, hãy nhìn vào phần phân tích bên dưới.
Đối mặt với vấn đề quản lý tài nguyên phần quan trọng, nhìn chung có hai nhóm ý tưởng:
Đầu tiên là sử dụng chiến lược bi quan như thế này: Mỗi khi tôi truy cập vào một biến chung trong phần quan trọng, sẽ luôn có người xung đột với tôi. Vì vậy, mỗi khi tôi truy cập vào nó, trước tiên tôi phải khóa toàn bộ đối tượng và sau đó. mở khóa nó sau khi hoàn thành việc truy cập.
Ngược lại, người lạc quan tin rằng mặc dù các biến được chia sẻ trong phần quan trọng sẽ xung đột, nhưng xung đột sẽ là một sự kiện có xác suất nhỏ, vì vậy, tôi có thể truy cập nó trước và sau đó chờ đợi. sử dụng Sau khi dữ liệu kết thúc và không có ai xung đột, thì thao tác của tôi thành công; nếu tôi thấy ai đó xung đột sau khi sử dụng xong thì tôi sẽ thử lại hoặc chuyển sang chiến lược bi quan.
。
Từ đây không khó để nhận thấy rằng việc khóa lại và đồng bộ hóa là một chiến lược bi quan điển hình. Nếu bạn thông minh, chắc hẳn bạn đã đoán rằng StampedLock cung cấp một công cụ khóa lạc quan, vì vậy đây là một công cụ bổ sung quan trọng cho việc khóa lại.
Cách sử dụng cơ bản của StampedLock
。
Một ví dụ rất hay được cung cấp trong tài liệu StampedLock, cho phép chúng ta hiểu nhanh cách sử dụng StampedLock. Hãy để tôi xem ví dụ này. Mô tả về nó được viết trong phần bình luận.
。
Đây là một lời giải thích khác về ý nghĩa của phương thức valid(). Chữ ký hàm trông như thế này:
- công cộng boolean xác thực (dấu dài)
Tham số được chấp nhận của nó là dấu bưu điện được trả về bởi thao tác khóa cuối cùng. Nếu khóa này chưa được áp dụng cho khóa ghi trước khi gọi xác thực(), nó sẽ trả về true. Điều này cũng có nghĩa là dữ liệu được chia sẻ được bảo vệ bởi khóa chưa được sửa đổi. , vì vậy các thao tác Đọc trước đó chắc chắn có thể đảm bảo tính toàn vẹn và nhất quán của dữ liệu.
Ngược lại, nếu khóa đã được áp dụng thành công cho khóa ghi trước khi xác thực(), điều đó có nghĩa là các hoạt động đọc và ghi dữ liệu trước đó xung đột và chương trình cần thử lại hoặc nâng cấp lên khóa bi quan.
So sánh với khóa reentrant.
Từ ví dụ trên, không khó để thấy rằng xét về độ phức tạp của lập trình, StampedLock thực sự phức tạp hơn nhiều so với khóa đăng lại và mã không còn ngắn gọn như trước.
Vậy tại sao chúng ta vẫn sử dụng nó?
。
Lý do cơ bản nhất là để cải thiện hiệu suất! Nói chung, hiệu suất của loại khóa lạc quan này nhanh hơn nhiều lần so với khóa quay lại thông thường và khi số lượng luồng tiếp tục tăng, khoảng cách hiệu suất sẽ ngày càng lớn hơn.
Nói tóm lại, trong một số lượng lớn các trường hợp đồng thời, hiệu suất của StampedLock sẽ vượt qua các khóa đăng nhập lại và các khóa đọc-ghi.
Nhưng xét cho cùng, trên đời không có gì là hoàn hảo, và StampedLock cũng không phải là toàn năng. Những khuyết điểm của nó như sau:
Việc mã hóa sẽ rắc rối hơn nếu sử dụng cách đọc lạc quan, các tình huống xung đột phải được chính ứng dụng xử lý.
Nó không được đăng lại. Nếu bạn vô tình gọi nó hai lần trong cùng một chủ đề, thế giới của bạn sẽ sạch sẽ. . . . .
Nó không hỗ trợ cơ chế chờ/thông báo.
Nếu không có điểm nào trong 3 điểm trên là vấn đề với bạn thì tôi tin rằng StampedLock sẽ là lựa chọn đầu tiên của bạn.
Cấu trúc dữ liệu nội bộ
。
Để giúp bạn hiểu rõ hơn về StampedLock, đây là phần giới thiệu ngắn gọn về cách triển khai nội bộ và cấu trúc dữ liệu của nó.
Trong StampedLock, có một hàng đợi lưu trữ các luồng đang chờ trên khóa. Hàng đợi là một danh sách được liên kết và phần tử trong danh sách được liên kết là một đối tượng có tên WNode:
Khi có một số luồng đang chờ trong hàng đợi, toàn bộ hàng đợi có thể trông như thế này:
Ngoài hàng đợi này, một trường đặc biệt quan trọng khác trong StampedLock là trạng thái dài, là số nguyên 64 bit sử dụng nó rất khéo léo.
Giá trị ban đầu của trạng thái là
- riêng tư tĩnh cuối cùng số nguyên LG_READERS = 7;
- riêng tư tĩnh WBIT dài cuối cùng = 1L << LG_READERS;
- riêng tư tĩnh cuối cùng dài ORIGIN = WBIT << 1;
Đó là...0001 0000 0000 (có quá nhiều số 0 ở phía trước nên tôi sẽ không viết chúng ra, hãy bù thành 64~). Tại sao 0 không được sử dụng làm giá trị ban đầu ở đây? nghĩa là để tránh xung đột, tôi chọn số khác 0.
Nếu có khóa ghi bị chiếm dụng thì đặt bit 7 thành 1...0001 1000 0000, nghĩa là thêm WBIT.
Mỗi khi khóa ghi được giải phóng, 1 byte sẽ được thêm vào, nhưng thay vì thêm trạng thái trực tiếp, byte cuối cùng sẽ bị xóa và chỉ 7 byte đầu tiên được sử dụng để thống kê. Do đó, sau khi nhả khóa ghi, trạng thái sẽ trở thành:...0010 0000 0000. Sau khi thêm một khóa khác, nó sẽ trở thành:...0010 1000 0000, v.v.
Tại sao chúng ta cần ghi lại số lần nhả khóa ghi ở đây?
。
Điều này là do việc đánh giá trạng thái của toàn bộ trạng thái dựa trên các hoạt động CAS. Các hoạt động CAS thông thường có thể gặp phải sự cố ABA. Nếu số lần không được ghi lại thì khi khóa ghi được giải phóng, áp dụng và sau đó được giải phóng, chúng tôi sẽ không thể xác định liệu dữ liệu đã được ghi hay chưa. Số lượng bản phát hành được ghi lại ở đây, vì vậy khi xảy ra "phát hành-> ứng dụng-> phát hành", hoạt động CAS có thể kiểm tra các thay đổi dữ liệu và xác định rằng hoạt động ghi đã xảy ra. Là một khóa lạc quan, nó có thể được đánh giá chính xác. xung đột đã xảy ra và phần việc còn lại được giao cho ứng dụng giải quyết xung đột. Vì vậy, số lần nhả khóa được ghi lại ở đây để theo dõi xung đột luồng một cách chính xác.
7 bit của byte trạng thái còn lại được sử dụng để ghi lại số lượng luồng đọc khóa. Vì chỉ có 7 bit nên chỉ có thể ghi được 126 đáng thương. Hãy xem RFULL trong mã bên dưới, đó là số lượng đầy đủ. chủ đề đọc được tải. Phải làm gì nếu vượt quá giới hạn? Phần vượt quá được ghi lại trong trường readerOverflow.
- riêng tư tĩnh WBIT dài cuối cùng = 1L << LG_READERS;
- riêng tư tĩnh RBITS dài cuối cùng = WBIT - 1L;
- riêng tư tĩnh cuối cùng dài RFULL = RBITS - 1L;
- riêng tư tạm thời số nguyên người đọc tràn;
Tóm lại, cấu trúc của biến trạng thái như sau:
。
Viết ứng dụng khóa và phát hành
。
Sau khi hiểu rõ cấu trúc dữ liệu bên trong của StampedLock, chúng ta cùng tìm hiểu ứng dụng và cách giải phóng các khóa ghi nhé!
- công cộng dài writeLock() {
- dài s, Kế tiếp;
- trở lại ((((s = state) & ABITS) == 0L && // Liệu khóa đọc-ghi có bị chiếm không, nếu không, hãy đặt dấu khóa ghi
- U.compareAndSwapLong(này, STATE, s, Kế tiếp = s + WBIT)) ?
- // Nếu khóa ghi chiếm phạm vi thành côngKế tiếp, nếu thất bại, hãy nhập AcacquiWrite() để chiếm khóa.
- Kế tiếp : thu thập Viết(SAI, 0L));
- }
Nếu CAS không thiết lập được trạng thái, điều đó có nghĩa là ứng dụng khóa ghi không thành công. Lúc này, AcacquiWrite() sẽ được gọi để áp dụng hoặc chờ. AcacquiWrite() thường thực hiện những việc sau:
1. Tham gia nhóm.
- Nếu nút đầu bằng nút đuôi wtail == whead thì nghĩa là sắp đến lượt của tôi nên tôi sẽ quay và đợi, khi nào tôi tóm được thì sẽ xong.
- Nếu wtail==null, điều đó có nghĩa là hàng đợi chưa được khởi tạo, vì vậy hãy khởi tạo hàng đợi.
- Nếu có các nút chờ khác trong hàng đợi thì bạn chỉ có thể tham gia hàng đợi và chờ đợi.
2. Chặn và chờ đợi.
- Nếu nút đầu bằng nút trước (h = whead) == p thì nghĩa là sắp đến lượt của tôi và tôi sẽ tiếp tục quay và chờ chiến đấu.
- Nếu không, hãy đánh thức luồng đọc trong nút đầu
- Nếu khóa không thể được sử dụng trước thì park() luồng hiện tại
Nói một cách đơn giản, hàm AcacquiWrite() được sử dụng để cạnh tranh các khóa. Giá trị trả về của nó là dấu bưu điện biểu thị trạng thái khóa hiện tại. Đồng thời, để cải thiện hiệu suất của khóa, AcacquiWrite() sử dụng một số lượng lớn. quay thử lại. Do đó, mã này có vẻ hơi mơ hồ.
Việc giải phóng khóa ghi như sau. Tham số đến của unlockWrite() là dấu bưu điện thu được khi đăng ký khóa:
- công cộng void unlockWrite(dấu dài) {
- Mã h;
- //Kiểm tra xem trạng thái khóa có bình thường không
- nếu (trạng thái != tem || (tem & WBIT) == 0L)
- ném ngoại lệ IllegalMonitorStateException mới();
- // Đặt bit cờ ở trạng thái thành 0, điều này cũng đóng vai trò tăng số lần mở khóa.
- trạng thái = (tem += WBIT) == 0L ? NGUỒN GỐC : tem;
- // Nút đầu không trống, cố gắng đánh thức các luồng tiếp theo
- nếu ((h = whead) != vô giá trị && trạng thái h != 0)
- // Đánh thức (bỏ công việc) một chủ đề tiếp theo
- phát hành(h);
- }
Đọc ứng dụng khóa và phát hành
。
Mã để có được khóa đọc như sau:
- công cộng dài readLock() {
- s dài = trạng thái, Kế tiếp;
- // Nếu không có khóa ghi trong hàng đợi và số luồng đọc không vượt quá 126, khóa được lấy trực tiếp và số lượng luồng đọc tăng thêm 1
- trở lại ((whead == wtail && (s & ABITS) < RFULL &&
- U.compareAndSwapLong(này, STATE, s, Kế tiếp = s + RUNIT)) ?
- //Nếu cuộc thi thất bại, nhập AcacquiRead() để cạnh tranh hoặc chờ.
- Kế tiếp : thu thập Đọc(SAI, 0L));
- }
Việc triển khai AcacquiRead() khá phức tạp và có thể được chia đại khái thành các bước sau:
Nói tóm lại, nó đang quay, quay và quay lại. Thông qua việc quay liên tục, chúng tôi cố gắng tránh để sợi chỉ thực sự bị treo. Chỉ khi vòng quay không đủ tốt, sợi chỉ mới thực sự được phép chờ.
Sau đây là quá trình giải phóng khóa đọc:
。
Vấn đề đọc bi quan StampedLock chiếm CPU
。
StampedLock chắc chắn là một điều tốt, nhưng vì nó cực kỳ phức tạp nên một số vấn đề nhỏ chắc chắn sẽ xảy ra. Ví dụ sau đây minh họa vấn đề khóa bi quan StampedLock chiếm CPU một cách điên cuồng:
- công cộng lớp StampedLockTest {
- công cộng tĩnh void main(String[] args) ném InterruptedException {
- khóa StampedLock cuối cùng = khóa StampedLock mới();
- Luồng t1 = luồng mới(() -> {
- // Lấy khóa ghi
- khóa. writeLock();
- // Chương trình mô phỏng chặn và chờ các tài nguyên khác
- KhóaHỗ trợ.park();
- });
- t1.bắt đầu();
- // Đảm bảo rằng t1 có được khóa ghi
- Luồng.sleep(100);
- Luồng t2 = luồng mới(() -> {
- // Chặn trên khóa đọc bi quan
- khóa. readLock();
- });
- t2.bắt đầu();
- // Đảm bảo rằng t2 bị chặn trong khóa đọc
- Luồng.sleep(100);
- // Việc ngắt luồng t2 sẽ khiến CPU nơi đặt luồng t2 tăng vọt.
- t2.ngắt();
- t2.tham gia();
- }
- }
Trong đoạn mã trên, sau khi ngắt t2, mức sử dụng CPU của t2 sẽ là 100%. Lúc này, t2 bị chặn trên hàm readLock() Nói cách khác, sau khi bị gián đoạn, khóa đọc StampedLock có thể chiếm CPU. Lý do cho điều này là gì? Chắc hẳn kẻ ngu ngốc của cơ chế này đã nghĩ rằng điều này là do có quá nhiều vòng quay trong StampedLock. Vâng, suy đoán của bạn là đúng!
Lý do cụ thể như sau:
。
Nếu không có gián đoạn, luồng bị chặn trên readLock() sẽ vào park() và đợi sau vài lần quay. Khi nó vào park() và chờ, nó sẽ không chiếm CPU. Nhưng một đặc điểm của hàm park() là khi luồng bị gián đoạn, park() sẽ trả về ngay lập tức. Kết quả trả về không được tính và sẽ không đưa ra bất kỳ ngoại lệ nào cho bạn, điều này thật đáng xấu hổ. Ban đầu bạn muốn unpark() thread khi lock đã sẵn sàng, nhưng bây giờ lock không tốt, bạn trực tiếp ngắt nó và park() cũng quay lại, nhưng rốt cuộc lock không tốt nên bạn đã đi đến chính mình một lần nữa.
Sau khi quay xung quanh, nó lại quay lại chức năng park(), nhưng thật đáng buồn, cờ ngắt của luồng luôn mở và park() không thể chặn được nữa. Vì vậy, vòng quay tiếp theo lại bắt đầu, vòng quay không thể dừng lại. , vậy là CPU đã đầy.
Để giải quyết vấn đề này, về cơ bản nó cần phải ở bên trong StampedLock. Khi park() trả về, cần xác định dấu ngắt và thực hiện xử lý chính xác, chẳng hạn như thoát, ném ngoại lệ hoặc xóa bit ngắt, điều này có thể giải quyết được. vấn đề.
Nhưng thật không may, ít nhất là trong JDK8, không có cách xử lý như vậy. Vì vậy, vấn đề trên phát sinh, sau khi ngắt readLock(), CPU sẽ đầy. Mong mọi người chú ý.
viết ở cuối
。
Hôm nay, chúng tôi đã giới thiệu chi tiết hơn về cách sử dụng và các ý tưởng triển khai chính của StampedLock là một phần bổ sung quan trọng cho khóa đăng nhập lại và khóa đọc-ghi.
Nó cung cấp một chiến lược khóa lạc quan và là một cách triển khai khóa độc đáo. Tất nhiên, về độ khó lập trình, StampedLock sẽ cồng kềnh hơn một chút so với khóa đăng nhập lại và khóa đọc-ghi, nhưng nó sẽ mang lại những cải thiện hiệu suất theo cấp số nhân.
Dưới đây là một số gợi ý nhỏ cho bạn. Nếu số lượng luồng ứng dụng của chúng tôi có thể kiểm soát được, không quá nhiều và sự cạnh tranh không quá khốc liệt thì chúng tôi có thể trực tiếp sử dụng các khóa đồng bộ hóa, khóa lại và khóa đọc-ghi nếu số lượng; số luồng ứng dụng Có rất nhiều, sự cạnh tranh rất khốc liệt và nhạy cảm với hiệu suất, vì vậy chúng tôi vẫn cần phải làm việc chăm chỉ và sử dụng StampedLock phức tạp hơn để cải thiện thông lượng của chương trình.
Có hai điểm cần đặc biệt chú ý khi sử dụng StampedLock: Thứ nhất, StampedLock không được đăng ký lại và bạn không được tự khóa mình bằng một luồng duy nhất. Thứ hai, StampedLock không có cơ chế chờ/thông báo nếu bạn thực sự cần. chức năng này, bạn chỉ có thể tôi có thể đi xung quanh.
Tôi là Ao Bing. Bạn càng biết nhiều, bạn càng không biết. Hẹn gặp lại lần sau.
Địa chỉ gốc: https://mp.weixin.qq.com/s/gjfeayR36vDAr3FAVd4w4g.
Cuối cùng, bài viết này về cách sử dụng StampedLock và các ý tưởng triển khai chính sẽ kết thúc tại đây. Nếu bạn muốn biết thêm về cách sử dụng StampedLock và các ý tưởng triển khai chính, vui lòng tìm kiếm các bài viết về CFSDN hoặc tiếp tục duyệt qua các bài viết liên quan. ủng hộ blog của tôi trong tương lai! .
Tôi là một lập trình viên xuất sắc, rất giỏi!