Đây là một lời giải thích và ví dụ về cách thực hiện điều này. Hãy cho tôi biết nếu có những phần không rõ ràng.
Gist với nguồn
phổ cập
Khởi tạo:
Chỉ mục chủ đề được áp dụng theo kiểu tăng nguyên tử. Điều này được quản lý bằng cách sử dụng một AtomicInteger
tên nextIndex
. Các chỉ mục này được gán cho các luồng thông qua một ThreadLocal
thể hiện tự khởi chạy bằng cách lấy chỉ mục tiếp theo từ đó nextIndex
và tăng nó. Điều này xảy ra lần đầu tiên khi chỉ mục của mỗi luồng được lấy lần đầu tiên. A ThreadLocal
được tạo để theo dõi chuỗi cuối cùng mà chủ đề này tạo ra. Nó được khởi tạo 0. Tham chiếu đối tượng nhà máy tuần tự được truyền vào và lưu trữ. Hai AtomicReferenceArray
trường hợp được tạo ra kích thước n
. Đối tượng đuôi được gán cho từng tham chiếu, đã được khởi tạo với trạng thái ban đầu do Sequential
nhà máy cung cấp . n
là số lượng chủ đề tối đa được phép. Mỗi phần tử trong các mảng 'thuộc về chỉ mục luồng tương ứng.
Áp dụng phương pháp:
Đây là phương pháp làm công việc thú vị. Nó làm như sau:
- Tạo một nút mới cho lệnh gọi này: của tôi
- Đặt nút mới này trong mảng thông báo tại chỉ mục của luồng hiện tại
Sau đó, vòng lặp trình tự bắt đầu. Nó sẽ tiếp tục cho đến khi lệnh gọi hiện tại đã được giải trình tự:
- tìm một nút trong mảng thông báo bằng cách sử dụng chuỗi của nút cuối cùng được tạo bởi luồng này. Thêm về điều này sau.
- nếu một nút được tìm thấy ở bước 2 thì nó chưa được tuần tự, hãy tiếp tục với nó, nếu không, chỉ cần tập trung vào lời gọi hiện tại. Điều này sẽ chỉ cố gắng giúp một nút khác cho mỗi lần gọi.
- Bất cứ nút nào được chọn trong bước 3, hãy tiếp tục thử trình tự sau nút thứ tự cuối cùng (các luồng khác có thể can thiệp.) Bất kể thành công, hãy đặt tham chiếu đầu của các luồng hiện tại vào chuỗi được trả về bởi
decideNext()
Chìa khóa của vòng lặp lồng nhau được mô tả ở trên là decideNext()
phương thức. Để hiểu điều đó, chúng ta cần nhìn vào lớp Node.
Lớp nút
Lớp này chỉ định các nút trong danh sách liên kết đôi. Không có nhiều hành động trong lớp này. Hầu hết các phương thức là các phương thức truy xuất đơn giản nên khá tự giải thích.
phương pháp đuôi
cái này trả về một thể hiện nút đặc biệt với một chuỗi 0. Nó chỉ hoạt động như một trình giữ chỗ cho đến khi một lệnh gọi thay thế nó.
Thuộc tính và khởi tạo
seq
: số thứ tự, được khởi tạo thành -1 (có nghĩa là không có kết quả)
invocation
: giá trị của lời gọi của apply()
. Đặt khi xây dựng.
next
: AtomicReference
cho liên kết chuyển tiếp. một khi được giao, điều này sẽ không bao giờ được thay đổi
previous
: AtomicReference
cho liên kết ngược được chỉ định khi giải trình tự và bị xóa bởitruncate()
Quyết định tiếp theo
Phương thức này chỉ là một trong Node với logic không tầm thường. Tóm lại, một nút được cung cấp như một ứng cử viên để trở thành nút tiếp theo trong danh sách được liên kết. Các compareAndSet()
phương pháp sẽ kiểm tra nếu nó tham khảo là null và nếu như vậy, thiết lập các tham chiếu đến các ứng cử viên. Nếu tham chiếu đã được đặt, nó không làm gì cả. Hoạt động này là nguyên tử vì vậy nếu hai ứng cử viên được cung cấp cùng một lúc, chỉ có một ứng cử viên sẽ được chọn. Điều này đảm bảo chỉ có một nút sẽ được chọn là nút tiếp theo. Nếu nút ứng cử viên được chọn, chuỗi của nó được đặt thành giá trị tiếp theo và liên kết trước đó được đặt thành nút này.
Nhảy lại phương thức áp dụng lớp Universal ...
Đã gọi decideNext()
nút được giải trình tự cuối cùng (khi được chọn) bằng nút của chúng tôi hoặc nút từ announce
mảng, có hai lần xuất hiện có thể xảy ra: 1. Nút đã được giải trình tự thành công 2. Một số luồng khác đã xử lý trước luồng này.
Bước tiếp theo là kiểm tra xem nút được tạo cho lệnh gọi này. Điều này có thể xảy ra bởi vì chủ đề này đã giải trình tự thành công nó hoặc một số chủ đề khác nhặt nó từ announce
mảng và giải trình tự cho chúng tôi. Nếu nó chưa được giải trình tự, quá trình được lặp lại. Mặt khác, cuộc gọi kết thúc bằng cách xóa mảng thông báo cho chỉ mục của luồng này và trả về giá trị kết quả của lệnh gọi. Mảng thông báo được xóa để đảm bảo không có tham chiếu nào đến nút còn lại xung quanh sẽ ngăn nút được thu gom rác và do đó giữ tất cả các nút trong danh sách được liên kết từ thời điểm đó còn tồn tại.
Đánh giá phương pháp
Bây giờ nút của lệnh gọi đã được giải trình tự thành công, yêu cầu cần phải được đánh giá. Để làm điều đó, bước đầu tiên là đảm bảo rằng các yêu cầu trước cái này đã được đánh giá. Nếu họ không có chủ đề này sẽ không chờ đợi nhưng sẽ thực hiện công việc đó ngay lập tức.
Phương pháp SureP Warrior
Các ensurePrior()
phương pháp làm việc này bằng cách kiểm tra các nút trước đó trong danh sách liên kết. Nếu trạng thái của nó không được đặt, nút trước đó sẽ được đánh giá. Nút đó là đệ quy. Nếu nút trước nút trước chưa được đánh giá, nó sẽ gọi đánh giá cho nút đó và cứ thế tiếp tục.
Bây giờ nút trước đó được biết là có trạng thái, chúng ta có thể đánh giá nút này. Nút cuối cùng được lấy và gán cho một biến cục bộ. Nếu tham chiếu này là null, điều đó có nghĩa là một số luồng khác đã lấy trước cái này và đã đánh giá nút này; thiết lập trạng thái của nó. Mặt khác, trạng thái của nút trước được truyền cho Sequential
phương thức áp dụng của đối tượng cùng với lời gọi của nút này. Trạng thái được trả về được đặt trên nút và truncate()
phương thức được gọi, xóa liên kết ngược khỏi nút vì nó không còn cần thiết nữa.
Phương pháp MoveForward
Phương thức di chuyển về phía trước sẽ cố gắng di chuyển tất cả các tham chiếu đầu đến nút này nếu chúng chưa được trỏ đến một cái gì đó xa hơn. Điều này là để đảm bảo rằng nếu một luồng ngừng gọi, thì đầu của nó sẽ không giữ lại một tham chiếu đến một nút không còn cần thiết nữa. Các compareAndSet()
phương pháp sẽ đảm bảo chúng tôi chỉ cập nhật nút nếu một số chủ đề khác vẫn không thay đổi nó vì nó đã được lấy ra.
Thông báo mảng và giúp đỡ
Chìa khóa để làm cho cách tiếp cận này không phải chờ đợi thay vì chỉ đơn giản là không khóa là chúng ta không thể cho rằng bộ lập lịch xử lý sẽ ưu tiên cho mỗi luồng khi cần. Nếu mỗi luồng chỉ đơn giản là cố gắng sắp xếp các nút riêng của nó, thì có thể một luồng có thể liên tục được tải trước khi tải. Để giải thích cho khả năng này, trước tiên, mỗi luồng sẽ cố gắng 'trợ giúp' các luồng khác có thể không được giải trình tự.
Ý tưởng cơ bản là khi mỗi luồng tạo thành công các nút, các chuỗi được gán sẽ tăng đơn điệu. Nếu một luồng hoặc luồng liên tục làm trống trước một luồng khác, thì chỉ mục sử dụng để tìm các nút không có kết quả trong announce
mảng sẽ di chuyển về phía trước. Ngay cả khi tất cả các luồng hiện đang cố gắng sắp xếp một nút đã cho liên tục được xử lý trước bởi một luồng khác, cuối cùng tất cả các luồng sẽ cố gắng xâu chuỗi nút đó. Để minh họa, chúng tôi sẽ xây dựng một ví dụ với ba luồng.
Tại điểm bắt đầu, tất cả các phần tử đầu và thông báo của ba luồng được chỉ vào tail
nút. Đối lastSequence
với mỗi chủ đề là 0.
Tại thời điểm này, Thread 1 được thực thi với một lời gọi. Nó kiểm tra mảng thông báo cho chuỗi cuối cùng của nó (không), đó là nút mà nó hiện đang được lên lịch để lập chỉ mục. Nó tuần tự nút và nó lastSequence
được đặt thành 1.
Chủ đề 2 hiện được thực thi với một lời gọi, nó kiểm tra mảng thông báo ở chuỗi cuối cùng của nó (không) và thấy rằng nó không cần trợ giúp và vì vậy cố gắng thực hiện chuỗi gọi đó. Nó thành công và bây giờ nó lastSequence
được đặt thành 2.
Bây giờ, luồng 3 đã được thực thi và nó cũng thấy rằng nút tại announce[0]
đã được sắp xếp theo trình tự và trình tự đó là lệnh gọi riêng của nó. Nó lastSequence
được thiết lập tới 3.
Bây giờ Thread 1 được gọi lại. Nó kiểm tra mảng thông báo tại chỉ mục 1 và thấy rằng nó đã được giải trình tự. Đồng thời, Thread 2 được gọi. Nó kiểm tra mảng thông báo tại chỉ mục 2 và thấy rằng nó đã được giải trình tự. Cả Thread 1 và Thread 2 hiện đang cố gắng sắp xếp các nút riêng của chúng. Chủ đề 2 chiến thắng và nó tiếp theo đó là lời mời. Nó lastSequence
được đặt thành 4. Trong khi đó, chuỗi ba đã được gọi. Nó kiểm tra chỉ mục nó lastSequence
(mod 3) và thấy rằng nút tại announce[0]
chưa được giải trình tự. Thread 2 một lần nữa được gọi cùng lúc với Thread 1 trong lần thử thứ hai. Chủ đề 1tìm thấy một lời gọi không có kết quả tại announce[1]
đó là nút vừa được tạo bởi Thread 2 . Nó cố gắng thực hiện chuỗi yêu cầu của Thread 2 và thành công. Chủ đề 2 tìm thấy nút riêng của nó tại announce[1]
và nó đã được giải trình tự. Nó đặt nó lastSequence
thành 5. Chủ đề 3 sau đó được gọi và tìm thấy nút mà chủ đề 1 được đặt announce[0]
vẫn chưa được giải trình tự và cố gắng thực hiện. Trong khi đó, Thread 2 cũng đã được gọi và pre-empts Thread 3. Nó tuần tự nó là nút và đặt nó lastSequence
thành 6.
Chủ đề kém 1 . Mặc dù Thread 3 đang cố gắng sắp xếp nó, cả hai luồng đã liên tục bị cản trở bởi bộ lập lịch. Nhưng tại thời điểm này. Chủ đề 2 hiện cũng đang trỏ đến announce[0]
(6 mod 3). Tất cả ba luồng được thiết lập để cố gắng sắp xếp thứ tự giống nhau. Bất kể luồng nào thành công, nút tiếp theo sẽ được giải trình tự sẽ là lệnh gọi chờ của luồng 1 tức là nút được tham chiếu bởi announce[0]
.
Điều này là không thể tránh khỏi. Để các luồng được xử lý trước, các luồng khác phải được sắp xếp các nút và khi chúng làm như vậy, chúng sẽ tiếp tục di chuyển về lastSequence
phía trước. Nếu nút của một luồng đã cho liên tục không được tuần tự, cuối cùng tất cả các luồng sẽ được trỏ đến chỉ mục của nó trong mảng thông báo. Không có luồng nào sẽ làm bất cứ điều gì khác cho đến khi nút mà nó đang cố gắng trợ giúp đã được giải trình tự, trường hợp xấu nhất là tất cả các luồng đều trỏ đến cùng một nút không được xử lý. Do đó, thời gian cần thiết để sắp xếp bất kỳ lệnh gọi nào là một hàm của số lượng luồng chứ không phải kích thước của đầu vào.