Bài này, bài đầu tiên trong loạt bài ba phần, mô tả một số đặc tính này và giải thích cách bạn có thể áp dụng chúng để đạt được hiệu năng thời gian thực trong các ứng dụng của chính mình
Trang 1Phát triển với Java thời gian thực, Phần 1: Khai thác các đặc tính độc nhất
của Java thời gian thực
Tận dụng hiệu năng Java thời gian thực trong ứng dụng của bạn
Sean C Foley, Phát triển phần mềm, IBM
Tóm tắt: Bộ Java thời gian thực (Real-time Java™) kết hợp dễ dàng việc lập
trình bằng ngôn ngữ Java theo hiệu năng do ứng dụng yêu cầu mà phải phù hợp với các ràng buộc thời gian thực Các phần mở rộng của ngôn ngữ Java đưa ra các đặc tính về môi trường thời gian thực mà đang thiếu trong môi trường thời gian chạy Java truyền thống Bài này, bài đầu tiên trong loạt bài ba phần, mô tả một số đặc tính này và giải thích cách bạn có thể áp dụng chúng để đạt được hiệu năng thời gian thực trong các ứng dụng của chính mình
Java thời gian thực là một bộ các tăng cường cho ngôn ngữ Java, cung cấp cho các ứng dụng một mức hiệu năng thời gian thực, vượt trội hiệu năng của công nghệ Java chuẩn Hiệu năng thời gian thực khác với hiệu năng thông lượng truyền thống, là một thước đo điển hình của tổng số các chỉ thị, tác vụ, hoặc công việc có thể được thực hiện trong khoảng thời gian ấn định Hiệu năng thời gian thực tập trung vào thời gian mà một ứng dụng yêu cầu để đáp ứng các kích thích bên ngoài
mà không vượt quá các ràng buộc thời gian cho trước Trong trường hợp của các
hệ thống thời gian thực cứng (hard real-time), các ràng buộc như vậy không bao giờ được vượt quá; các hệ thống thời gian thực mềm (soft real-time) có một dung
sai cao hơn đối với các vi phạm Hiệu năng thời gian thực đòi hỏi chính ứng dụng phải giành được quyền điều khiển của bộ xử lý sao cho nó có thể trả lời các kích thích, và trong khi trả lời các tác nhân kích thích đó thì bộ mã của ứng dụng không
bị khóa do thực hiện các quy trình tương tranh trong máy ảo đó Java thời gian thực đưa ra độ đáp ứng mà trước đây chưa được thoả mãn trong các ứng dụng Java
Một máy ảo Java (JVM) thời gian thực có thể tận dụng các dịch vụ hệ điều hành thời gian thực (RTOS) để cung cấp các khả năng thời gian thực cứng, hoặc nó có thể chạy trên nhiều hệ điều hành thông thường đối với các áp dụng có các ràng buộc thời gian thực mềm dẻo hơn Một số công nghệ sử dụng trong Java thời gian thực trở nên “miễn phí” khi bạn chuyển sang sử dụng máy ảo Java thời gian thực Nhưng để khai thác một số đặc tính của Java thời gian thực, cần phải có một số thay đổi về ứng dụng Các đặc tính này là trọng tâm của bài viết này
Các quy trình con phải bị ràng buộc
Trang 2Một JVM phục vụ một ứng dụng cho trước bằng cách thực hiện công việc mà ứng dụng đó chỉ điều khiển theo cách lỏng Một vài quy trình thời gian chạy con làm việc trong JVM, gồm:
Gom rác: Đây là công việc để phục hồi lại các khối nhớ thời gian chạy
(run-time memory) mà ứng dụng đã loại bỏ Việc gom rác có thể làm chậm việc thực thi ứng dụng trong một khoảng thời gian
Nạp lớp: Quy trình này — gọi như vậy vì các ứng dụng Java được nạp ở
mức chi tiết của các lớp, liên quan đến việc nạp các cấu trúc ứng dụng — các chỉ thị, và các tài nguyên khác từ hệ thống tệp hoặc mạng Trong Java
chuẩn, ứng dụng nạp từng lớp khi nó được tham chiếu lần đầu (nạp chậm)
Biên dịch động đúng thời (JIT dynamic compilation): Nhiều máy ảo sử
dụng việc biên dịch động của các phương thức từ ngôn ngữ máy của Java (Java bytecode) sang các chỉ thị máy riêng khi ứng dụng đang chạy Mặc dù việc này cải thiện được hiệu năng, hoạt động biên dịch tự nó có thể gây ra
sự trì hoãn tạm thời, khóa việc chạy mã ứng dụng
Lập lịch: Trong Java chuẩn, cho phép mức điều khiển tối thiểu để ứng
dụng ra lệnh cả việc lập lịch việc chạy các xử lí (threads) của chính mình lẫn lập lịch của ứng dụng tương quan với các ứng dụng khác đang chạy trên cùng hệ điều hành
Tất cả các quy trình con này có thể gây trở ngại đến khả năng phản hồi các tác nhân kích thích bên ngoài của một ứng dụng, vì chúng có thể làm chậm việc thực thi bộ mã ứng dụng Thí dụ một chuỗi chỉ thị hẳn có thể được lên lịch thực hiện để trả lời một tín hiệu từ mạng, hệ thống radar, bàn phím, hoặc bất kỳ thiết bị nào khác Một ứng dụng thời gian thực có một khoảng thời gian tối thiểu chấp nhận được trong đó một quy trình không liên quan đến, chẳng hạn như cho phép gom rác làm chậm việc thực hiện chuỗi chỉ thị trả lời
Java thời gian thực đưa ra các công nghệ đa dạng được thiết kế để giảm can thiệp đến ứng dụng khỏi các quy trình con ẩn này Các công nghệ “miễn phí” này xuất hiện khi bạn chuyển sang JVM thời gian thực bao gồm việc gom rác đặc biệt có hạn chế khoảng thời gian và tác động của các gián đoạn đối với việc thu gom, tải lớp đặc biệt mà cho phép hiệu năng được tối ưu hoá vào lúc khởi động, thay vì việc tối ưu hoá bị chậm, khoá và đồng bộ hoá đặc biệt, và lập lịch xử lí ưu tiên đặc biệt với việc tránh bị đảo ngược quyền ưu tiên Tuy nhiên, đòi hỏi một số thay đổi cho ứng dụng — cụ thể là khai thác các đặc tính do Đặc tả Thời gian Thực cho Java (RTSJ) đưa ra
Trang 3RTSJ đảm bảo một API có nhiều đặc tính thời gian thực trong các JVM Một số các đặc tính này có tính bắt buộc khi thực hiện đặc tả, số khác thì tuỳ ý Đặc tả bao hàm các lĩnh vực chung về:
Lập lịch thời gian thực
Quản lý nhớ nâng cao
Các bộ định thời gian phân giải cao
xử lí có quyền ưu tiên cao nhất được thực hiện không bị gián đoạn), cùng với việc
kế thừa quyền ưu tiên (một thuật toán tránh các xử lí quyền ưu tiên thấp hơn giữ
vô hạn một khoá mà một xử lí có quyền ưu tiên cao hơn đang yêu cầu và được
chạy không bị cản trở — tình huống này được xem như đảo quyền ưu tiên)
Bạn có thể xây dựng nên một cách rõ ràng các cá thể của RealtimeThread (xử lí thời gian thực) trong mã của bạn Nhưng cũng có thể thay đổi ứng dụng của bạn theo một cách tối thiểu để xử lí thời gian thực, nên tránh được sự cố gắng phát triển đáng kể và các chi phí liên quan Thể hiện sau đây là các ví dụ khác nhau về các cách để cho phép tạo xử lí thời gian thực ít can thiệp nhất và minh bạch nhất (Bạn có thể tải về mã nguồn cho toàn bộ các thí dụ trong bài viết.) Các kỹ thuật này cho phép một ứng dụng khai thác các xử lí thời gian thực với sự cố gắng tối thiểu và cho phép ứng dụng giữ được sự tương thích với các máy ảo chuẩn
Chỉ định kiểu xử lí theo quyền ưu tiên
Trang 4Liệt kê 1 trình bày một khối mã gán một xử lí thời gian thực hoặc xử lí thông thường với giá trị ưu tiên Nếu nó đang chạy trên một máy ảo thời gian thực, một
public class ThreadLogic implements Runnable {
static void startThread(int priority) {
Thread thread = ThreadAssigner.assignThread(
priority, new ThreadLogic());
Trang 5class ThreadAssigner {
static Thread assignThread(int priority, Runnable runnable) {
Thread thread = null;
static Thread assignRTThread(int priority, Runnable runnable) {
Scheduler defScheduler = Scheduler.getDefaultScheduler();
Trang 6PriorityScheduler scheduler = (PriorityScheduler) defScheduler;
if(priority >= scheduler.getMinPriority()) {
return new RealtimeThread(
null, null, null, null, null, runnable);
Trong Liệt kê 1, phương thức cung cấp các đối tượng RealtimeThread được tách riêng thành một lớp của chính nó Với cách này, phương thức không được xác thực cho đến khi lớp được nạp vào, điều được làm khi phương thức
assignRTThread được truy cập đầu tiên Khi lớp được nạp, bộ kiểm tra ngôn ngữ máy của máy ảo thời gian thực cố gắng kiểm tra lại lớp RealtimeThread có phải là một lớp con của lớp Thread, hay không nó thông báo thất bại với một lỗi
NoClassDefFoundError nếu không tìm ra các lớp thời gian thực
Chỉ định các xử lí nhờ phản chiếu
Liệt kê 2 trình bày một kỹ thuật thay thế có cùng hiệu quả như Liệt kê 1 Nó khởi động bằng một giá trị ưu tiên để xác định kiểu xử lí mong muốn, tạo ra hoặc là một xử lí thời gian thực hoặc một xử lí thông thường dựa trên lớp tên Mã phản chiếu trông chờ vào sự tồn tại của một hàm kiến thiết trong lớp mà lấy một cá thể của java.lang.Runnable làm đối số cuối cùng và chuyển giá trị rỗng cho tất cả đối
số khác
Trang 7Liệt kê 2 Sử dụng phản chiếu để chỉ định các xử lí
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
public class ThreadLogic implements Runnable {
static void startThread(int priority) {
Thread thread = ThreadAssigner.assignThread(
priority, new ThreadLogic());
thread.start();
}
public void run() {
System.out.println("Running " + Thread.currentThread()); }
Trang 8thread = assignThread(priority <= Thread.MAX_PRIORITY, runnable); } catch(InvocationTargetException e) {
thread = new Thread(runnable);
priority = Math.min(priority, Thread.MAX_PRIORITY);
}
thread.setPriority(priority);
return thread;
}
static Thread assignThread(boolean regular, Runnable runnable)
throws InvocationTargetException, IllegalAccessException,
Trang 9static Thread assignThread(String className, Runnable runnable) throws InvocationTargetException, IllegalAccessException, InstantiationException, ClassNotFoundException {
Class clazz = Class.forName(className);
Constructor selectedConstructor = null;
Constructor constructors[] = clazz.getConstructors();
Trang 10}
if(selectedConstructor == null ||
selectedConstructor.getParameterTypes().length > parameterTypesLength) {
throw new InstantiationException(
"no compatible constructor");
}
Class parameterTypes[] =
selectedConstructor.getParameterTypes();
int parameterTypesLength = parameterTypes.length;
Object arguments[] = new Object[parameterTypesLength]; arguments[parameterTypesLength - 1] = runnable;
return (Thread) selectedConstructor.newInstance(arguments); }
}
Trang 11Bộ mã trong Liệt kê 2 không cần biên dịch với các lớp thời gian thực trên
classpath (một biến môi trường để JVM và Java tìm ra các thư viện lớp), do các xử
lí thời gian thực được tạo ra bằng cách sử dụng phản chiếu Java
Chỉ định kiểu xử lí bằng sự kế thừa lớp
Ví dụ tiếp theo minh họa cách có thể xử lí thời gian thực của việc thay đổi sự kế thừa của một lớp cho trước Bạn có thể tạo ra hai phiên bản của một lớp xử lí cho trước, một phiên bản là sự nhận thức được javax.realtime.RealtimeThread và phiên bản kia không có Lựa chọn của bạn về cái này hay cái kia phụ thuộc vào JVM đang ẩn Bạn có thể cho phép một trong hai cái chỉ đơn giản bằng cách bao gồm tệp lớp tương ứng theo sắp xếp của bạn Với lựa chọn nào, thì mã cũng tương đối đơn giản và tránh được bất kỳ việc xử lý ngoại lệ nào, không giống như các ví
dụ trước đây Tuy nhiên, khi bạn phân bổ ứng dụng, bạn phải bao gồm 1 trong 2 lựa chọn lớp, tuỳ thuộc vào máy ảo được liên kết nào sẽ chạy ứng dụng
Mã trong Liệt kê 3 tạo ra các xử lí Java thông thường theo một cách chuẩn:
Liệt kê 3 Sử dụng kế thừa lớp để chỉ định các xử lí
import javax.realtime.PriorityScheduler;
import javax.realtime.RealtimeThread;
import javax.realtime.Scheduler;
public class ThreadLogic implements Runnable {
static void startThread(int priority) {
ThreadContainerBase base = new ThreadContainer(priority, new
ThreadLogic());
Thread thread = base.thread;
Trang 12thread.start();
}
public void run() {
System.out.println("Running " + Thread.currentThread()); }
}
class ThreadContainer extends ThreadContainerBase {
ThreadContainer(int priority, Runnable runnable) {
Trang 13Liệt kê 4 Một lớp thùng chứa xử lí khác để dùng thời gian thực
class ThreadContainer extends ThreadContainerBase {
ThreadContainer(int priority, Runnable runnable) {
super(assignRTThread(priority, runnable));
thread.setPriority(priority);
}
static Thread assignRTThread(int priority, Runnable runnable) {
Scheduler defScheduler = Scheduler.getDefaultScheduler();
PriorityScheduler scheduler = (PriorityScheduler) defScheduler;
if(priority >= scheduler.getMinPriority()) {
return new RealtimeThread(
null, null, null, null, null, runnable);
Trang 14RTSJ đưa ra khái niệm về một ngữ cảnh cấp phát cho mỗi xử lí, và nó đưa vào các
vùng nhớ bổ sung Khi một vùng nhớ dùng làm ngữ cảnh cấp phát cho một xử lí, tất cả các đối tượng được tạo ra bởi xử lí đó được phân bổ từ khu vực đó RTSJ quy định các vùng nhớ được tách riêng phụ sau:
Vùng nhớ đống đơn
Một vùng nhớ đống đơn bất tử (singleton immortal heap memory area), tức
bộ nhớ không bao giờ được sử dụng lại Việc khởi tạo xử lí một lớp sử dụng vùng này làm ngữ cảnh cấp phát khi chạy bộ khởi tạo tĩnh Mặc dù bộ nhớ bất tử không đòi hỏi sự chú ý từ bộ gom rác, việc sử dụng nó không bị hạn chế, vì bộ nhớ không thể phục hồi được
Các vùng nhớ được khoanh vùng (scopes) Các vùng không đòi hỏi sự hoạt
động từ việc gom rác, và bộ nhớ của chúng có thể được phục hồi — lập tức toàn bộ — để sử dụng lại Các đối tượng được phân bổ trong một vùng được hoàn chỉnh và dọn sạch, giải phóng bộ nhớ được phân bổ của chúng
Trang 15để sử dụng lại, khi máy ảo đã xác định rằng vùng đó không còn là vùng ngữ cảnh cấp phát cho bất kỳ xử lí sống nào nữa
Các vùng nhớ vật lý được định danh theo kiểu hoặc theo địa chỉ Bạn có thể
thiết kế từng vùng nhớ vật lý để sử dụng lại như một khu vực được khoanh vùng, hoặc để sử dụng đơn lẻ như một vùng bất tử Các vùng nhớ như vậy
có thể cung cấp việc truy cập bộ nhớ các đặc tính riêng hoặc từ các thiết bị riêng, chẳng hạn như bộ nhớ cực nhanh (flash memory) hoặc bộ nhớ dùng chung (shared memory)
Vùng đưa ra các hạn chế áp đặt về các tham chiếu đối tượng Khi một khối nhớ được khoanh vùng được giải phóng và các đối tượng bên trong được thu dọn, không một đối tượng nào với một quy chiếu đến khối nhớ được giải phóng có thể tồn tại, mà sẽ đưa đến kết quả là một dấu quy chiếu lơ lửng (dangling pointer) (một quy chiếu không định vị vào một địa chỉ nào) Việc này được thực hiện một phần bởi sự áp đặt của các qui tắc gán Các nguyên tắc này ra lệnh rằng các đối tượng được phân bổ từ các vùng nhớ chưa được khoanh vùng không thể trỏ đến các đối tượng được khoanh vùng Điều này đảm bảo rằng khi các đối tượng được khoanh vùng được giải phóng, các đối tượng từ các vùng nhớ khác không bị bỏ lại với các tham chiếu đến các đối tượng không tồn tại
Hình 1 minh hoạ các vùng nhớ và các qui tắc gán này:
Trang 16Hình 1 Các vùng nhớ và các qui tắc gán đối với các tham chiếu đối tượng
Các qui tắc gán cho phép các đối tượng trong một vùng trỏ đến vùng khác Tuy nhiên, điều này có nghĩa là phải có một chuỗi bị áp đặt của việc làm sạch vùng đối với mỗi xử lí, một chuỗi được duy trì bởi ngăn xếp (stack) trong mỗi xử lí Ngăn xếp này cũng bao gồm các tham chiếu đến các vùng nhớ khác mà đã được nhập vào ngoài các vùng Bất cứ khi nào một vùng nhớ trở thành ngữ cảnh cấp phát đối với một xử lí, nó đều được đặt lên đỉnh ngăn xếp vùng của xử lí Các qui tắc gán
ra lệnh rằng các đối tượng trong các vùng cao hơn trên vùng nhớ có thể quy chiếu đến đối tượng trong các vùng thấp hơn trên ngăn xếp, vì các vùng trên đỉnh được dọn sạch đầu tiên Các tham chiếu từ các vùng thấp hơn đến các vùng cao hơn bị cấm
Thứ tự của các vùng trên vùng nhớ cũng được phối hợp với thứ tự của các vùng trên vùng nhớ của các xử lí khác Khi một vùng đã được đặt trên vùng nhớ của bất
kỳ xử lí nào, vùng gần nhất bên dưới nó trên ngăn xếp được coi là cha (hoặc cha
được coi là vùng sơ khởi đơn lẻ (solitary primordial scope)), nếu không còn vùng
nào khác trên vùng nhớ Trong khi vùng đó duy trì được trên ngăn xếp đó, nó có thể được đặt lên vùng nhớ của bất kỳ xử lí nào khác chỉ khi cha duy trì được sự nhất quán, có nghĩa nó là vùng cao nhất trên ngăn xếp của xử lí khác Nói một cách khác, một vùng khi sử dụng chỉ có duy nhất một cha đơn lẻ Điều này đảm bảo rằng khi các vùng được giải phóng, việc thu dọn xảy ra theo một thứ tự dãy
Trang 17giống như vậy mà không để ý đến xử lí nào thực hiện việc thu dọn của mỗi vùng,
và các qui tắc gán giữ được sự nhất quán qua tất cả các xử lí
Cách khai thác các vùng nhớ tách riêng
Bạn có thể sử dụng một vùng nhớ riêng bằng cách quy định vùng này là vùng nhớ ban đầu đối với một xử lí để chạy vào (khi đối tượng xử lí được kiến thiết), hoặc bằng cách nhập vào vùng một cách rõ ràng, với điều kiện nó với một đối tượng Runnable sẽ được thực hiện bằng vùng làm vùng mặc định
Phải đặc biệt cân nhắc khi bạn sử dụng các vùng nhớ khác nhau, vì chúng mang theo những phức tạp và rủi ro có thể xảy ra Bạn phải chọn ra kích thước và số lượng các vùng Nếu các vùng đang được sử dụng, bạn phải cẩn thận khi thiết kế sắp xếp thứ tự của các ngăn xếp vùng của các xử lí, và phải duy trì nhận thức về các qui tắc gán
javax.realtime.MemoryAccessError (lỗi truy cập bộ nhớ java thời gian thực) bị loại bỏ
Một tuỳ chọn lập lịch khác là bộ xử lý sự kiện không đồng bộ (asynchronous event handler), mà bạn có thể sử dụng để lập lịch bộ mã sẽ được thực hiện để trả lời các
sự kiện không đồng bộ hoặc theo chu kỳ (Các sự kiện có thể là theo chu kỳ nếu chúng được khởi động bởi một bộ định thời.) Việc này cho phép bạn từ bỏ nhu cầu lập lịch các xử lí một cách rõ ràng đối với các sự kiện như vậy Thay vào đó, máy
ảo duy trì một vùng đệm (pool) các xử lí được chia sẻ và gửi đi để chạy mã của các bộ xử lý sự kiện không đồng bộ bất cứ khi nào sự kiện xảy ra Việc này có thể làm đơn giản hóa các ứng dụng thời gian thực, giải phóng bạn khỏi việc quản lý các xử lí và vùng nhớ
Sơ đồ lớp trong Hình 2 trình bày các tuỳ chọn sẵn có dùng vào việc lập lịch mã:
Trang 18Hình 2 Các tuỳ chọn minh hoạ sơ đồ lớp để lập lịch mã
Hình 3 hiển thị cách các bộ xử lý sự kiện không đồng bộ được gửi đi:
Hình 3 Cách các bộ xử lý sự kiện không đồng bộ được gửi đi
Nói chung, có thể là có lợi về tính khả chuyển và tính modun nếu tách riêng mã đáp ứng sự kiện từ bộ mã có thể kích hoạt và gửi đi bộ xử lý Khi bộ mã được gói