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.
import java.io.*; class Main { public static void main (String[] $args) { Runnable $task = (() -> System.out.println ("Do something")); $task.run ();
} } // -- end class
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
.
import java.io.*; class Main { public static void main (String[] $args) { Runnable $task = (() -> { try { Thread.sleep (1000); System.out.println ("Extra thread"); } catch (Exception $exception) { System.out.println ($exception.toString ()); } }); // -- Runnable Thread $thread = new Thread ($task, "Thread name"); $thread.start (); System.out.println ("Main thread");
} } // -- end class
Ở đâ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
.
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()
.
import java.io.*; class Main { public static void main (String[] $args) { Runnable $task = ... ; Thread $thread = new Thread ($task, "Thread name"); $thread.start (); try { $thread.join (); } catch (Exception $exception) { System.out.println ($exception.toString ()); } System.out.println ("Main thread"); } } // -- end class
Kết quả vận hành:
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.
public class Context { private int data; public Context () { this.data = 0; } public void increase () { this.data += 1; } public void decrease () { this.data -= 1; } public int getData () { return this.data; } } // -- end Context
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.
public class SynchronizedContext { private int data; public Context () { this.data = 0; } public synchronized void increase () { this.data += 1; } public synchronized void decrease () { this.data -= 1; } public synchronized int getData () { return this.data; } } // -- end Context
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 của 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 tự viết code để quản lý một database
đơn giản cho ứng dụng muốn xây dựng. Đối với Sub-Series này thì mình đã cân nhắc khá nhiều và quyết định chọn chủ đề cho mini project
nghiệm thu kiến thức là ứng dụng quản lý một cửa hàng bán và cho thuê sách.
Cơ sở dữ liệu sẽ ở dạng mở và lưu trữ trong các tệp .xml
và vì vậy nên nếu như bạn chọn sử dụng một kiểu CSDL nào đó khác như SQL
thì có thể tham khảo hướng dẫn của W3schools
hoặc TutorialsPoint
và bỏ qua bài viết sau.