sách gpt4 ai đã đi

Hiểu sâu sắc về cơ chế lập lịch bộ đếm thời gian Java

In lại Tác giả:qq735679552 Thời gian cập nhật: 2022-09-28 22:32:09 27 4
mua khóa gpt4 Nike

CFSDN nhấn mạnh vào việc tạo ra giá trị thông qua mã nguồn mở. Chúng tôi cam kết xây dựng một nền tảng chia sẻ tài nguyên để mọi người làm CNTT có thể tìm thấy thế giới tuyệt vời của riêng mình tại đây.

Bài đăng trên blog CFSDN này cung cấp hiểu biết sâu sắc về cơ chế lập lịch bộ đếm thời gian Java. Bài đăng được tác giả thu thập và biên soạn. Nếu bạn quan tâm đến bài viết này, vui lòng thích bài viết.

Giới thiệu.

Khi triển khai chức năng lập lịch thời gian, chúng ta thường sử dụng các thư viện của bên thứ ba để hoàn thiện chức năng này như: quartz, spring schedule, v.v. Kể từ phiên bản 1.3, JDK đã cung cấp chức năng lập lịch dựa trên bộ đếm thời gian. Trong bộ đếm thời gian, các tác vụ được thực hiện tuần tự. Mặc dù đảm bảo tính an toàn của luồng, tính năng này thường mang lại một số tác dụng phụ nghiêm trọng, chẳng hạn như ảnh hưởng lẫn nhau giữa các tác vụ và hiệu quả thực hiện tác vụ thấp. Để giải quyết những vấn đề này của bộ hẹn giờ, JDK đã cung cấp chức năng lập lịch bộ hẹn giờ dựa trên ScheduledExecutorService kể từ phiên bản 1.5.

Trong phần này chúng ta chủ yếu phân tích chức năng của bộ hẹn giờ. Chúng tôi sẽ mở một bài viết mới để giải thích các chức năng của ScheduledExecutorService.

Cách sử dụng.

Timer cần được sử dụng cùng với timertask để hoàn thành chức năng lập lịch. timer đại diện cho bộ lập lịch và timertask đại diện cho tác vụ được bộ lập lịch thực hiện. Có hai loại lập lịch tác vụ: lập lịch một lần và lập lịch theo chu kỳ. Dưới đây, chúng ta hãy xem một số ví dụ để biết cách sử dụng chúng.

1. Lên lịch một lần.

?
1
2
3
4
5
6
7
8
9
10
11
12
công cộng tĩnh vô hiệu main(chuỗi[] args) {
   hẹn giờ hẹn giờ = mới bộ đếm thời gian();
   nhiệm vụ hẹn giờ = mới nhiệm vụ hẹn giờ() {
     @ghi đè công cộng vô hiệu chạy() {
       định dạng simpledateformat = mới định dạng ngày tháng đơn giản( "hh:mm:ss" );
       system.out.println(format.format(scheduledexecutiontime()) + ", gọi điện" );
     }
   };
   // Trì hoãn một giây và in một lần
   // Kết quả in như sau: 10:58:24, được gọi
   timer.schedule(nhiệm vụ, 1000 );
}

2. Lập lịch vòng lặp - schedule().

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
công cộng tĩnh vô hiệu main(chuỗi[] args) {
   hẹn giờ hẹn giờ = mới bộ đếm thời gian();
   nhiệm vụ hẹn giờ = mới nhiệm vụ hẹn giờ() {
     @ghi đè công cộng vô hiệu chạy() {
       định dạng simpledateformat = mới định dạng ngày tháng đơn giản( "hh:mm:ss" );
       system.out.println(format.format(scheduledexecutiontime()) + ", gọi điện" );
     }
   };
   // Phương pháp lập lịch thời gian cố định, trì hoãn một giây, sau đó in một lần mỗi giây
   // Kết quả in như sau:
   // 11:03:55, đã gọi
   // 11:03:56, đã gọi
   // 11:03:57, đã gọi
   // 11:03:58, đã gọi
   // 11:03:59, đã gọi
   // ...
   timer.schedule(nhiệm vụ, 1000 , 1000 );
}

3. Lập lịch vòng tròn - scheduleatfixedrate() .

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
công cộng tĩnh vô hiệu main(chuỗi[] args) {
   hẹn giờ hẹn giờ = mới bộ đếm thời gian();
   nhiệm vụ hẹn giờ = mới nhiệm vụ hẹn giờ() {
     @ghi đè công cộng vô hiệu chạy() {
       định dạng simpledateformat = mới định dạng ngày tháng đơn giản( "hh:mm:ss" );
       system.out.println(format.format(scheduledexecutiontime()) + ", gọi điện" );
     }
   };
   // Lên lịch tốc độ cố định, trì hoãn một giây, sau đó in một lần mỗi giây
   // Kết quả in như sau:
   // 11:08:43, đã gọi
   // 11:08:44, đã gọi
   // 11:08:45, đã gọi
   // 11:08:46, đã gọi
   // 11:08:47, đã gọi
   // ...
   timer.scheduleatfixedrate(nhiệm vụ, 1000 , 1000 );
}

4. Sự khác biệt giữa schedule() và schedulefixedrate().

Xét theo kết quả của 2 và 3, hiệu quả mà chúng đạt được có vẻ như giống nhau. Vì hiệu ứng là như nhau, tại sao JDK lại triển khai nó theo hai phương pháp? Chúng phải khác nhau chứ! .

Trong điều kiện bình thường, tác dụng của chúng hoàn toàn giống nhau. Trong trường hợp bất thường - khi thời gian thực hiện nhiệm vụ dài hơn khoảng thời gian, hiệu ứng của chúng sẽ khác nhau.

  1. phương thức schedule(), thời gian thực hiện tiếp theo của tác vụ liên quan đến thời gian thực hiện cuối cùng thực sự hoàn thành, do đó thời gian thực hiện sẽ liên tục bị trì hoãn
  2. phương thức scheduleatfixedrate(), thời gian thực hiện tiếp theo của tác vụ sẽ liên quan đến thời gian khi lần thực hiện cuối cùng bắt đầu, do đó thời gian thực hiện sẽ không bị chậm trễ
  3. Vì bộ đếm thời gian được triển khai theo cách đơn luồng nên không có phương pháp nào trong hai phương pháp này có vấn đề về an toàn luồng.

Trước tiên chúng ta hãy xem xét những tác động bất thường của schedule():

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
hai mươi mốt
hai mươi hai
hai mươi ba
công cộng tĩnh vô hiệu main(chuỗi[] args) {
   hẹn giờ hẹn giờ = mới bộ đếm thời gian();
   nhiệm vụ hẹn giờ = mới nhiệm vụ hẹn giờ() {
     @ghi đè công cộng vô hiệu chạy() {
       định dạng simpledateformat = mới định dạng ngày tháng đơn giản( "hh:mm:ss" );
       thử {
         thread.sleep( 3000 );
       } nắm lấy (interruptedexception e) {
         e.printstacktrace();
       }
       system.out.println(format.format(scheduledexecutiontime()) + ", gọi điện" );
     }
   };
 
   timer.schedule(nhiệm vụ, 1000 , 2000 );
   // Kết quả thực hiện như sau:
   // 11:18:56, đã gọi
   // 11:18:59, đã gọi
   // 11:19:02, đã gọi
   // 11:19:05, đã gọi
   // 11:19:08, đã gọi
   // 11:19:11, đã gọi
}

Tiếp theo, chúng ta hãy xem xét những tác động bất thường của scheduleeatfixedrate():

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
hai mươi mốt
hai mươi hai
hai mươi ba
công cộng tĩnh vô hiệu main(chuỗi[] args) {
   hẹn giờ hẹn giờ = mới bộ đếm thời gian();
   nhiệm vụ hẹn giờ = mới nhiệm vụ hẹn giờ() {
     @ghi đè công cộng vô hiệu chạy() {
       định dạng simpledateformat = mới định dạng ngày tháng đơn giản( "hh:mm:ss" );
       thử {
         thread.sleep( 3000 );
       } nắm lấy (interruptedexception e) {
         e.printstacktrace();
       }
       system.out.println(format.format(scheduledexecutiontime()) + ", gọi điện" );
     }
   };
 
   timer.scheduleatfixedrate(nhiệm vụ, 1000 , 2000 );
   // Kết quả thực hiện như sau:
   // 11:20:45, đã gọi
   // 11:20:47, đã gọi
   // 11:20:49, đã gọi
   // 11:20:51, đã gọi
   // 11:20:53, đã gọi
   // 11:20:55, đã gọi
}

Tác giả luôn tin rằng thực hành là cách tốt hơn để kiểm tra sự thật. Ví dụ trên xác minh phỏng đoán ban đầu của chúng tôi từ bên ngoài.

Tuy nhiên, điều này lại nảy sinh một câu hỏi khác. Vì bộ đếm thời gian được triển khai trong một luồng duy nhất bên trong, vậy ScheduleAtFixedRate sẽ xuất dữ liệu sau mỗi 2 giây như thế nào khi khoảng thời gian thực hiện là 2 giây và tác vụ thực sự thực hiện trong 3 giây?

【Chú ý đặc biệt】 .

Thực ra đây chỉ là một trò ảo thuật. Điều quan trọng cần lưu ý là giá trị đầu ra của phương thức print được tạo ra bằng cách gọi scheduleexecutiontime(), không nhất thiết là thời gian thực hiện thực tế của tác vụ, mà là thời gian tác vụ hiện tại cần được thực thi.

Đọc mã nguồn.

Quan điểm của tác giả về kiến ​​thức là ngoài việc biết các sự kiện, người ta còn cần biết lý do đằng sau chúng. Đọc mã nguồn là chìa khóa quan trọng để mở ra cánh cửa dẫn đến hiểu biết tại sao một điều gì đó lại xảy ra. Trong JDK, bộ đếm thời gian chủ yếu bao gồm timertask, taskqueue và timerthread.

1. nhiệm vụ hẹn giờ.

timertask biểu thị tác vụ được thực hiện bởi trình lập lịch tác vụ. Nó kế thừa từ runnable và duy trì trạng thái của tác vụ bên trong. Tổng cộng có bốn trạng thái.

  1. virgin, tên tiếng Anh là virgin, chỉ ra rằng nhiệm vụ vẫn chưa được lên lịch
  2. đã lên lịch, đã được lên lịch, nhưng vẫn chưa được thực hiện
  3. đã thực hiện: Đối với một tác vụ được thực hiện một lần, điều đó có nghĩa là tác vụ đó đã được thực hiện; đối với một tác vụ được thực hiện nhiều lần, trạng thái này không hợp lệ.
  4. đã hủy, nhiệm vụ đã bị hủy

timertask cũng có các biến thành viên sau.

  1. nextexecutiontime, thời gian thực hiện tiếp theo
  2. khoảng thời gian: khoảng thời gian thực hiện nhiệm vụ. Số dương biểu thị tỷ lệ cố định; số âm biểu thị độ trễ cố định; 0 biểu thị chỉ thực hiện một lần

Sau khi phân tích các chức năng chung, chúng ta hãy xem xét mã của nó.

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
hai mươi mốt
hai mươi hai
hai mươi ba
hai mươi bốn
25
26
/**
  * trạng thái của tác vụ này, được chọn từ các hằng số bên dưới.
  */
số nguyên trạng thái = trinh nguyên;
 
/**
  * nhiệm vụ này vẫn chưa được lên lịch.
  */
tĩnh cuối cùng số nguyên trinh nữ = 0 ;
 
/**
  * nhiệm vụ này được lên lịch thực hiện. Nếu đây là nhiệm vụ không lặp lại,
  * vẫn chưa được thực hiện.
  */
tĩnh cuối cùng số nguyên đã lên lịch = 1 ;
 
/**
  * Nhiệm vụ không lặp lại này đã được thực hiện (hoặc hiện đang được thực hiện)
  * đang thực hiện) và chưa bị hủy.
  */
tĩnh cuối cùng số nguyên đã thực hiện = 2 ;
 
/**
  * tác vụ này đã bị hủy (bằng cách gọi đến timertask.cancel).
  */
tĩnh cuối cùng số nguyên đã hủy = 3 ;

Timertask có hai phương thức hoạt động.

  1. cancel() // hủy tác vụ
  2. scheduleexecutiontime() // Lấy thời gian thực hiện tác vụ

cancel() tương đối đơn giản, chủ yếu là khóa tác vụ hiện tại và sau đó thay đổi trạng thái thành đã hủy.

?
1
2
3
4
5
6
7
công cộng Boolean Hủy bỏ() {
   đồng bộ (khóa) {
     Boolean kết quả = (trạng thái == đã lên lịch);
     trạng thái = đã hủy;
     trở lại kết quả;
   }
}

Trong scheduleexecutiontime(), thời gian thực hiện tác vụ được tính bằng cách trừ thời gian khoảng thời gian khỏi thời gian thực hiện tiếp theo.

?
1
2
3
4
5
6
công cộng dài thời gian thực hiện theo lịch trình() {
   đồng bộ (khóa) {
     trở lại (dấu chấm < 0 ? thời gian thực hiện tiếp theo + chu kỳ
               :nexexecutiontime - dấu chấm);
   }
}

2. hàng đợi tác vụ.

Taskqueue là hàng đợi được sử dụng để lưu trữ các tác vụ trong bộ đếm thời gian. Nó được triển khai nội bộ bằng cách sử dụng [thuật toán heap tối thiểu] và tác vụ ở đầu heap sẽ được thực thi trước. Do sử dụng [min heap], taskqueue cực kỳ hiệu quả trong việc xác định thời gian thực thi đã đến hay chưa. Chúng ta hãy cùng xem xét cách nó được triển khai nội bộ.

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
hai mươi mốt
hai mươi hai
hai mươi ba
hai mươi bốn
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
lớp học hàng đợi tác vụ {
   /**
    * hàng đợi ưu tiên được biểu diễn như một đống nhị phân cân bằng: hai đứa trẻ
    * của hàng đợi [n] là hàng đợi [2 * n] và hàng đợi [2 * n + 1]. hàng đợi ưu tiên là
    * được sắp xếp trên trường nextexecutiontime: timertask có giá trị thấp nhất
    * nextexecutiontime đang trong hàng đợi[1] (giả sử hàng đợi không trống). đối với
    * mỗi nút n trong đống, và mỗi con cháu của n, d,
    * n.thời gian thực hiện tiếp theo <= d.thời gian thực hiện tiếp theo.
    *
    * Sử dụng mảng để lưu trữ các tác vụ
    */
   riêng tư timertask[] hàng đợi = mới nhiệm vụ hẹn giờ[ 128 ];
 
   /**
    * số lượng tác vụ trong hàng đợi ưu tiên. (các tác vụ được lưu trữ trong
    * hàng đợi [1] lên đến hàng đợi [kích thước]).
    *
    * Được sử dụng để chỉ số lượng tác vụ trong hàng đợi. Cần lưu ý rằng số lượng tác vụ không bằng độ dài của mảng
    */
   riêng tư số nguyên kích thước = 0 ;
 
   /**
    * trả về số lượng tác vụ hiện có trong hàng đợi.
    */
   số nguyên kích cỡ() {
     trở lại kích cỡ;
   }
 
   /**
    * thêm một nhiệm vụ mới vào hàng đợi ưu tiên.
    *
    * Thêm một nhiệm vụ vào hàng đợi
    */
   vô hiệu thêm(nhiệm vụ timertask) {
     // phát triển kho lưu trữ nếu cần thiết
     // Nếu số lượng tác vụ vượt quá độ dài của mảng, việc mở rộng động được thực hiện bằng cách sao chép mảng
     nếu như (kích thước + 1 == độ dài hàng đợi)
       hàng đợi = mảng.copyof(hàng đợi, 2 *queue.length);
 
     // Đưa mục tác vụ hiện tại vào hàng đợi
     hàng đợi[++kích thước] = nhiệm vụ;
     // Điều chỉnh lên trên để tạo lại một đống nhỏ nhất
     sửa chữa(kích thước);
   }
 
   /**
    * trả về "nhiệm vụ chính" của hàng đợi ưu tiên. (nhiệm vụ chính là
    * nhiệm vụ có thời gian thực thi tiếp theo thấp nhất.)
    *
    * Phần tử đầu tiên của hàng đợi là tác vụ đầu tiên được thực hiện
    */
   nhiệm vụ hẹn giờ getmin() {
     trở lại xếp hàng[ 1 ];
   }
 
   /**
    * trả về tác vụ thứ i trong hàng đợi ưu tiên, trong đó i có phạm vi từ 1 (
    * nhiệm vụ chính, được trả về bởi getmin) cho số lượng nhiệm vụ trên
    * xếp hàng, bao gồm.
    *
    * Lấy phần tử của chỉ mục được chỉ định trong hàng đợi
    */
   timertask lấy( số nguyên Tôi) {
     trở lại hàng đợi[i];
   }
 
   /**
    * xóa tác vụ chính khỏi hàng đợi ưu tiên.
    *
    * Loại bỏ phần tử trên cùng của đống. Sau khi loại bỏ, cần phải điều chỉnh xuống dưới để tạo lại đống tối thiểu
    */
   vô hiệu xóamin() {
     xếp hàng[ 1 ] = hàng đợi[kích thước];
     hàng đợi[kích thước--] = vô giá trị ; // xóa tham chiếu thừa để tránh rò rỉ bộ nhớ
     sửa chữa( 1 );
   }
 
   /**
    * xóa phần tử thứ i khỏi hàng đợi mà không cần quan tâm đến việc duy trì
    * bất biến heap. nhớ lại rằng hàng đợi là dựa trên một, vì vậy
    * 1 <= i <= kích thước.
    *
    * Nhanh chóng tháo phần tử ở vị trí đã chỉ định mà không cần điều chỉnh lại ngăn xếp
    */
   vô hiệu xóa nhanh( số nguyên Tôi) {
     khẳng định i <= kích thước;
 
     queue[i] = queue[kích thước];
     hàng đợi[kích thước--] = vô giá trị ; // bỏ tham chiếu thừa để tránh rò rỉ bộ nhớ
   }
 
   /**
    * thiết lập thời gian thực thi tiếp theo liên quan đến tác vụ chính thành
    * giá trị được chỉ định và điều chỉnh hàng đợi ưu tiên cho phù hợp.
    *
    * Lên lịch lại và điều chỉnh xuống để tạo lại đống tối thiểu
    */
   vô hiệu lên lịch lại phút( dài thời gian mới) {
     xếp hàng[ 1 ].nexexecutiontime = thời gian mới;
     sửa chữa( 1 );
   }
 
   /**
    * trả về true nếu hàng đợi ưu tiên không chứa phần tử nào.
    *
    * Hàng đợi có trống không?
    */
   Boolean rỗng() {
     trở lại kích thước== 0 ;
   }
 
   /**
    * xóa tất cả các phần tử khỏi hàng đợi ưu tiên.
    *
    * Xóa tất cả các phần tử khỏi hàng đợi
    */
   vô hiệu thông thoáng() {
     // null out tham chiếu tác vụ để ngăn chặn rò rỉ bộ nhớ
     ( số nguyên tôi= 1 ; i<=kích thước; i++)
       hàng đợi[i] = vô giá trị ;
 
     kích thước = 0 ;
   }
 
   /**
    * thiết lập bất biến heap (được mô tả ở trên) giả sử heap
    * thỏa mãn bất biến ngoại trừ có thể đối với nút lá được lập chỉ mục bởi k
    * (có thể có thời gian thực thi tiếp theo ngắn hơn thời gian thực thi của chương trình cha).
    *
    * Phương pháp này hoạt động bằng cách "thăng cấp" hàng đợi [k] lên trên hệ thống phân cấp
    * (bằng cách hoán đổi nó với phần tử cha của nó) nhiều lần cho đến khi hàng đợi [k]
    * nextexecutiontime lớn hơn hoặc bằng thời gian thực thi của chương trình cha.
    *
    * Điều chỉnh lên trên để tạo lại đống tối thiểu
    */
   riêng tư vô hiệu sửa chữa( số nguyên k) {
     trong khi (k > 1 ) {
       số nguyên j = k >> 1 ;
       nếu như (hàng đợi[j].thời gian thực hiện tiếp theo <= hàng đợi[k].thời gian thực hiện tiếp theo)
         phá vỡ ;
       timertask tmp = hàng đợi[j]; hàng đợi[j] = hàng đợi[k]; hàng đợi[k] = tmp;
       k = j;
     }
   }
 
   /**
    * thiết lập bất biến heap (được mô tả ở trên) trong cây con
    * có gốc tại k, được cho là thỏa mãn bất biến heap ngoại trừ
    * có thể đối với chính nút k (có thể có thời gian thực hiện tiếp theo lớn hơn
    * hơn là con của nó).
    *
    * Phương pháp này hoạt động bằng cách "hạ cấp" hàng đợi [k] xuống dưới thứ bậc
    * (bằng cách hoán đổi nó với phần tử con nhỏ hơn của nó) nhiều lần cho đến khi hàng đợi [k]
    * nextexecutiontime nhỏ hơn hoặc bằng thời gian thực hiện của các tiến trình con của nó.
    *
    * Điều chỉnh xuống dưới để tạo lại đống tối thiểu
    */
   riêng tư vô hiệu sửa chữa( số nguyên k) {
     số nguyên j;
     trong khi ((j = k << 1 ) <= kích thước && j > 0 ) {
       nếu như (j < kích thước &&
         hàng đợi[j].thời gian thực hiện tiếp theo > hàng đợi[j+ 1 ].thời gian thực hiện tiếp theo)
         j++; // j lập chỉ mục đứa trẻ nhỏ nhất
       nếu như (hàng đợi[k].thời gian thực hiện tiếp theo <= hàng đợi[j].thời gian thực hiện tiếp theo)
         phá vỡ ;
       timertask tmp = hàng đợi[j]; hàng đợi[j] = hàng đợi[k]; hàng đợi[k] = tmp;
       k = j;
     }
   }
 
   /**
    * thiết lập bất biến heap (được mô tả ở trên) trong toàn bộ cây,
    * không giả định bất cứ điều gì về thứ tự của các phần tử trước khi gọi.
    */
   vô hiệu làm đống() {
     ( số nguyên i = kích thước/ 2 ; tôi >= 1 ; Tôi--)
       sửa lỗi(i);
   }
}

3. luồng hẹn giờ.

Là một biến thành viên của bộ đếm thời gian, timerthread đóng vai trò hiệu chuẩn bộ lập lịch. Trước tiên chúng ta hãy xem xét phương pháp xây dựng của nó, chủ yếu là giữ hàng đợi tác vụ.

?
1
2
3
timerthread(hàng đợi tác vụ) {
   cái này .queue = hàng đợi;
}

Tiếp theo, chúng ta hãy xem phương thức run(), đây là điểm vào để thực thi luồng.

?
1
2
3
4
5
6
7
8
9
10
11
công cộng vô hiệu chạy() {
   thử {
     vòng lặp chính();
   } Cuối cùng {
     // ai đó đã giết chủ đề này, hành động như thể bộ đếm thời gian đã bị hủy
     đồng bộ (hàng đợi) {
       newtasksmaybescheduled = SAI ;
       queue.clear(); // loại bỏ các tham chiếu lỗi thời
     }
   }
}

Logic chính đều nằm trong phương thức mainloop(). Sau khi phương thức vòng lặp chính được thực thi, tài nguyên sẽ được dọn sạch. Chúng ta hãy cùng xem xét phương thức mainloop().

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
hai mươi mốt
hai mươi hai
hai mươi ba
hai mươi bốn
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
riêng tư vô hiệu vòng lặp chính() {
   // vòng lặp vô hạn while
   trong khi ( ĐÚNG VẬY ) {
     thử {
       nhiệm vụ timertask;
       Boolean đã làm nhiệm vụ;
       // Khóa hàng đợi để đảm bảo rằng tất cả các tác vụ trong hàng đợi được thực hiện tuần tự
       đồng bộ (hàng đợi) {
         // chờ cho hàng đợi không còn trống
         // Hoạt động 1, hàng đợi trống và cần phải chờ các tác vụ mới được lên lịch. Lúc này, hoạt động chờ được thực hiện
         trong khi (queue.isempty() && newtasksmaybescheduled)
           hàng đợi.chờ();
         // Ở đây chúng ta kiểm tra xem hàng đợi có trống nữa không vì một tác vụ đã đến cho [Hoạt động 1] và tác vụ đó đã bị hủy (hoạt động `hủy` đã được thực hiện).
         // Nếu hàng đợi lại trống, bạn cần thoát khỏi luồng để tránh vòng lặp bị kẹt
         nếu như (queue.isemty())
           phá vỡ ; // hàng đợi trống và sẽ tồn tại mãi mãi; chết
 
         // hàng đợi không rỗng; hãy xem evt đầu tiên và làm điều đúng đắn
         dài thời gian hiện tại, thời gian thực hiện;
         // Lấy phần tử trên cùng trong hàng đợi (tác vụ có thời gian thực thi tiếp theo nhỏ nhất)
         nhiệm vụ = queue.getmin();
         // Các phần tử heap được khóa ở đây để đảm bảo tính khả kiến ​​và tính nguyên tử của tác vụ
         đồng bộ (task.lock) {
           // Nhiệm vụ đã hủy sẽ không còn được thực hiện nữa và cần phải xóa khỏi hàng đợi
           nếu như (task.state == timertask.cancelled) {
             queue.removemin();
             Tiếp tục ; // không cần hành động, thăm dò hàng đợi một lần nữa
           }
           // Lấy thời gian hệ thống hiện tại và thời gian thực hiện tiếp theo của tác vụ
           currenttime = system.currenttimemillis();
           thời gian thực hiện = nhiệm vụ. thời gian thực hiện tiếp theo;
 
           // Nếu thời gian thực hiện tiếp theo của tác vụ <= thời gian hệ thống hiện tại, hãy thực hiện tác vụ này (đặt cờ trạng thái `taskfired` thành true)
           nếu như (taskfired = (thời gian thực hiện <=thời gian hiện tại)) {
             // `peroid` là 0, cho biết nhiệm vụ này chỉ cần được thực hiện một lần
             nếu như (nhiệm vụ.thời gian == 0 ) { // không lặp lại, xóa
               queue.removemin();
               task.state = timertask.executed;
             }
             // dấu chấm không phải là 0, cho biết nhiệm vụ này cần được thực hiện nhiều lần
             // Đây là sự khác biệt giữa phương thức `schedule()` và `scheduleatfixedrate()`
             khác { // lặp lại nhiệm vụ, lên lịch lại
               hàng đợi.reschedulemin(
                nhiệm vụ.thời gian< 0 ? thời gian hiện tại - task.period
                       : thời gian thực hiện + thời gian tác vụ);
             }
           }
         }
         // Nhiệm vụ không được kích hoạt, hàng đợi bị tạm dừng (có thời gian chờ)
         nếu như (!nhiệm vụ đã được thực hiện) // tác vụ vẫn chưa được kích hoạt; hãy đợi
           queue.wait(thời gian thực hiện - thời gian hiện tại);
       }
       // Nhiệm vụ được kích hoạt và thực thi. Sau khi thực hiện, nhập vòng lặp tiếp theo
       nếu như (đã hoàn thành nhiệm vụ) // tác vụ đã được kích hoạt; chạy nó, không giữ khóa
         task.run();
     } nắm lấy (interruptedexception e) {
     }
   }
}

4. bộ đếm thời gian.

Bộ đếm thời gian thực hiện những điều sau thông qua phương pháp xây dựng:

  • Điền vào các thuộc tính khác nhau của đối tượng timerthread (chẳng hạn như tên luồng, liệu đó có phải là luồng daemon hay không)
  • Bắt đầu một chủ đề
?
1
2
3
4
5
6
7
8
9
10
/**
  * chuỗi thời gian.
  */
riêng tư cuối cùng timerthread luồng = mới timerthread(hàng đợi);
 
công cộng timer(tên chuỗi, Boolean là daemon) {
   thread.setname(tên);
   thread.setdaemon(isdaemon);
   thread.bắt đầu();
}

Trong timer, chỉ có hai phương thức lập lịch thực sự được cung cấp cho người dùng, đó là schedule() và scheduleFixedRate(). Chúng ta hãy cùng xem xét.

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
hai mươi mốt
hai mươi hai
hai mươi ba
hai mươi bốn
25
26
27
28
29
30
31
32
33
34
35
36
37
38
công cộng vô hiệu lịch trình(timertask task, dài trì hoãn) {
   nếu như (trì hoãn < 0 )
     ném mới ngoại lệ đối số bất hợp pháp( "trì hoãn tiêu cực." );
   sched(nhiệm vụ, hệ thống.currenttimemillis()+trì hoãn, 0 );
}
 
công cộng vô hiệu lịch trình(timertask task, ngày giờ) {
   sched(nhiệm vụ, thời gian.gettime(), 0 );
}
 
công cộng vô hiệu lịch trình(timertask task, dài trì hoãn, dài Giai đoạn)
   nếu như (trì hoãn < 0 )
     ném mới ngoại lệ đối số bất hợp pháp( "trì hoãn tiêu cực." );
   nếu như (dấu chấm <= 0 )
     ném mới ngoại lệ đối số bất hợp pháp( "giai đoạn không tích cực." );
   sched(nhiệm vụ, hệ thống.currenttimemillis()+trì hoãn, -khoảng thời gian);
}
 
công cộng vô hiệu lịch trình(timertask task, ngày firsttime, dài Giai đoạn)
   nếu như (dấu chấm <= 0 )
     ném mới ngoại lệ đối số bất hợp pháp( "giai đoạn không tích cực." );
   sched(nhiệm vụ, firsttime.gettime(), -period);
}
 
công cộng vô hiệu scheduleatfixedrate(timertask task, dài trì hoãn, dài Giai đoạn)
   nếu như (trì hoãn < 0 )
     ném mới ngoại lệ đối số bất hợp pháp( "trì hoãn tiêu cực." );
   nếu như (dấu chấm <= 0 )
     ném mới ngoại lệ đối số bất hợp pháp( "giai đoạn không tích cực." );
   sched(nhiệm vụ, hệ thống.currenttimemillis()+trì hoãn, chu kỳ);
}
 
công cộng vô hiệu scheduleatfixedrate(timertask task, date firsttime,
                 dài Giai đoạn)
   nếu như (dấu chấm <= 0 )
     ném mới ngoại lệ đối số bất hợp pháp( "giai đoạn không tích cực." );
   sched(nhiệm vụ, firsttime.gettime(), thời gian);
}

Từ đoạn mã trên, chúng ta có thể thấy những điểm sau.

  1. Cả hai phương pháp cuối cùng đều gọi phương thức riêng tư sched()
  2. Khoảng thời gian được truyền vào schedule() là một số âm và khoảng thời gian được truyền vào scheduleatfixedrate() là một số dương.

Tiếp theo chúng ta xem phương thức sched().

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
hai mươi mốt
hai mươi hai
hai mươi ba
hai mươi bốn
25
26
27
28
29
30
31
32
riêng tư vô hiệu sched(timertask nhiệm vụ, dài thời gian, dài Giai đoạn)
   // 1. Kiểm tra xem `time` không thể là số âm
   nếu như (thời gian < 0 )
     ném mới ngoại lệ đối số bất hợp pháp( "thời gian thực hiện bất hợp pháp." );
 
   // hạn chế giá trị của chu kỳ đủ để ngăn chặn số
   // tràn nhưng vẫn có hiệu quả vô cùng lớn.
   // 2. `period` không thể vượt quá `long.max_value >> 1`
   nếu như (math.abs(chu kỳ) > ( dài .giá_trị_tối_đa >> 1 ))
     thời kỳ >>= 1 ;
 
   đồng bộ (hàng đợi) {
     // 3. Khi bộ hẹn giờ bị hủy, nó không thể được lên lịch
     nếu như (!thread.newtasksmaybescheduled)
       ném mới ngoại lệ bất hợp pháp( "bộ hẹn giờ đã bị hủy." );
 
     // 4. Khóa tác vụ, sau đó đặt thời gian thực hiện tiếp theo, chu kỳ thực hiện và trạng thái tác vụ của tác vụ để đảm bảo rằng việc lập lịch tác vụ và hủy tác vụ là an toàn cho luồng
     đồng bộ (task.lock) {
       nếu như (task.state != timertask.virgin)
         ném mới ngoại lệ bất hợp pháp(
           "nhiệm vụ đã được lên lịch hoặc đã hủy" );
       task.nexexecutiontime = thời gian;
       task.period = kỳ;
       task.state = timertask.scheduled;
     }
     // 5. Thêm tác vụ vào hàng đợi
     queue.add(nhiệm vụ);
     // 6. Nếu phần tử trên cùng trong hàng đợi là tác vụ hiện tại, hãy đánh thức hàng đợi để `timerthread` có thể lên lịch cho tác vụ
     nếu như (queue.getmin() == nhiệm vụ)
       queue. thông báo();
   }
}

Phương thức sched() trải qua các bước sau:

  1. Kiểm tra xem thời gian không thể là số âm
  2. chu kỳ không thể vượt quá long.max_value >> 1
  3. Khi bộ hẹn giờ bị hủy, nó không thể được lên lịch
  4. Khóa tác vụ, sau đó đặt thời gian thực hiện tiếp theo, chu kỳ thực hiện và trạng thái tác vụ của tác vụ để đảm bảo rằng việc lập lịch tác vụ và hủy tác vụ là an toàn cho luồng
  5. Thêm tác vụ vào hàng đợi
  6. Nếu phần tử trên cùng trong hàng đợi là tác vụ hiện tại, hàng đợi sẽ được đánh thức để timerthread có thể lên lịch cho tác vụ.

【Lưu ý】:Chúng ta cần đặc biệt chú ý đến điểm 6. Tại sao hàng đợi chỉ thức dậy khi phần tử trên cùng của ngăn xếp là tác vụ hiện tại? Lý do nằm ở ý nghĩa của phần tử trên cùng của heap, nghĩa là: phần tử trên cùng của heap biểu thị tác vụ được thực hiện gần nhất với thời điểm hiện tại! .

[Ví dụ 1]: Nếu thời gian hiện tại là 1 giây, có một tác vụ a trong hàng đợi cần được thực hiện trong 3 giây và tác vụ b mới được thêm vào của chúng ta cần được thực hiện trong 5 giây. Lúc này, vì timerthread có hoạt động wait(timeout) nên nó sẽ tự động thức dậy khi hết thời gian. Do đó, vì lý do hiệu suất, không cần phải đánh thức trong quá trình thực hiện sched().

[Ví dụ 2]: Nếu thời gian hiện tại là 1 giây, có một tác vụ a trong hàng đợi cần được thực hiện trong 3 giây và tác vụ b mới được thêm vào của chúng ta cần được thực hiện trong 2 giây. Lúc này, nếu thao tác đánh thức không được thực hiện trong sched(), tác vụ a sẽ được thực thi trong 3 giây. Tuy nhiên, vì tác vụ B cần được thực hiện trong 2 giây nên thời gian thực hiện đã trôi qua, do đó xảy ra sự cố.

Sau khi phân tích phương thức lập lịch tác vụ sched(), chúng tôi tiếp tục phân tích các phương thức khác. Trước tiên chúng ta hãy xem cancel(), được sử dụng để hủy việc thực hiện bộ đếm thời gian.

?
1
2
3
4
5
6
7
công cộng vô hiệu Hủy bỏ() {
   đồng bộ (hàng đợi) {
     thread.newtasksmaybescheduled = SAI ;
     queue.clear();
     queue. thông báo(); // trong trường hợp hàng đợi đã trống.
   }
}

Từ phân tích mã nguồn ở trên, phương pháp này thực hiện những điều sau:

  1. Đặt cờ newtasksmaybescheduled của timerthread thành false
  2. Xóa hàng đợi
  3. Hàng đợi đánh thức

Đôi khi, có thể có nhiều tác vụ hẹn giờ trong một bộ hẹn giờ. Nếu chúng ta chỉ hủy một số timertask, thay vì tất cả, thì ngoài việc gọi phương thức cancel() trên timertask, chúng ta cũng cần phải dọn dẹp timer. Phương pháp dọn dẹp ở đây là purge(). Chúng ta hãy xem xét logic triển khai của nó.

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
công cộng số nguyên thanh lọc() {
    số nguyên kết quả = 0 ;
 
    đồng bộ (hàng đợi) {
      // 1. Duyệt qua tất cả các tác vụ. Nếu tác vụ bị hủy, hãy xóa tác vụ đó khỏi hàng đợi và tăng số lượng tác vụ bị xóa thêm một.
      ( số nguyên i = hàng đợi.kích thước(); i > 0 ; Tôi--) {
        nếu như (queue.get(i).state == timertask.cancelled) {
          queue.quickremove(i);
          kết quả++;
        }
      }
      // 2. Định hình lại hàng đợi thành một đống nhỏ nhất
      nếu như (kết quả != 0 )
        queue.heapify();
    }
 
    trở lại kết quả;
  }

5. Phương pháp đánh thức hàng đợi.

Thông qua phân tích mã nguồn trước đó, chúng ta có thể thấy rằng chức năng đánh thức hàng đợi tồn tại ở những nơi sau:

  1. timer. hủy()
  2. timer.sched()
  3. timer.threadreaper.finalize()

Điểm thứ nhất và thứ hai đã được phân tích. Bây giờ chúng ta hãy xem xét điểm thứ ba.

?
1
2
3
4
5
6
7
8
riêng tư cuối cùng đối tượng threadreaper = mới sự vật() {
   được bảo vệ vô hiệu hoàn thiện() ném có thể ném được
     đồng bộ (hàng đợi) {
       thread.newtasksmaybescheduled = SAI ;
       queue. thông báo(); // trong trường hợp hàng đợi trống.
     }
   }
};

Phương pháp này được sử dụng để đánh thức hàng đợi tác vụ trong giai đoạn gc, giai đoạn mà người đọc thường quên.

Vậy, hãy cùng quay lại và suy nghĩ xem tại sao chúng ta cần đoạn mã này?

Khi chúng ta phân tích timerthread, chúng ta thấy rằng nếu bộ đếm thời gian không được lên lịch sau khi được tạo, nó sẽ tiếp tục chờ và rơi vào trạng thái tạm dừng. Để tránh tình huống này, chuyên gia về xử lý đồng thời Doug Lea đã khéo léo nghĩ ra cách đặt cờ trạng thái newtasksmaybescheduled trong finalize() và đánh thức hàng đợi tác vụ (queue.notify()) để giải cứu timerthread khỏi vòng lặp vô hạn.

Tóm tắt.

Đầu tiên, bài viết này trình bày cách sử dụng bộ đếm thời gian, sau đó phân tích sự khác biệt và mối liên hệ giữa các phương thức lập lịch schedule() và scheduleFixedRate().

Sau đó, để hiểu sâu hơn về bộ đếm thời gian, chúng tôi đã tiến hành phân tích chuyên sâu bằng cách đọc mã nguồn. Có thể thấy rằng cách triển khai bên trong của nó rất thông minh và được cân nhắc kỹ lưỡng.

Tuy nhiên, đặc điểm thực thi tuần tự của bộ đếm thời gian hạn chế ứng dụng của nó trong môi trường đồng thời cao. Sau đó chúng ta sẽ phân tích sâu hơn về cách lập lịch tác vụ được triển khai trong môi trường phân tán, đồng thời cao. Chúng ta hãy cùng chờ xem~.

Trên đây là toàn bộ nội dung bài viết này, hy vọng sẽ giúp ích cho việc học tập của mọi người, và mong các bạn ủng hộ tôi.

Liên kết gốc: https://www.jianshu.com/p/f4c195840159.

Cuối cùng, bài viết này về hiểu biết sâu sắc về cơ chế lập lịch thời gian Java (Timer) đã kết thúc tại đây. Nếu bạn muốn biết thêm về hiểu biết sâu sắc về cơ chế lập lịch thời gian Java (Timer), vui lòng tìm kiếm các bài viết CFSDN hoặc tiếp tục duyệt các bài viết liên quan. Tôi hy vọng bạn sẽ ủng hộ blog của tôi trong tương lai! .

27 4 0
qq735679552
Hồ sơ cá nhân

Tôi là một lập trình viên xuất sắc, rất giỏi!

Nhận phiếu giảm giá Didi Taxi miễn phí
Mã giảm giá Didi Taxi
Giấy chứng nhận ICP Bắc Kinh số 000000
Hợp tác quảng cáo: 1813099741@qq.com 6ren.com