Nhóm các công cụ lập trình ứng dụng vận hành đa nhiệm được Java
cung cấp kèm theo hai khái niệm là Process
và Thread
. Trong đó thì một Process
mô tả một môi trường vận hành code hoàn chỉnh, và Thread
là một dạng Process
với thiết kế đơn giản hơn và tồn tại bên trong Process
. Mỗi ứng dụng JVM
mặc định là được khởi tạo với một Process
, và mỗi Process
được mặc định là được khởi tạo với một Thread
.
Ở đây chúng ta sẽ không đề cập đến vấn đề quản lý ở cấp độ của các Process
mà chỉ tìm hiểu về cách tạo ra các Thread
.
Runnable
interface:
java.lang.Runnable
Trước khi nghĩ tới việc thực hiện song song các tác vụ thì chúng ta cần một giao diện chung để định nghĩa các tác vụ. Thay vì object A
mô tả một tác vụ có phương thức khởi chạy là .start()
và object B
mô tả một tác vụ khác có phương thức khởi chạy là .execute()
, thì ở đây chúng ta có Runnable
để tạo giao diện lập trình chung cho các object
mô tả các tác vụ đơn với phương thức duy nhất .run()
để khởi chạy.
1 2 3 4 5 6 7 8 9 10 11 12 | <span class="token keyword">import</span> <span class="token namespace">java<span class="token punctuation">.</span>io<span class="token punctuation">.</span></span><span class="token operator">*</span><span class="token punctuation">;</span> <span class="token keyword">class</span> <span class="token class-name">Main</span> <span class="token punctuation">{</span> <span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token keyword">void</span> main <span class="token punctuation">(</span><span class="token class-name">String</span><span class="token punctuation">[</span><span class="token punctuation">]</span> $args<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token class-name">Runnable</span> $task <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">-></span> <span class="token class-name">System</span><span class="token punctuation">.</span>out<span class="token punctuation">.</span>println <span class="token punctuation">(</span><span class="token string">"Do something"</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span> $task<span class="token punctuation">.</span>run <span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token comment">// -- end class</span> |
Lúc này các object
mô tả các tác vụ như $task
được tạo ra đã có giao diện lập trình chung, nhưng vẫn chưa được thực thi trên các tiến trình riêng biệt. Bước tiếp theo là chúng ta cần tạo ra các tiến trình thực thi code độc lập so với tiến trình vận hành chính của phương thức main
.
Thread
class:
java.util.Thread
Chúng ta có thể sử dụng class Thread
để tạo ra các object
mô tả các tiến trình thực thi code độc lập theo hai cách: Bởi vì class Thread
có triển khai Runnable
vì vậy nên chúng ta có thể tạo ra các object
thuộc class
nặc danh và override
phương thức .run()
; Hoặc, chúng ta cũng có thể truyền một object Runnable
vào trình khởi tạo của Thread
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | <span class="token keyword">import</span> <span class="token namespace">java<span class="token punctuation">.</span>io<span class="token punctuation">.</span></span><span class="token operator">*</span><span class="token punctuation">;</span> <span class="token keyword">class</span> <span class="token class-name">Main</span> <span class="token punctuation">{</span> <span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token keyword">void</span> main <span class="token punctuation">(</span><span class="token class-name">String</span><span class="token punctuation">[</span><span class="token punctuation">]</span> $args<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token class-name">Runnable</span> $task <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">-></span> <span class="token punctuation">{</span> <span class="token keyword">try</span> <span class="token punctuation">{</span> <span class="token class-name">Thread</span><span class="token punctuation">.</span>sleep <span class="token punctuation">(</span><span class="token number">1000</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token class-name">System</span><span class="token punctuation">.</span>out<span class="token punctuation">.</span>println <span class="token punctuation">(</span><span class="token string">"Extra thread"</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token keyword">catch</span> <span class="token punctuation">(</span><span class="token class-name">Exception</span> $exception<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token class-name">System</span><span class="token punctuation">.</span>out<span class="token punctuation">.</span>println <span class="token punctuation">(</span>$exception<span class="token punctuation">.</span>toString <span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// -- Runnable</span> <span class="token class-name">Thread</span> $thread <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">Thread</span> <span class="token punctuation">(</span>$task<span class="token punctuation">,</span> <span class="token string">"Thread name"</span><span class="token punctuation">)</span><span class="token punctuation">;</span> $thread<span class="token punctuation">.</span>start <span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token class-name">System</span><span class="token punctuation">.</span>out<span class="token punctuation">.</span>println <span class="token punctuation">(</span><span class="token string">"Main thread"</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token comment">// -- end class</span> |
Ở đây chúng ta có câu lệnh Thread.sleep(1000);
sẽ tạm dừng tiến trình của Thread
đang thực thi $task
. Tuy nhiên, do chúng ta đã đặt $task
vào một tiến trình mới nên tiến trình của main
không bị ảnh hưởng; Và kết quả là câu lệnh in ra dòng chữ "Main thread"
sẽ được thực hiện trước so với câu lệnh in trong $task
.
1 2 3 4 5 6 | Main thread delay 1 second... Extra thread |
Trong trường hợp muốn gộp tiến trình thực thi của một Thread
với một tiến trình bất kỳ thì chúng ta cần giữ địa chỉ tham chiếu của Thread
đó và gọi phương thức .join()
trong tiến trình muốn gộp. Ở đây chúng ta sẽ thử gộp trở lại tiến trình chính main
bằng cách gọi phương thức .join()
ngay sau khi .start()
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | <span class="token keyword">import</span> <span class="token namespace">java<span class="token punctuation">.</span>io<span class="token punctuation">.</span></span><span class="token operator">*</span><span class="token punctuation">;</span> <span class="token keyword">class</span> <span class="token class-name">Main</span> <span class="token punctuation">{</span> <span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token keyword">void</span> main <span class="token punctuation">(</span><span class="token class-name">String</span><span class="token punctuation">[</span><span class="token punctuation">]</span> $args<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token class-name">Runnable</span> $task <span class="token operator">=</span> <span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span> <span class="token punctuation">;</span> <span class="token class-name">Thread</span> $thread <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">Thread</span> <span class="token punctuation">(</span>$task<span class="token punctuation">,</span> <span class="token string">"Thread name"</span><span class="token punctuation">)</span><span class="token punctuation">;</span> $thread<span class="token punctuation">.</span>start <span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">try</span> <span class="token punctuation">{</span> $thread<span class="token punctuation">.</span>join <span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token keyword">catch</span> <span class="token punctuation">(</span><span class="token class-name">Exception</span> $exception<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token class-name">System</span><span class="token punctuation">.</span>out<span class="token punctuation">.</span>println <span class="token punctuation">(</span>$exception<span class="token punctuation">.</span>toString <span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token class-name">System</span><span class="token punctuation">.</span>out<span class="token punctuation">.</span>println <span class="token punctuation">(</span><span class="token string">"Main thread"</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token comment">// -- end class</span> |
Kết quả vận hành:
1 2 3 4 5 | delay 1 second... Extra thread Main thread |
Synchronized
Ở bài viết trước, khi giới thiệu tổng quan về Java Collections Framework
, chúng ta đã thấy Java
có cung cấp thêm các phiên bản cấu trúc dữ liệu thread-safe
. Cụm từ thread-safe
có nghĩa là các cấu trúc này được thiết kế để đảm bảo rằng: trong trường hợp cùng lúc được truy xuất và chỉnh sửa bởi nhiều thread
khác nhau thì kết quả hoạt động sẽ luôn ổn định giống như khi làm việc với một thread
duy nhất.
Để hiểu rõ hơn ở điểm này, chúng ta sẽ xuất phát với một cấu trúc dữ liệu tự định nghĩa và giả định rằng cấu trúc này sẽ lưu trữ toàn bộ dữ liệu mô tả bối cảnh hoạt động của một ứng dụng.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | <span class="token keyword">public</span> <span class="token keyword">class</span> <span class="token class-name">Context</span> <span class="token punctuation">{</span> <span class="token keyword">private</span> <span class="token keyword">int</span> data<span class="token punctuation">;</span> <span class="token keyword">public</span> <span class="token class-name">Context</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">this</span><span class="token punctuation">.</span>data <span class="token operator">=</span> <span class="token number">0</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token keyword">public</span> <span class="token keyword">void</span> increase <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">this</span><span class="token punctuation">.</span>data <span class="token operator">+=</span> <span class="token number">1</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token keyword">public</span> <span class="token keyword">void</span> decrease <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">this</span><span class="token punctuation">.</span>data <span class="token operator">-=</span> <span class="token number">1</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token keyword">public</span> <span class="token keyword">int</span> getData <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">return</span> <span class="token keyword">this</span><span class="token punctuation">.</span>data<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token comment">// -- end Context</span> |
Lúc này chúng ta có một object Context
sử dụng chung bởi tất cả các thành phần trong ứng dụng. Giả sử, chúng ta có thread A
thực hiện tăng giá trị và truy xuất, và đồng thời thread B
thực hiện giảm giá trị và truy xuất. Lúc này, kết quả vận hành có thể sẽ diễn ra như sau:
thread A
truy xuất giá trị củadata=0
và thực hiện tăng giá trị thành1
.thread B
truy xuất giá trị củadata=0
và thực hiện giảm giá trị thành-1
.thread A
ghi giá trị trở lại bộ nhớdata=1
.thread B
ghi giá trị trở lại bộ nhớdata=-1
.thread A
truy xuất giá trị và in radata=-1
.thread B
truy xuất giá trị và in radata=-1
.
Như vậy là kết quả mà chúng ta dự kiến cho logic hoạt động của thread A
đã sai lệch. Thay vì in ra giá trị là 1
thì câu lệnh in của thread A
lại in ra -1
. Và cấu trúc dữ liệu Context
mà chúng ta định nghĩa lúc này được gọi là non-threadsafe
– có nghĩa là không đảm bảo an toàn khi sử dụng trong môi trường đa nhiệm. Và để đơn giản hóa giải pháp ở đây thì Java
có cung cấp một từ khóa synchronized
để có thể sử dụng cho các phương thức và các khối lệnh.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | <span class="token keyword">public</span> <span class="token keyword">class</span> <span class="token class-name">SynchronizedContext</span> <span class="token punctuation">{</span> <span class="token keyword">private</span> <span class="token keyword">int</span> data<span class="token punctuation">;</span> <span class="token keyword">public</span> <span class="token class-name">Context</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">this</span><span class="token punctuation">.</span>data <span class="token operator">=</span> <span class="token number">0</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token keyword">public</span> <span class="token keyword">synchronized</span> <span class="token keyword">void</span> increase <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">this</span><span class="token punctuation">.</span>data <span class="token operator">+=</span> <span class="token number">1</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token keyword">public</span> <span class="token keyword">synchronized</span> <span class="token keyword">void</span> decrease <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">this</span><span class="token punctuation">.</span>data <span class="token operator">-=</span> <span class="token number">1</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token keyword">public</span> <span class="token keyword">synchronized</span> <span class="token keyword">int</span> getData <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">return</span> <span class="token keyword">this</span><span class="token punctuation">.</span>data<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token comment">// -- end Context</span> |
Và đây chính là cách mà Java
tạo ra các phiên bản thread-safe
của các cấu trúc dữ liệu. Khi có nhiều thread
đồng thời gọi một phương thức synchronized
, giả sử đầu tiên .increase()
được thread A
gọi sẽ được xử lý trước như trên. Thì tất cả các lời gọi phương thức khác của SynchronizedContext
đều sẽ tạm dừng để chờ .increase()
được thực thi xong trên thread A
.
Sau khi .increase()
được thực thi xong, JVM
sẽ tiếp tục kiểm tra thread-id
của thread A
trong số các lời gọi phương thức còn lại và tìm thấy .getData()
để in ra và sẽ tiếp tục ưu tiên thread A
xử lý tương tác này. Và kết quả hoạt động mà chúng ta có ở đây sẽ là:
thread A
truy xuất giá trị củadata=0
và thực hiện tăng giá trị thành1
.thread A
ghi giá trị trở lại bộ nhớdata=1
.thread A
truy xuất giá trị và in radata=1
.thread B
truy xuất giá trị củadata=1
và thực hiện giảm giá trị thành0
.thread B
ghi giá trị trở lại bộ nhớdata=0
.thread B
truy xuất giá trị và in radata=0
.
Trong trường hợp cần chuyển đổi một cấu trúc dữ liệu
non-threadsafe
thành kiểusynchronized
, chúng ta có thể sử dụng các phương thứcsynchronized
củajava.util.Collections
Đây có lẽ đã là điểm dừng phù hợp cho chủ đề Concurrency Programming
trong Sub-Series mang tính chất giới thiệu ngôn ngữ. Tiếp theo, chúng ta đã có thể suy nghĩ tới việc thực hiện mini project
để thực hành áp dụng tư duy OOP
vào kiến trúc code của phần mềm muốn xây dựng.
Tới đây thì mình đã phải thay đổi quyết định khá nhiều lần giữa lựa chọn xây dựng một bàn cờ vua chế độ 2 người chơi và một ứng dụng quản lý dữ liệu. Tuy nhiên, cho dù là lựa chọn nào thì cũng sẽ cần phải tạo ra một giao diện đồ họa để tương tác với người dùng thay cho giao diện cửa sổ dòng lệnh. Các lựa chọn còn lại hiển nhiên là:
- Vẽ giao diện trong cửa sổ trình duyệt web với
HTML
,CSS
, vàJavaScript
. - Hoặc, vẽ giao diện ứng dụng độc lập với một
framework
nào đó có hỗ trợJava
.
Lựa chọn đầu tiên sẽ kéo theo yêu cầu tìm hiểu về module java.net
để tạo máy chủ web làm back-end
xử lý logic hoạt động cốt lõi của ứng dụng bằng Java
. Nghe thì có vẻ sẽ đỡ tốn công sức hơn bởi chúng ta đã biết về bộ ngôn ngữ front-end
của lập trình web. Tuy nhiên, nếu như để tạo ứng dụng vận hành offline
thì Java
có cung cấp module.desktop
hỗ trợ vẽ giao diện người dùng. Nếu bạn không hẳn ưa việc sử dụng các ngôn ngữ nền web ở trên thì có thể đồng hành cùng mình trong những bài viết tiếp theo, tìm hiểu về các package
vẽ giao diện đồ họa java.awt
và javax.swing
.