Tải bản đầy đủ (.docx) (19 trang)

Tiểu luận môn điện toán đám mây SONG SONG HÓA LỚP BÀI TOÁN DẠNG CHIA ĐỂ TRỊ BẰNG JAVA FORK JOIN FRAMEWORK

Bạn đang xem bản rút gọn của tài liệu. Xem và tải ngay bản đầy đủ của tài liệu tại đây (225.19 KB, 19 trang )

Tiểu luận môn học: Điện toán lưới và đám mây
ĐẠI HỌC QUỐC GIA THÀNH PHỐ HỒ CHÍ MINH
TRƯỜNG ĐẠI HỌC CÔNG NGHỆ THÔNG TIN

VÕ THÀNH NHÂN
SONG SONG HÓA LỚP BÀI TOÁN DẠNG CHIA ĐỂ TRỊ
BẰNG JAVA FORK/JOIN FRAMEWORK
MSHV: CH1301103
TIỂU LUẬN MÔN HỌC: Điện toán lưới và đám mây
GVHD: PGS.TS. Nguyễn Phi Khứ
TP. Hồ Chí Minh
Tháng 6 - 2014
1
Tiểu luận môn học: Điện toán lưới và đám mây
Mục lục
Lời mở đầu
Khi phần cứng máy tính ngày càng trở nên mạnh hơn, khi chip CPU của máy tính cá
nhân ngày càng nhỏ và chứa nhiều CPU hơn cùng khả năng tính toán mạnh mẽ hơn thì
một trong những đòi hỏi được đặt ra là các chương trình phần mềm phải tận dụng được
sức mạnh tính toán này để gải quyết một cách hiệu quả các bài toán thực tế. Ngày nay có
rất nhiều vấn đề yêu cầu những khối lượng tính toán rất lớn như: xử lý ảnh, xử lý ngôn
ngữ tự nhiên, data mining, truy vấn các cơ sở dữ liệu đa phương tiện, các hệ thống thời
gian thực, thực tại ảo… Để giải quyết những vấn đề này xu hướng hiện nay là áp dụng
các kĩ thuật lập trình song song vào các chương trình phần mềm để đạt được hiệu quả tốt
2
Tiểu luận môn học: Điện toán lưới và đám mây
hơn. Tuy nhiên lập trình song song đòi hỏi nhiều kĩ thuật và khó hơn nhiều so với lập
trình tuần tự. Nên tự nhiên phát sinh nhu cầu xây dựng các ngôn ngữ lập trình, khuôn
khổ(framework), thư viện chuẩn để hỗ trợ người lập trình xây dựng các chương trình
song song dễ dàng và đơn giản hơn. Rất nhiều ngôn ngữ, khuôn khổ, thư viện dạng này
đã ra đời và đáp ứng rất tốt nhu cầu trên như: Occam, Java thread, Pthread, PVM, MPI,


Cilk…
Từ phiên bản 1.7 Java đã bổ sung gói thư viện java.util.concurrent vào nền tảng Java SE
để hỗ trợ người lập trình xây dựng các chương trình trong môi trường đa xử lý được dễ
dàng hơn. Đặc biệt là đưa vào khuôn khổ Fork/Join nhằm trợ giúp song song hóa các lớp
bài toán “chia để trị” một cách dễ dàng và hiệu quả. Nội dung của tiểu luận là tập trung
giới thiệu khuôn khổ này cùng một số ví dụ minh họa cách song song hóa bài toán theo
phong cách fork/join của Java.
1. Sơ lược về Java Fork/Join framework
1.1 Nguồn gốc hình thành
- Khuôn khổ(framework) Fork/Join là một phần của JSR-166 nhằm cung cấp
những class tiện ích, hỗ trợ những chức năng thông dụng cho lập trình tương
tranh(concurrent programming). Nó bao gồm một vài khuôn khổ nhỏ, chuẩn và có khả
năng mở rộng cũng như những class cài đặt những chức năng phổ dụng, tẻ nhạt, thường
xuyên xuất hiện trong lập trình tương tranh.
3
Tiểu luận môn học: Điện toán lưới và đám mây
- Khuôn khổ Fork/Join là một cài đặt cho interface ExecutorService mà giúp
người lập trình tận dụng lợi thế các hệ thống đa xử lý(multiple processsors). Nó được
thiết kế cho các bài toán có thể chia thành các bài toán nhỏ hơn một các đệ quy. Mục đích
là để sử dụng năng lực của tất cả các bộ xử lý trong hệ thống để nâng cao hiệu năng giải
bài toán. Như vậy khuôn khổ Fork/Join rất thích hợp cho các lớp bài toán mà có thể giải
được bằng các thuật toán “chia để trị”( divide−and−conquer algorithms).
1.2 Thuật toán Fork/join điển hình
- Tính song song trong khuôn khổ Fork/Join là một trong những kĩ thuật thiết kế
đơn giản và hiệu quả nhất để nhận được hiệu năng song song tốt. Thuật toán tổng quát
khi áp dụng cho các bài toán “chia để trị” có dạng sau đây:
Result solve(Problem problem)
{
If (problem là nhỏ)
giải trực tiếp problem

else
{
independent-parts = chia problem thành những phần độc lập
với mỗi part trong independent-parts // fork operation
tạo ra một fork/join subtask để gọi đệ quy solve(part)
đợi tất cả các fork/join subtask hoàn tất // join operation
tổ hợp kết quả từ các fork/join subtask
}
}
Các subtask trong thao tác fork là thực thi một cách song song và thao tác join sẽ ngăn
không cho task hiện hành tiếp tục cho đến khi tất cả các subtask được hoàn tất. Quá trình
chia nhỏ bài toán sẽ lặp lại cho đến khi các bài toán con đủ nhỏ để giải một cách tuần tự,
đơn giản.
4
Tiểu luận môn học: Điện toán lưới và đám mây
1.3 Cơ chế hoạt động
- Thiết kế tổng quát của khuôn khổ Fork/Join là một biến thể “lấy trộm công
việc”(work-stealing) kế thừa từ Cilk(một ngôn ngữ lập trình dựa trên ANSI C được phát
triển từ năm 1994 tại phòng thí nghiệm khoa học máy tính của MIT). Kĩ thuật cài đặt
chính gói gọn trong việc khởi tạo và quản lý hiệu quả các hàng đợi công việc(tasks
queue) và các luồng làm việc(worker threads).
- Các chương trình Fork/join chia bài toán thành những phần độc lập nhỏ hơn
vào các subtask và thực thi các subtask một cách song song. Chúng ta đã biết Java hỗ trợ
lập trình tương tranh(concurrent programming) và lập trình song song bằng các
luồng(thread) nên sẽ là tự nhiên nếu nghĩ rằng mỗi subtask sẽ chạy trong một luồng
riêng. Tuy nhiên, khuôn khổ Fork/join không làm như thế vì những lí do sau đây:
• Các Fork/join task cần quản lý các yêu cầu đồng bộ hóa(synchonization) một cách
đơn giản. Các đồ thị tính toán được sản sinh ra bởi các fork/join task cần có một
chiến lược lập lịch hiệu quả hơn các luồng sao cho tận dụng tối đa các CPU hiện
có trong hệ thống. Thêm vào đó các fork/join task không cần phải block trừ khi nó

đợi các subtask hoàn tất. Như vậy các chi phí phát sinh do việc quản lý và theo dõi
các luồng bị block sẽ không còn.
• Bởi vì cách tiếp cận của khuôn khổ fork/join là chia bài toán ban đầu thành những
bài toán độc lập nhỏ hơn nên sẽ phát sinh tình trạng là có khá nhiều các fork/join
task. Nếu mỗi fork/join task chạy trong một luồng riêng rẽ thì có khả năng sẽ làm
cho hệ thống chạy rất chậm hoặc bị treo do có quá nhiều luồng được sinh ra. Đó là
chưa kể chi phí để khởi tạo và quản lý luồng có thể sẽ lớn hơn thời gian tính toán
cần thiết của task đó.
- Từ hai lý do nêu trên ta thấy rằng các luồng là quá nặng nề và cồng kềnh cho
các khuôn khổ Fork/join. Nên cần thiết có một cách tiếp cận khác cho khuôn khổ này.
Những người thiết kế khuôn khổ Fork/join đã đưa ra một thiết kế như sau:
• Tạo ra một hồ chứa các luồng làm việc (pool of worker threads). Mỗi luồng làm
việc trong hồ chứa này chính là một luồng Java chuẩn(standard thread) và nó sẽ có
một hàng đợi chứa các fork/join task. Thông thường thì số lượng các luồng trong
5
Tiểu luận môn học: Điện toán lưới và đám mây
hồ tương ứng với số lượng CPU của hệ thống. Sau đó mỗi luồng sẽ được ánh xạ
vào một CPU nhất định và thực thi các fork/join task trong hàng đợi của nó.
• Hàng đợi đề cập ở trên là một hàng đợi hai đầu(double−ended queue) dùng để lập
lịch, quản lý và thực thi các fork/join task thông qua luồng làm việc(worker
thread). Cơ chế lập lịch này dựa vào một khái niệm gọi là “lấy trộm công
việc”(work-stealing). Đây không phải là một khái niệm mới mà nó đã xuất hiện
trong Cilk. Về cơ bản có thể hiểu cơ chế này như sau:
 Những fork/join subtask được sinh ra từ một fork/join task sẽ được đưa
vào hàng đợi của luồng đang thực hiện fork/join task đó
 Các worker thread chọn fork/join task từ hàng đợi để thi hành theo quy
tắc LIFO(Last In First Out). Khi một worker thread hết task trong hàng
đợi của nó, nó sẽ cố gắng “trộm” một fork/join task từ một worker
thread khác theo quy tắc FIFO(First In First Out) nghĩa là chúng luôn
làm thao tác ở hai đầu khác nhau của hàng đợi. Cách làm này vừa hạn

chế tối xung đột khi hai thread cùng truy cập hàng đợi mà còn tận dụng
tính chất đệ quy của nguyên lý “chia để trị” khiến cho các fork/join task
sẽ có cơ hội được phân rã thành những task nhỏ hơn bởi luồng trộm
việc(stealing thread)
 Khi một worker thread bắt gặp thao tác join, nó sẽ thi hành một
fork/join task khác(lấy trong hàng đợi hay “trộm” từ luồng làm việc
khác) cho đến khi task đích thông báo hoàn tất. Tất cả các fork/join task
khác sẽ chạy mà không bị blocking. Nếu một worker thread không có
task thực thi và gặp thất bại khi “trộm” task từ worker thread khác nó sẽ
sleep, yields hoặc bị điều chỉnh lại độ ưu tiên sau đó sẽ lại cố gắng
“trộm” task. Quá trình này lặp lại cho đến khi tất cả các worker thread
đều rãnh rỗi. Trong trường hợp như vậy, tất cả các worker thread trong
hồ sẽ bị block cho dến khi có một fork/join task mới xuất hiện.
6
Tiểu luận môn học: Điện toán lưới và đám mây
• Tạo ra một abstract class tên là ForkJoinTask để biễu diễn cho một fork/join task.
Class này có một abstract method quan trọng là exec được gọi khi thi hành một
fork/join task. Để hỗ trợ người lập trình nhanh chóng tạo lập các fork/join task thì
từ class này có 2 class kế thừa là RecursiveAction và RecursiveTask. Hai class này
định nghĩa lại tất cả các abstract method trong class ForkJoinTask và chúng thêm
vào một abstract method mới tên là compute(được gọi trong method exec). Như
vậy để tạo ra một fork/join task thì người lập trình chỉ cần tạo ra một class kế thừa
từ một trong hai class RecursiveAction hoặc RecursiveTask và định nghĩa lại
method compute.
• Tạo ra một class ForkJoinPool để quản lý hồ chứa các luồng làm việc(pool of
worker thread), khởi tạo sự thực thi của một fork/join task khi nó được triệu gọi từ
một luồng bên ngoài(như trong hàm main chẳng hạn)
7
Tiểu luận môn học: Điện toán lưới và đám mây
2. Ứng dụng

Trong phần này sẽ trình bày một số ví dụ minh họa cách song song hóa một bài toán với
Java Fork/Join framework. Với mỗi bài toán sẽ gồm 2 phần:
8
Tiểu luận môn học: Điện toán lưới và đám mây
- Mã nguồn cài đặt cho phiên bản song song cùng một số giải thích(nếu cần thiết)
- Một bảng trình bày thời gian chạy cho 2 phiên bản: tuần tự và song song(ở mỗi
lần chạy cả hai phiên bản đều chạy cùng một bộ dữ liệu). Tất cả thời gian chạy đều được
tính bằng đơn vị giây. Các thử nghiệm chạy trên một máy Intel core i5 có 4 bộ xử lý.
Để xem toàn bộ mã nguồn, có thể tham khảo các file .java kèm theo. Toàn bộ mã nguồn
được biên dịch bằng jdk 1.8 update 5.
2.1. Tìm số hạng a
n
trong dãy số Fibonacci với n cho trước
- Xét bài toán: cần tìm số hạng a
n
trong dãy số Fibonacci với n cho trước. Ta có
thể song song hóa bài toán này như sau:
class FibonacciForkJoinTask extends RecursiveAction
{
static final int threshold = 13;
volatile int number; // value of a
n

public FibonacciForkJoinTask(int n)
{
number = n;
}

@Override
public void compute()

{
int n = number;
if (n <= threshold)
number = seqFib(n);
else
{
FibonacciForkJoinTask f1 = new FibonacciForkJoinTask(n - 1);
FibonacciForkJoinTask f2 = new FibonacciForkJoinTask(n - 2);
invokeAll(f1, f2);
number = f1.number + f2.number;
}
}
private int seqFib(int n)
9
Tiểu luận môn học: Điện toán lưới và đám mây
{
if (n <= 1)
return n;
return seqFib(n-1) + seqFib(n-2);
}
}
Bảng sau đây là kết quả mười lần chạy thực nghiệm khi phát sinh ngẫu nhiên n từ 30 –
50. Ta thấy là trong đa số trường hợp phiên bản song song luôn chạy nhanh hơn bản tuần
tự gần ba lần.
Bảng 2.1 Kết quả chạy thực nghiệm cho bài toán tìm số hạng a
n
trong dãy Fibonacci khi
biết n
2.2 Sắp xếp mảng số nguyên bằng thuật toán Quicksort
- Xét bài toán: cần sắp xếp một mảng các số nguyên bằng thuật toán Quicksort. Ta

có thể song song hóa bài toán này như sau:
class ForkJoinQuicksortTask extends RecursiveAction
{
private static final int SERIAL_THRESHOLD = 0x1000;
10
N Serial execution time Paralell execution time
Speedu
p
30 0.004 0.002 2.00
31 0.007 0.002 3.50
32 0.01 0.003 3.33
33 0.016 0.006 2.67
36 0.071 0.024 2.96
37 0.114 0.124 0.92
38 0.179 0.067 2.67
39 0.294 0.083 3.54
43 1.994 0.614 3.25
44 3.213 0.939 3.42
Tiểu luận môn học: Điện toán lưới và đám mây
private final int[] a;
private final int left;
private final int right;
public ForkJoinQuicksortTask(int[] a)
{
this(a, 0, a.length - 1);
}
private ForkJoinQuicksortTask(int[] a, int left, int right)
{
this.a = a;
this.left = left;

this.right = right;
}
@Override
protected void compute()
{
if (right - left < SERIAL_THRESHOLD)
Arrays.sort(a, left, right + 1);
else
{
int[] indexs = partition(a, left, right);
int curLeft = indexs[0], curRight = indexs[1];
ForkJoinTask t1 = null;
if (left < curRight)
t1 = new ForkJoinQuicksortTask(a, left, curRight).fork();
if (curLeft < right)
new ForkJoinQuicksortTask(a, curLeft, right).invoke();
if (t1 != null)
t1.join();
}
}

private int[] partition(int[] a, int left, int right)
{
// chose middle value of range for our pivot
int pivotValue = a[(left + right) >>> 1];
while (left <= right)
{
while (a[left] < pivotValue)
++left;
while (a[right] > pivotValue)

right;
if (left <= right)
{
int tmp = a[left];
a[left] = a[right];
a[right] = tmp;
left++;
right ;
11
Tiểu luận môn học: Điện toán lưới và đám mây
}
}

return new int[] { left, right };
}
}
Bảng sau đây là kết quả thực nghiệm của mười lần chạy. Mỗi lần sắp xếp một mảng
nguyên ngẫu nhiên có một trăm triệu phần tử. Phiên bản song song hóa với Fork/Join
luôn chạy nhanh hơn
gấp ba lần so với phiên
bản tuần tự.
Bảng 2.2 Kết quả chạy thực nghiệm sắp xếp mảng số nguyên một trăm triệu phần tử bằng
thuật toán Quicksort
2.3 Nhân hai ma trận vuông bằng thuật toán Strassen
- Xét bài toán: cần nhân hai ma trận vuông bằng thuật toán Strassen. Để đơn giản
chỉ xét các ma trận vuông có kích thước là một lũy thừa của hai. Ta có thể song song hóa
bài toán này như sau:
class StrassenForkJoin extends RecursiveAction
12
Serial execution time Paralell execution time Speedup

10.519 3.341 3.15
10.83 3.243 3.34
10.829 2.878 3.76
10.648 2.957 3.60
10.906 3.522 3.10
10.829 3.334 3.25
10.63 3.103 3.43
10.629 3.013 3.53
10.74 3.322 3.23
10.638 3.106 3.42
Tiểu luận môn học: Điện toán lưới và đám mây
{
public int[][] result;
int[][] a, b;
int n;

public StrassenForkJoin(int[][] a, int[][] b, int n)
{
this.a = a;
this.b = b;
this.n = n;
}
private int[][] sumMatrix(int[][] a, int[][] b, int n)
{
int[][] res = new int[n][n];

for (int i = 0; i < n; ++i)
for (int j = 0; j < n; ++j)
{
res[i][j] = a[i][j] + b[i][j];

}

return res;
}
private int[][] subMatrix(int[][] a, int[][] b, int n)
{
int[][] res = new int[n][n];

for (int i = 0; i < n; ++i)
for (int j = 0; j < n; ++j)
{
res[i][j] = a[i][j] - b[i][j];
}

return res;
}
private int[][] mulMatrix(int[][] a, int[][] b, int n)
{
13
Tiểu luận môn học: Điện toán lưới và đám mây
int[][] res = new int[n][n];
int t = 0;

for (int i = 0; i < n; ++i)
for (int j = 0; j < n; ++j)
{
t = 0;
for (int k = 0; k < n; ++k)
t += a[i][k]*b[k][j];


res[i][j] = t;
}

return res;
}

private void fillMatrix(int[][] a, int[][] b, int nB, int row, int col)
{
int r, c;

r = (row-1)*nB;
c = (col-1)*nB;
for (int i = 0; i < nB; ++i)
for (int j = 0; j < nB; ++j)
a[r + i][c + j] = b[i][j];

}

private int[][] extractMatrix(int[][] a, int n, int row, int col)
{
int[][] res = new int[n/2][n/2];
int r, c;

r = (row-1)*n/2;
c = (col-1)*n/2;
for (int i = 0; i < n/2; ++i)
14
Tiểu luận môn học: Điện toán lưới và đám mây
for (int j = 0; j < n/2; ++j)
res[i][j] = a[r + i][c + j];


return res;
}

@Override
public void compute()
{
if (n <= 2)
result = mulMatrix(a, b, n);
else
{
int[][] a11, a22, a21, a12, b21, b22, b11, b12,
c11, c12, c21, c22;
StrassenForkJoin t1, t2, t3, t4, t5, t6, t7;

a12 = extractMatrix(a, n, 1, 2);
a22 = extractMatrix(a, n, 2, 2);
b21 = extractMatrix(b, n, 2, 1);
b22 = extractMatrix(b, n, 2, 2);
t1 = new StrassenForkJoin(subMatrix(a12, a22, n/2),
sumMatrix(b21, b22, n/2), n/2);
a11 = extractMatrix(a, n, 1, 1);
b11 = extractMatrix(b, n, 1, 1);
t2 = new StrassenForkJoin(sumMatrix(a11, a22, n/2),
sumMatrix(b11, b22, n/2), n/2);
a21 = extractMatrix(a, n, 2, 1);
b12 = extractMatrix(b, n, 1, 2);
t3 = new StrassenForkJoin(subMatrix(a11, a21, n/2),
sumMatrix(b11, b12, n/2), n/2);
a12 = extractMatrix(a, n, 1, 2);

t4 = new StrassenForkJoin(sumMatrix(a11, a12, n/2), b22, n/2);
15
Tiểu luận môn học: Điện toán lưới và đám mây
t5 = new StrassenForkJoin(a11, subMatrix(b12, b22, n/2), n/2);
t6 = new StrassenForkJoin(a22, subMatrix(b21, b11, n/2), n/2);
t7 = new StrassenForkJoin(sumMatrix(a21, a22, n/2), b11, n/2);

invokeAll(t1, t2, t3, t4, t5, t6, t7);

c11 = sumMatrix(subMatrix(sumMatrix(t1.result, t2.result, n/2),
t4.result, n/2), t6.result, n/2);
c12 = sumMatrix(t4.result, t5.result, n/2);
c21 = sumMatrix(t6.result, t7.result, n/2);
c22 = subMatrix(sumMatrix(subMatrix(t2.result, t3.result, n/2),
t5.result, n/2), t7.result, n/2);
result = new int[n][n];
fillMatrix(result, c11, n/2, 1, 1);
fillMatrix(result, c12, n/2, 1, 2);
fillMatrix(result, c21, n/2, 2, 1);
fillMatrix(result, c22, n/2, 2, 2);
}
}
}
Bảng sau đây là kết quả chạy thực nghiệm với các ma trận ngẫu nhiên có kích thước từ 2,
4, 8…2048. Với kích thước từ 2 – 64 phiên bản song song chạy chậm hơn phiên bản tuần
16
Tiểu luận môn học: Điện toán lưới và đám mây
tự. Nhưng từ kích thước 128 trở về sau phiên bản song song chạy nhanh hơn gấp ba lần
phiên bản tuần tự .
N Serial execution time Paralell execution time Speedup

2 0.001 0.052 0.02
4 0.000 0.001 0.00
8 0.000 0.000 0.00
16 0.001 0.002 0.50
32 0.004 0.008 0.50
64 0.027 0.033 0.82
128 0.145 0.062 2.34
512 5.138 1.514 3.39
1024 35.934 10.319 3.48
2048 246.697 70.131 3.52
Bảng 2.3 Kết quả chạy thực nghiệm bài toán nhân hai ma trận vuông bằng thuật toán
Strassen
3. Kết luận
Tiểu luận đã tập trung vào giới thiệu khuôn khổ Java Fork/Join cùng một số ví dụ minh
họa cách áp dụng khuôn khổ này cho một số bài toán cụ thể, như sau:
- Giới thiệu nguồn gốc hình thành, mục đích của khuôn khổ Java Fork/Join.
- Mô tả ý tưởng thiết kế và cách thức vận hành khuôn khổ một cách tổng quan.
17
Tiểu luận môn học: Điện toán lưới và đám mây
- Trình bày ví dụ áp dụng bằng mã cụ thể để có thể kiểm chứng khi cần thiết
- Đưa ra một bảng so sánh hiệu năng thời gian chạy cho cả hai phiên bản tuần tự,
song song.
Bên cạnh đó tiểu luận cũng có một số hạn chế như: chưa đưa ra một hình ảnh trực quan
về kiến trúc của khuôn khổ Java Fork/Join, chưa thảo luận sâu về những vấn đề kinh điển
trong mô hình lập trình song song chia sẻ bộ nhớ như đồng bộ hóa(synchronization), thu
gom rác(Garbage collection), vai trò của hàng đợi task trong việc điều phối các task cho
các worker thread cũng như chưa chứng minh rằng có phải: lấy task để thực thi bằng quy
tắc LIFO và trộm task bằng quy tắc FIFO là thiết kế tối ưu các cho các khuôn khổ
fork/join hay không.
Vì vậy hướng phát triển của tiểu luận là:

- Mô tả kiến trúc và các thiết kế cơ bản của khuôn khổ bằng biểu đồ UML
- Thảo luận các về cơ chế đồng bộ hóa (synchronization), thu gom rác(Garbage
collection) trong quá trình thực hiện song song.
- Chứng minh rằng: lấy task để thực thi bằng quy tắc LIFO và trộm task bằng quy
tắc FIFO là thiết kế tối ưu các cho các khuôn khổ fork/join.
- Đánh giá hiệu năng so với các khuôn khổ khác có cùng tính năng.
4. Tài liệu tham khảo
1. Doug Lea's Home Page, />2. Hồ Thuần(Chủ biên), Hồ Cẩm Hà, Trần Thiên Thành(2008), Cấu trúc dữ liệu, Phân
tích thuật toán và Phát triển phần mềm, Nhà xuất bản Giáo Dục
3. />18
Tiểu luận môn học: Điện toán lưới và đám mây
4. The Java™ Tutorials,
/>19

×