Mục lục
Ở bài trước, chúng ta đã biết các thao tác cơ bản trong xử lý đa luồng như tạo một thread, chạy thread với start() method. Sử dụng join() method để đảm bảo thứ tự chạy của các thread, việc này có vẽ giống như là đồng bộ hoá các thread thì phải! Trong bài này chúng ta sẽ tìm hiểu về cơ chế và cách để đồng bộ hoá các thread.
Trong chương trình đa luồng sẽ thường xuyên xảy ra các hoạt động thất thường và tạo ra các kết quả không mong muốn do các thread không được đồng bộ hoá. Ví dụ như Thread A cần kết quả của Thread B thực thi xong để tính toán tiếp, do không có cơ chế đồng bộ hoá Thread A start() và truy xuất biến biến lưu trữ kết quả trong khi Thread B chưa thực thi xong dẫn đến các kết quả sai.
Đồng bộ hoá là một công việc mà chúng ta phải sắp xếp lại thứ tự truy cập của các thread vào một đoạn mã nguồn truy cập các biến thuộc class hoặc biến instance, hay các tài nguyên khác.
Note: Các thread không thể chia sẽ các biến local(biến khai báo bên trong method) và tham số truyền vào vì chúng được cấp phát vùng nhớ method-call stack của từng thread. Kết quả là mỗi thread mỗi thread sẽ có một bản copy của các biến local và tham số truyền vào. Nhưng các thread có thể chia sẽ các biến thuộc class, các biến instance vì chúng được cấp phát vùng nhớ trong heap-memory.
Tại sao cần đồng bộ hoá?
Tại sao chúng ta lại cần đồng bộ hoá? Hãy xem ví dụ sau đây
class Val { public static Integer i= 0; } class ThreadA extends Thread { @Override public void run() { for (int i = 0; i < 5; i++) { Val.i += (int) (Math.random() * 1000); System.out.println("Thread A: " + Val.i); } } } class ThreadB extends Thread { @Override public void run() { for (int i = 0; i < 5; i++) { Val.i -= (int) (Math.random() * 1000); System.out.println("Thread B: " + Val.i); } } } class Main { public static void main(String[] args) { new ThreadA().start(); new ThreadB().start(); } }
Chúng ta có 2 thread ThreadA và ThreadB đều truy cập vào biến static của Val class. Mình mong muốn giá trị i sau khiđược ThreadA tăng lên một khoảng giá trị, rồi mới bị ThreadB trừ đi một khoảng giá trị!
Thế nhưng Output lại không như mình mong muốn
Thread A: -356
Thread B: -356
Thread A: 551
Thread B: -422
Thread A: -78
Thread B: -513
Thread A: 360
Thread B: -1
Thread A: 162
Thread B: -318
ThreadA và ThreadB cứ chạy loạn xạ cả lên! làm sao để cho chúng chạy đúng như mình mong muốn đây? Trước tiên hãy tìm hiểu về cơ chế thực hiện đồng bộ hoá trong java thực hiện ra sao đã nhé!
Cơ chế đồng bộ hoá trong java – Java’s synchronization
Java cung cấp cơ chế đồng bộ hoá cho phép các lập trình viên có thể điều phối đường đi thread. Cơ chế này dựa trên khái niệm monitor và lock. Chúng ta có thể ví monitor như là một lá chắn bảo vệ các tài nguyên(biến thuộc class, biến instance, etc), còn lock như một chìa khoá mà monitor dùng để ngăn chặn nhiều thread truy cập vào các tài nguyên mà monitor đang bảo vệ. Khi một thread muốn truy cập vào các tài nguyên được bảo vệ vởi monitor chúng phải chiếm được lock để giành quyền truy cập, khi lock đã được chiếm bởi 1 thread, các thread khác sẽ được JVM sắp vào vùng chờ. Khi thread đã sử dụng xong các tài nguyên bên trong monitor nó sẽ tiến hành phát hành một lock mới, JVM sẽ xoá vùng chờ và cho phép các thread khác chiếm lock và truy cập vào monitor.
Mệnh đề synchronized
Sử dụng mệnh đề synchronized để bảo vệ tài nguyên khỏi bị truy cập bởi nhiều luồng cùng một lúc, các tài nguyên này có thể là các biến class, biến instance etc.
'synchronized' '(' objectidentifier ')' '{' // block code '}'
Để đồng bộ hoá với synchronized, trước tiên mệnh đề sẽ bắt đầu bằng từ khoá synchronized, objectidentifier được bao trong cặp dấu ngoặc là một object sẽ được bảo vệ khỏi sự truy cập từ nhiều thread trong cùng một lúc. Sau khi thực thi hết block code thì một lock mới được phát hành và nhường chỗ cho các thread khác truy cập vào objectidentifier.
class Val { public static Integer i = 0; } class ThreadA extends Thread { @Override public void run() { synchronized (Val.i) { for (int i = 0; i < 5; i++) { Val.i += (int) (Math.random() * 1000); System.out.println("Thread A: " + Val.i); } } } } class ThreadB extends Thread { @Override public void run() { synchronized (Val.i) { for (int i = 0; i < 5; i++) { Val.i -= (int) (Math.random() * 1000); System.out.println("Thread B: " + Val.i); } } } } class Main { public static void main(String[] args) { new ThreadA().start(); new ThreadB().start(); } }
Quay trở lại ví dụ lúc đầu, mình đã thêm từ khoá synchronized bao bên ngoài biến i của Val class. Trong hàm main() ThreadA được start() trước, khi gặp synchronized nó sẽ chiếm được lock và có quyền truy cập vào biến i của Val class, đoạn mã bên trong synchronized của ThreadA sau khi thực thi xong thì một lock mới mới được phát hành và sẽ đến ThreadB chiếm giữ.
Output:
Thread A: 18
Thread A: 292
Thread A: 449
Thread A: 988
Thread A: 1466
Thread B: 988
Thread B: 708
Thread B: -124
Thread B: -709
Thread B: -1340
synchronized method
Sử dụng synchronized object không đúng cách đôi khi sẽ làm giảm hiệu suất của chương trình. Ví dụ như trong chương trình của bạn có một method trong đó nó sử dụng synchronized để chiếm giữa trên cùng một object, như vậy method đó phải trải qua 2 lần monitor/lock mà không cần thiết làm tăng thời gian thực thi của chương trình và làm giảm hiệu suất.
Sử dụng synchronized method của instance sẽ khiến 1 thread phải chiếm được lock của object mà method đang tham chiếu đến. Chương trình chỉ phải monitor/lock 1 lần giúp tăng hiệu suất cho chương trình.
class Val { public Integer i = 0; public StringBuilder str = new StringBuilder(); public synchronized void update(String nameThread) { for (int i = 0; i < 5; i++) { this.i += (int) (Math.random() * 1000); System.out.println(nameThread + " : " + this.i); } for (int i = 0; i < 100; i++) { this.str.append(i); } System.out.println(this.str.toString()); System.out.println("---------------------------------------"); } } class ThreadA extends Thread { Val val; public ThreadA(Val val){ this.val = val; } @Override public void run() { this.val.update("ThreadA"); } } class ThreadB extends Thread { Val val;0 public ThreadB(Val val){ this.val = val; } @Override public void run() { this.val.update("ThreadB"); } } class Main { public static void main(String[] args) { Val val = new Val(); new ThreadA(val).start(); new ThreadB(val).start(); } }
OutputL
ThreadA : 105
ThreadA : 183
ThreadA : 1163
ThreadA : 2015
ThreadA : 2352
012345678910111213…
—————————————
ThreadB : 2595
ThreadB : 2749
ThreadB : 3604
ThreadB : 3630
ThreadB : 4230
01234567891011121314…
—————————————
ThreadA và ThreadB đều tham chiếu đến cùng một Val object được tạo ra ở hàm main. Method update() được đồng bộ khiến cho các thread có thể chiếm lấy toàn bộ object mà nó đang tham chiếu đến(Val object được tạo ở hàm main).
Ở trên là ví dụ về synchronized method của instance, trong một chương trình chúng ta có thể có cả synchronized method instance và synchronized method class(synchronized static method). Vậy chúng ta khác nhau như thế nào? Nói chung synchronized instance method được sử dụng khi các tài nguyên thuộc về riêng cho mỗi instance, còn khi các tài nguyên thuộc về tất cả các instance(biến static) thì sử dụng synchronized method class(synchronized static nethod).
Ví dụ bạn tạo ra class có một static method dùng để đăng ký danh sách đăng ký các sinh viên, tất cả các object tạo ra từ class có thể sử dụng static method này, sổ sách sinh viên trong trường hợp này nên là một biến static Collection. Nếu static method này được sử dụng trong nhiều thread thì chúng ta nên sử dụng synchronized static method.
Tóm lược
Qua bài viết mình đã giới thiệu sơ lược về cơ chế đồng bộ hóa và cách sử dụng synchronized để đồng bộ hóa các thread. Tuy nhiên chung ta còn rất nhiều vấn đề trong xử lý bất đồng bộ! Hãy tưởng tượng chúng ta có đến 100 hay 1000 thread thì synchronized kiểu gì đây? Deadlock? Chúng ta sẽ cần nhau tìm hiểu ở những phần sau nha. Cảm ơn các bạn.
Nguồn tham khảo