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

Mộ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.

RTSJ đả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ý sự kiện không đồng bộ
  • Ngắt không đồng bộ các xử lí

Các xử lí thời gian thực

RTSJ định nghĩa javax.realtime.RealtimeThread — là một lớp con của lớp chuẩn java.lang.Thread. Trên chính nó, RealtimeThread tạo ra một số đặc tính tiên tiến của đặc tả. Thí dụ các xử lí thời gian thực là chủ thể của bộ lập lịch xử lí thời gian thực. Bộ lập lịch đảm bảo một phạm vi duy nhất các quyền ưu tiên lập lịch và có thể thực hiện chính sách lập lịch thời gian thực vào trước – ra trước (đảm bảo các 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

Liệ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 số xử lí có thể là xử lí thời gian thực.

Liệt kê 1. Gán lớp xử lí theo quyền ưu tiên

import javax.realtime.PriorityScheduler;
import javax.realtime.RealtimeThread;
import javax.realtime.Scheduler;

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());
}
}

class ThreadAssigner {
static Thread assignThread(int priority, Runnable runnable) {
Thread thread = null;
if(priority <= Thread.MAX_PRIORITY) { thread = new Thread(runnable); } else { try { thread = RTThreadAssigner.assignRTThread(priority, runnable); } catch(LinkageError e) {} if(thread == null) { priority = Thread.MAX_PRIORITY; thread = new Thread(runnable); } } thread.setPriority(priority); return thread; } } class RTThreadAssigner { 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);
}
return null;
}
 

}

Mã trong Liệt kê 1 phải được biên dịch bằng các lớp RTSJ. Vào thời gian chạy, nếu không tìm thấy các lớp thời gian thực, mã sẽ nắm bắt được LinkageError bị máy ảo loại bỏ và tạo các đối tượng xử lí Java thông thường thay cho các xử lí thời gian thực. Việc này cho phép mã chạy trên bất kỳ máy ảo nào, dù có thời gian thực hay không.

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.

Liệ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());
}
}

class ThreadAssigner {
static Thread assignThread(int priority, Runnable runnable) {
Thread thread = null;
try {
thread = assignThread(priority <= Thread.MAX_PRIORITY, runnable); } catch(InvocationTargetException e) { } catch(IllegalAccessException e) { } catch(InstantiationException e) { } catch(ClassNotFoundException e) { } if(thread == null) { 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, InstantiationException, ClassNotFoundException { Thread thread = assignThread( regular ? "java.lang.Thread" : "javax.realtime.RealtimeThread", runnable); return thread; } static Thread assignThread(String className, Runnable runnable) throws InvocationTargetException, IllegalAccessException, InstantiationException, ClassNotFoundException { Class clazz = Class.forName(className); Constructor selectedConstructor = null; Constructor constructors[] = clazz.getConstructors(); top: for(Constructor constructor : constructors) { Class parameterTypes[] = constructor.getParameterTypes(); int parameterTypesLength = parameterTypes.length; if(parameterTypesLength == 0) { continue; } Class lastParameter = parameterTypes[parameterTypesLength - 1]; if(lastParameter.equals(Runnable.class)) { for(Class parameter : parameterTypes) { if(parameter.isPrimitive()) { continue top; } } if(selectedConstructor == null || selectedConstructor.getParameterTypes().length > parameterTypesLength) {
selectedConstructor = constructor;
}
}
}
if(selectedConstructor == null) {
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);
}
}

Bộ 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;
thread.start();
}

public void run() {
System.out.println("Running " + Thread.currentThread());
}
}

class ThreadContainer extends ThreadContainerBase {
ThreadContainer(int priority, Runnable runnable) {
super(new Thread(runnable));
if(priority > Thread.MAX_PRIORITY) {
priority = Thread.MAX_PRIORITY;
}
thread.setPriority(priority);
}
}

class ThreadContainerBase {
final Thread thread;

ThreadContainerBase(Thread thread) {
this.thread = thread;
}
}

Để kích hoạt các xử lí thời gian thực, bạn có thể thay đổi mã ThreadContainer như trình bày trong Liệt kê 4:

Liệ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);
}
return new Thread(runnable);
}
}

Bạn có thể gộp vào tệp lớp ThreadContainer vừa được biên dịch này trong ứng dụng của bạn chứ không phải tệp cũ khi chạy nó bằng một JVM thời gian thực.

Các vùng nhớ tách biệt

Phổ biến cho tất cả các JVM, gồm cả các JVM thời gian thực, là đống rác được thu gom (garbage-collected heap). JVM phục hồi lại bộ nhớ từ đống qua việc gom rác. Các JVM thời gian thực có các thuật toán thu gom rác được thiết kế riêng để tránh hoặc giảm thiểu can thiệp vào ứng dụng đang chạy.

RTSJ đư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 để 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:

Hì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
Hì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 vùng nhớ

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 giố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.

Các tuỳ chọn để lập lịch mã nhạy thời gian

Khi bạn sử dụng các vùng nhớ heap khác, bạn có thể chọn dùng javax.realtime.NoHeapRealtimeThread (NHRT – xử lí thời gian thực không có đống), một lớp con của javax.realtime.RealtimeThread, cho phép các xử lí mà được đảm bảo chạy không bị nhiễu từ bộ gom rác. Chúng có thể chạy mà không bị nhiễu vì chúng bị hạn chế truy cập bất kỳ đối tượng nào được phân bổ từ đống. Bất kỳ cố gắng vi phạm nào đến hạn chế truy cập này đều làm cho một 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ã:

Hình 2. Các tuỳ chọn minh hoạ sơ đồ lớp để lập lịch mã

Các tuỳ chọn lập lịch
Các tuỳ chọn lập lịch

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
 

Gửi đi không đồng bộ
Gửi đi không đồng bộ

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 trong một cài đặt của java.lang.Runnable, thì một số tuỳ chọn là có thể để gửi đi bộ mã đó. Bạn có thể chọn xây dựng nên một xử lí để khai thác bộ mã, hoặc sử dụng các bộ xử lý sự kiện không đồng bộ mà tận dụng các vùng đệm của các xử lí để thực hiện bộ mã theo yêu cầu, hoặc sử dụng các kết hợp của hai cái trên.

Bảng 1 trình bày một phân tích tổng quát về các đặc tính của các lựa chọn khả dĩ khác nhau:

Bảng 1. So sánh các phương thức gửi đi bộ mã trong Java thời gian thực

 

 Chia sẻ các xử lí để thực hiện bộ mãCó thể được gửi đi theo định kỳCó thể chạy trong bộ nhớ đốngCó thể chạy trong bộ nhớ bất tửCó thể chạy trong bộ nhớ được khoanh vùngCó thể được chỉ định một thời hạn chótSẽ chạy mà không bị nhiễu từ việc gom rác
Thread thông thườngKhôngKhôngKhôngKhôngKhông
RealtimeThreadKhôngKhông
NoHeapRealtimeThreadKhôngKhông
AsyncEventHandlerCó, khi được kèm theo một bộ định thời định kỳKhông
BoundAsyncEventHandlerKhôngCó, khi được kèm theo một bộ định thời định kỳKhông
Không
Không-đống AsyncEventHandlerCó, khi được kèm theo một bộ định thời định kỳKhông
Không-đống BoundAsyncEventHandlerKhôngCó, khi được kèm theo một bộ định thời định kỳKhông

Vài vấn đề về thiết kế duy nhất đối với Java thời gian thực gây ảnh hưởng khi bạn đang cân nhắc sử dụng các tuỳ chọn lập lịch và vùng nhớ. Việc lập trình đối với các môi trường thời gian thực nói chung là một nhiệm vụ thách thức hơn lập trình các ứng dụng truyền thống đơn giản, và Java thời gian thực đưa ra các thách thức của chính nó. Bảng 2 liệt kê một số phức tạp mà có thể nảy sinh khi sử dụng vùng nhớ bổ sung, các NHRT, và các đặc tính thời gian thực khác:

Bảng 2. Một số phức tạp và khó khăn không lường trước của xử lí và vùng nhớ thời gian thực

 

Xem xétChi tiết
Bộ nhớ phân bổ đến một vùng nhớMỗi vùng nhớ được tạo ra bởi một ứng dụng được phân bổ với một
kích thước yêu cầu. Chọn một kích thước quá lớn là cách sử dụng bộ
nhớ không hiệu quả, nhưng việc chọn một kích thước quá nhỏ có thể
làm ứng dụng dễ bị tổn hại đối với OutOfMemoryError (lỗi hết bộ nhớ). Trong khi phát
triển, thậm chí khi một ứng dụng không thay đổi, các thư viện ẩn
có thể thay đổi. Việc này có thể tạo ra việc sử dụng bộ nhớ bổ
sung không mong muốn, làm cho giới hạn vùng nhớ bị vượt quá. .
Các cân nhắc về định thời đối với các vùng được chia sẻ

Một vùng nhớ được khoanh vùng được chia sẻ bởi một số xử lí có
thể xuất hiện để có đủ kích thước vì nó chờ được thu dọn khi
không có xử lí nào sử dụng nó. Tuy nhiên, với các thay đổi
tinh vi trong việc định thời của các xử lí bằng cách sử dụng
vùng, có thể không bao giờ có lúc vùng đó không được sử dụng
khi ngữ cảnh cấp phát cho bất kỳ xử lí nào. Việc này tạo ra
khả năng bất ngờ là nó sẽ không bao giờ được thu dọn, gây ra
một lỗi OutOfMemoryError.

Các tranh chấp về khóa chặn tạm thời giữa các xử lí có thể xảy
ra khi chia sẻ các vùng được khoanh vùng được chia sẻ nhập vào
và được thu dọn.

Các ngoại lệ thời gian chạy IllegalAssignmentError, MemoryAccessError, và IllegalThreadStateExceptionCác ngoại lệ này có thể có kết quả nếu không chú ý đầy đủ đến
thiết kế mã. Trên thực tế, các thay đổi tinh vi trong hành vi và
định thời chương trình có thể làm cho chúng xuất hiện ra một cách
đột ngột. Một số ví dụ:
  • Một đối tượng từ đống mà thông thường không sẵn có để dùng
    cho một NHRT có thể trở nên sẵn có vì các thay đổi trong
    việc định thời và đồng bộ hoá giữa các xử lí.
  • Một lỗi IllegalAssignmentError
    (lỗi chỉ định không hợp lệ) có thể được đưa vào khi không
    biết một đối tượng được phân bổ từ vùng nhớ nào, hoặc nơi
    nào mà một vùng riêng được định vị trên ngăn xếp
    vùng.
  • IllegalThreadStateException
    được loại bỏ khi bộ mã nhập vào các các vùng nhớ được
    khoanh vùng được chạy bởi các xử lí thông thường.
  • Bộ mã tạo ra sự sử dụng phổ biến các trường tĩnh hay
    phương tiện khác để nhớ nhanh dữ liệu là không an toàn đối
    với các vùng vì các qui tắc gán, mà có thể gây ra một lỗi
    IllegalAssignmentError.
Khởi tạo lớpBất kỳ kiểu xử lí thời gian thực hoặc thông thường nào đều có thể
khởi tạo một lớp, bao gồm cả một NHRT, có thể gây ra bất một lỗi
không mong đợi MemoryAccessError.
Việc hoàn tất của các đối tượng với phương thức finalizeXử lí cuối cùng để thoát ra một vùng được sử dụng để hoàn thành
tất cả các đối tượng bên trong:
  • Nếu các phương thức finalize
    tạo ra các xử lí, các vùng không thể thu dọn được như mong
    đợi.
  • Việc hoàn tất cũng có thể đưa vào các khoá chết. Trước khi
    hoàn tất vùng nhớ, xử lí đang hoàn tất có thể đã thu được
    các khoá. Tranh chấp đối với các khoá chặn này từ các xử
    lí khác, và cũng là các khoá sẽ có được trong khi hoàn
    tất, có thể xảy ra và có thể tạo ra các khoá chết.
Các cản trở NHRT không mong đợiCác NHRT, mặc dù được đảm bảo để chạy không có nhiễu trực tiếp từ
việc gom bộ nhớ rác, có thể chia sẻ các khoá với các kiểu xử lí
khác nhau có thể có trước bằng việc gom bộ nhớ rác. Nếu NHRT bị
ngăn cản khi đang cố gắng giành một khoá như vậy trong khi xử lí
đang sở hữu khoá đó bị làm trở ngại do việc gom bộ nhớ rác, thì
NHRT này cũng bị cản trở gián tiếp do việc gom bộ nhớ rác.


Một thí dụ tổng hợp

Thí dụ tiếp theo bao hàm một số đặc tính thời gian thực đã được mô tả. Để bắt đầu, Liệt kê 5 trình bày hai lớp mô tả một tác nhân tạo ra (producer) dữ liệu sự kiện và một tác nhân tiêu thụ (consumer). Cả hai lớp là các cài đặt của Runnable (có thể chạy được) để chúng có thể dễ dàng thực thi bởi bất kỳ đối tượng Schedulable (có thể lập lịch) nào cho trước.

Liệt kê 5. Các lớp tác nhân tạo ra và tác nhân tiêu thụ đối với các đối tượng sự kiện

class Producer implements Runnable {
    volatile int eventIdentifier;
    final Thread listener;

    Producer(Thread listener) {
        this.listener = listener;
    }

    public void run() {
        LinkedList<Integer> events = getEvents();
        synchronized(listener) {
            listener.notify();
            events.add(++eventIdentifier); //autoboxing creates an Integer object here
        }
    }

    static LinkedList<Integer> getEvents() {
        ScopedMemory memoryArea = (ScopedMemory) RealtimeThread.getCurrentMemoryArea();
        LinkedList<Integer> events =
            (LinkedList<Integer>) memoryArea.getPortal();
        if(events == null) {
            synchronized(memoryArea) {
                if(events == null) {
                    events = new LinkedList<Integer>();
                    memoryArea.setPortal(events);
                }
            }
        }
        return events;
    }
}

class Consumer implements Runnable {
    boolean setConsuming = true;
    volatile boolean isConsuming;

    public void run() {
        Thread currentThread = Thread.currentThread();
        isConsuming = true;
        try {
            LinkedList<Integer> events = Producer.getEvents();
            int lastEventConsumed = 0;
            synchronized(currentThread) {
                while(setConsuming) {
                    while(lastEventConsumed < events.size()) {
                        System.out.print(events.get(lastEventConsumed++) + " ");
                    }
                    currentThread.wait();
                }
            }
        } catch(InterruptedException e) {
        } finally {
            isConsuming = false;
        }
    }
}

Trong Liệt kê 5, các đối tượng tác nhân tạo ra và tác nhân tiêu thụ truy cập đến một hàng đợi của các sự kiện mà được mã hóa như một chuỗi các đối tượng java.lang.Integer. Bộ mã này chờ đợi ngữ cảnh cấp phát hiện tại trở thành một vùng nhớ được khoanh vùng và chờ các hàng đợi của các sự kiện để được lưu giữ như là đối tượng cổng Web (portal object) của vùng đó. (Cổng Web là một đối tượng được phân bổ từ vùng mà có thể được lưu giữ trong chính đối tượng vùng nhớ được khoanh vùng, một sự tiện lợi hữu ích vì các đối tượng được khoanh vùng không thể được lưu trong các trường tĩnh hoặc trong các đối tượng được phân bổ từ một vùng cha.) Nếu không tìm thấy hàng đợi, nó sẽ được tạo ra. Một cặp trường cấp phát nhanh được dùng để thông báo cho các xử lí quan tâm về tiến độ sản xuất và tiêu thụ các sự kiện.

Hai lớp trong Liệt kê 6 trình bày cách mã trong Liệt kê 5 có thể được thực thi:

Liệt kê 6. Các lớp có thể lập lịch

class NoHeapHandler extends AsyncEventHandler {
    final MemoryArea sharedArea;
    final Producer producer;

    NoHeapHandler(
            PriorityScheduler scheduler,
            ScopedMemory sharedArea,
            Producer producer) {
        super(new PriorityParameters(scheduler.getMaxPriority()),
                null, null, null, null, true);
        this.sharedArea = sharedArea;
        this.producer = producer;
    }

    public void handleAsyncEvent() {
        sharedArea.enter(producer);
    }
}

class NoHeapThread extends NoHeapRealtimeThread {
    boolean terminate;
    final MemoryArea sharedArea;
    final Consumer consumer;

    NoHeapThread(
            PriorityScheduler scheduler,
            ScopedMemory sharedArea,
            Consumer consumer) {
        super(new PriorityParameters(scheduler.getNormPriority()),
            RealtimeThread.getCurrentMemoryArea());
        this.sharedArea = sharedArea;
        this.consumer = consumer;
    }

    public synchronized void run() {
        try {
            while(true) {
                if(consumer.setConsuming) {
                    sharedArea.enter(consumer);
                } else {
                    synchronized(this) {
                        if(!terminate) {
                            if(!consumer.setConsuming) {
                                wait();
                            }
                        } else {
                            break;
                        }
                    }
                }
            }
        } catch(InterruptedException e) {}
    }
}

Trong Liệt kê 6, bộ mã tác nhân tạo dữ liệu (data-producer) được chỉ định làm một bộ xử lý sự kiện không đồng bộ, sẽ được chạy ở quyền ưu tiên cao nhất đang có. Bộ xử lý này chỉ cần nhập vào một vùng nhớ được khoanh vùng để chạy bộ mã tác nhân tạo ra. Cũng vùng nhớ được khoanh vùng như vậy là một tham số cho một lớp NHRT mà hoạt động như tác nhân tiêu thụ của dữ liệu. Lớp xử lí cũng như vậy, cho phép truy cập đồng bộ đến các trường terminate và setConsuming cho hành vi ra lệnh. Khi xử lí tác nhân tiêu thụ đang tiêu thụ các sự kiện, nó nhập vào vùng nhớ được chia sẻ để thực thi mã tác nhân tiêu thụ, chạy với quyền ưu tiên thấp hơn tác nhân tạo ra. (Hành vi tiêu thụ trong thí dụ này là không quan trọng, chỉ đơn giản là in bộ định danh sự kiện ra bàn điều khiển giao diện.)

Liệt kê 7 trình bày việc bộ mã khởi tạo hệ thống và thể hiện hành vi hệ thống:

Liệt kê 7. Hành vi hệ thống

public class EventSystem implements Runnable {
    public static void main(String args[]) throws InterruptedException {
        RealtimeThread systemThread = new RealtimeThread(
                null, null, null, new VTMemory(20000L), null, null) {
            public void run() {
                VTMemory systemArea = new VTMemory(20000L, new EventSystem());
                systemArea.enter();
            }
        };
        systemThread.start();
    }

    public void run() {
        try {
            PriorityScheduler scheduler =
                (PriorityScheduler) Scheduler.getDefaultScheduler();
            VTMemory scopedArea = new VTMemory(20000L);
            Consumer consumer = new Consumer();
            NoHeapThread thread = new NoHeapThread(scheduler, scopedArea, consumer);
            Producer producer = new Producer(thread);
            NoHeapHandler handler = new NoHeapHandler(scheduler, scopedArea, producer);
            AsyncEvent event = new AsyncEvent();
            event.addHandler(handler);

            int handlerPriority =
                ((PriorityParameters) handler.getSchedulingParameters()).getPriority();
            RealtimeThread.currentRealtimeThread().setPriority(handlerPriority - 1);

            thread.start();
            waitForConsumer(consumer);

            //fire several events while there is a consumer
            event.fire();
            event.fire();
            event.fire();
            waitForEvent(producer, 3);

            setConsuming(thread, false);

            //fire a couple of events while there is no consumer
            event.fire();
            event.fire();

            waitForEvent(producer, 5);

            setConsuming(thread, true);
            waitForConsumer(consumer);

            //fire another event while there is a consumer
            event.fire();
            waitForEvent(producer, 6);

            synchronized(thread) {
                thread.terminate = true;
                setConsuming(thread, false);
            }

        } catch(InterruptedException e) {}
    }

    private void setConsuming(NoHeapThread thread, boolean enabled) {
        synchronized(thread) {
            thread.consumer.setConsuming = enabled;
            thread.notify();
        }
    }

    private void waitForEvent(Producer producer, int eventNumber)
            throws InterruptedException {
        while(producer.eventIdentifier < eventNumber) {
            Thread.sleep(100);
        }
    }

    private void waitForConsumer(Consumer consumer)
            throws InterruptedException {
        while(!consumer.isConsuming) {
            Thread.sleep(100);
        }
    }
}

Trong Liệt kê 7, một cặp vùng được sử dụng làm cơ sở cho ngăn xếp vùng đối với xử lí và bộ xử lý không-đống, một yêu cầu vì các lớp Schedulable này không thể truy cập bất kỳ đối tượng nào mà được phân bổ đống (heap-allocated). Một đối tượng sự kiện không đồng bộ đại diện cho sự kiện, với bộ xử lý kèm theo để được gửi đi khi sự kiện được thải bỏ. Khi hệ thống được khởi tạo, mã này khởi động xử lí tác nhân tiêu thụ và thải bỏ sự kiện vài lần, chạy với quyền ưu tiên chỉ thấp hơn quyền ưu tiên của bộ xử lý sự kiện. Bộ mã cũng tắt và bật xử lí tác nhân tiêu thụ trong khi bổ sung các sự kiện bị thải bỏ.

Liệt kê 8 hiển thị đầu ra khi EventSystem (Hệ thống Sự kiện) chạy trong một JVM thời gian thực:

Liệt kê 8. Xuất qua bàn điều khiển

1 2 3 6

Một khía cạnh thú vị của thí dụ này là lý do các sự kiện 4 và 5 không được báo cáo. Mỗi khi xử lí nghe (listening thread) báo cáo về các sự kiện trong hàng đợi, nó khởi động từ phía trước hàng đợi và đi đến phần cuối, đề nghị rằng tất cả 6 sự kiện sẽ được báo cáo ít nhất là một lần.

Tuy nhiên, thiết kế này đảm bảo rằng bộ nhớ được sử dụng để lưu các sự kiện được tự động loại bỏ khi không có xử lí nào tiêu thụ chúng. Khi một xử lí tác nhân tiêu thụ ngừng việc đọc từ hàng đợi, nó thoát ra vùng nhớ được khoanh vùng, vào lúc mà không có đối tượng Schedulable nào sử dụng vùng này làm ngữ cảnh cấp phát.

Sự vắng mặt của các đối tượng Schedulable bằng cách sử dụng vùng này có nghĩa là vùng được khoanh vùng không còn đối tượng nào và được cài đặt lại. Việc này gồm cả đối tượng cổng Web, cho nên hàng đợi và tất cả các sự kiện trong nó được loại bỏ khi xử lí ngừng việc nghe. Mỗi khi một sự kiện tiếp sau bị thải bỏ, hàng đợi sẽ được tạo lại và được đưa dữ liệu vào lại, nhưng không có xử lí nghe, bộ nhớ được loại bỏ ngay lập tức sau đó.

Việc quản lý bộ nhớ là tự động và chạy không bị bộ gom rác can thiệp, nếu bộ gom này là đang hoạt động (vì cả bộ xử lý và xử lí đều là không-đống.) Các sự kiện được lưu lại làm một hàng đợi của các đối tượng trong bộ nhớ, tiếp tục phát triển nếu một xử lí nghe sẵn có để tiêu thụ chúng. Nếu không sẵn có, hàng đợi và các sự kiện liên quan tự động được loại bỏ.

Một kịch bản sử dụng tổng quát

Với việc lập lịch và khung làm việc quản lý bộ nhớ, bạn có thể thiết kế ra một ứng dụng với các mức ưu tiên khác nhau cho các xử lí để thực hiện một cách tối ưu trong một máy ảo thời gian thực (và có đầy đủ khả năng trong các máy ảo khác). Ứng dụng có thể bao gồm các xử lí quản lí-sự kiện của mức ưu tiên cao, thu thập dữ liệu từ các đầu vào bên ngoài và lưu lại dữ liệu để xử lý. Do tính nhất thời và không đồng bộ của chúng, các xử lí quản lí-sự kiện này có lẽ thích hợp đối với việc quản lý bộ nhớ xen kẽ, và chúng có lẽ phụ thuộc nặng nhất vào các ràng buộc thời gian thực. Ở mức ưu tiên trung bình hẳn đang xử lý các xử lí mà tiêu thụ dữ liệu và tạo ra các tính toán, hoặc phân phối dữ liệu. Các xử lí mức trung bình có thể đòi hỏi áp dụng bộ xử lý trung tâm được phân bổ đầy đủ để quản lý các tải làm việc của chúng. Ở các mức ưu tiên thấp nhất, có thể có các xử lí duy trì và ghi nhật ký. Việc sử dụng một máy ảo thời gian thực để quản lý lập lịch và sử dụng bộ nhớ của các tác vụ khác nhau này trong ứng dụng có thể cho phép nó chạy hiệu quả nhất.

Dự định của RTSJ là cho phép các nhà phát triển viết ra các ứng dụng chạy trong các ràng buộc thời gian thực được yêu cầu. Chỉ cần sử dụng bộ lập lịch và các xử lí thời gian thực là có thể đủ thực hiện được mục tiêu đó. Nếu không, sự phát triển hiện đại hơn có thể là cần thiết để tận dụng một hoặc nhiều đặc tính hiện đại hơn do máy ảo thực hiện.

Kết luận Phần 1

Bài viết này đã phác thảo ra một số mẹo để bạn bắt đầu hoà nhập các phần tử của Java thời gian thực vào ứng dụng Java của bạn. Nó bao hàm một số đặc điểm lập lịch và quản lý bộ nhớ mà bạn hẳn muốn sử dụng để thực hiện hiệu năng thời gian thực. Đây là một điểm xuất phát để bạn tận dụng các lợi ích truyền thống của ngôn ngữ Java, chẳng hạn như sự tương tác và an toàn, và kết hợp chúng với các đặc tính mới cho phép bạn thoả mãn các ràng buộc thời gian thực mà ứng dụng của bạn đòi hỏi.

Trong phần tiếp theo ở loạt bài này, bạn sẽ tìm hiểu các kỹ thuật để chuyển một ứng dụng hiện hành sang Java thời gian thực. Bài cuối sẽ xây dựng trên hai phần đầu và dẫn bạn qua việc thiết kế, xác thực, và gỡ lỗi một hệ thống thời gian thực mà hợp nhất với Java thời gian thực.

Nguồn: ibm.com