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í
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.
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
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ã
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 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ớ đống | Có thể chạy trong bộ nhớ bất tử | Có thể chạy trong bộ nhớ được khoanh vùng | Có thể được chỉ định một thời hạn chót | Sẽ chạy mà không bị nhiễu từ việc gom rác | |
|---|---|---|---|---|---|---|---|
Thread thông thường | Không | Không | Có | Có | Không | Không | Không |
RealtimeThread
| Không | Có | Có | Có | Có | Có | Không |
NoHeapRealtimeThread
| Không | Có | Không | Có | Có | Có | Có |
AsyncEventHandler
| Có | Có, khi được kèm theo một bộ định thời định kỳ | Có | Có | Có | Có | Không |
BoundAsyncEventHandler
| Không | Có, khi được kèm theo một bộ định thời định kỳ | Có | Có | Có | Có | Không |
Không-đống AsyncEventHandler | Có | Có, khi được kèm theo một bộ định thời định kỳ | Không | Có | Có | Có | Có |
Không-đống BoundAsyncEventHandler | Không | Có, khi được kèm theo một bộ định thời định kỳ | Không | Có | Có | Có | Có |
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ét | Chi 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 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à IllegalThreadStateException | Cá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ụ:
|
| Khởi tạo lớp | Bấ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 finalize | Xử 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:
|
| Các cản trở NHRT không mong đợi | Cá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. |
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.
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.
| Mô tả | Tên | Kích thước | Phương thức tải |
|---|---|---|---|
| Source code for the article examples | j-devrtj1.zip | 5KB | HTTP |
Học tập
-
Real-time Java: Đọc loạt bài developerWorks sáu phần
này về Java thời gian thực.
-
Đặc tả Thời gian thực dùng cho
Java: Bạn sẽ tìm thấy RTSJ tại trang web Quy trình Cộng đồng Java
(Java Community Process).
-
Gosling: Java Thời gian thực (Builder.com): Trong video này,
James Gosling gạt bỏ quan niệm rằng thời gian thực nghĩa là thực
nhanh.
-
Hiệu sách công nghệ: Duyệt tìm các sách về chủ đề này và các chủ
đề kỹ thuật khác.
-
Vùng công nghệ Java
developerWorks: Tìm hàng trăm bài viết về các khía cạnh về lập
trình Java.
Lấy sản phẩm và công nghệ
-
IBM®
WebSphere® Real Time: Tải về một sản phẩm dùng thử.
-
Bộ Tối ưu hoá Thực
thi Ứng dụng Thời gian Thực của IBM dùng cho Java (IBM Real Time
Application Execution Optimizer for Java): Tối ưu hóa và xác thực
ứng dụng Java thời gian thực của bạn đối với các môi trường đặc biệt bằng
công cụ này.
Thảo luận
- Tham gia vào cộng đồng My developerWorks.

Sean Foley là một nhà phát triển phần mềm cho IBM Ottawa Lab trong trung tâm công nghệ Java IBM. Sean đã có bằng Cử nhân Toán học của Đại học Tổng hợp Queen và một bằng Thạc sĩ Toán học của Đại học Tổng hợp Toronto, với nghiên cứu tốt nghiệp tập trung vào các bài toán tổ hợp trong lý thuyết thiết kế và lý thuyết đồ thị. Sau này, Sean đã phát triển các phần mềm dùng cho một số công ty trong viễn thông di động và các ngành công nghiệp xử lí nhúng. Sean đã gia nhập Nhóm Phần mềm IBM vào năm 2002 để phát triển các JVM nhúng và các sản phẩm hỗ trợ, như các công cụ dùng để thực hiện phân tích tĩnh và các tối ưu hoá về các chương trình Java được biên dịch. Gần đây hơn, ông đã trở thành một người đã góp phần chủ yếu vào việc cài đặt thư viện lớp thời gian thực trong sản phẩm Thời gian Thực WebSphere của IBM. Ông hiện là một nhà lãnh đạo kỹ thuật trong nhóm tiếp tục phát triển và cải thiện công nghệ Java thời gian thực