Bài 4: Đồng bộ hóa(Synchronized) Thread

Khi nhiều thread cùng tương tác với một đối tượng, bạn cần phải điều khiển chúng một cách cẩn thận để tránh tranh chấp tài nguyên. Bài này giới thiệu những vấn đề có giới thiệu các lỗi thường gặp trong ứng dụng của bạn, khi nào và làm thế nào để sử dụng từ khóa synchronized để điều khiển việc truy cập vào các đối tượng và các biến cùng một thời điểm.

Việc bổ xung từ khóa synchronized vào khai báo phương thức nhằm đảm bảo chỉ có một thread được phép ở bên trong phương thức tại một thời điểm. Trước khi bạn học cách làm thế nào để định nghĩa một method đồng bộ hóa trong chương trình của bạn, hãy xem những gì xảy ra nếu việc đồng bộ hoá không được sử dụng trong một chương trình.

Ví dụ 1: mô phỏng 2 thread cùng truy cập đồng thời vào một phương thức của cùng một đối tượng.

1: public class BothInMethod extends Object {

2:     private String objID;

3:

4:     public BothInMethod(String objID) {

5:         this.objID = objID;

6:     }

7:

8:     public void doStuff(int val) {

9:         print(“entering doStuff()”);

10:         int num = val * 2 + objID.length();

11:         print(“in doStuff() – local variable num=” + num);

12:

13:         // slow things down to make observations

14:         try { Thread.sleep(2000); }

catch ( InterruptedException x ) { }

15:

16:         print(“leaving doStuff()”);

17:     }

18:

19:     public void print(String msg) {

20:         threadPrint(“objID=” + objID + “ – “ + msg);

21:     }

22:

23:     public static void threadPrint(String msg) {

24:         String threadName = Thread.currentThread().getName();

25:         System.out.println(threadName + “: “ + msg);

26:     }

27:

28:     public static void main(String[] args) {

29:         final BothInMethod bim = new BothInMethod(“obj1”);

30:

31:         Runnable runA = new Runnable() {

32:                 public void run() {

33:                     bim.doStuff(3);

34:                 }

35:             };

36:

37:         Thread threadA = new Thread(runA, “threadA”);

38:         threadA.start();

39:

40:         try { Thread.sleep(200); }

catch ( InterruptedException x ) { }

41:

42:         Runnable runB = new Runnable() {

43:                 public void run() {

44:                     bim.doStuff(7);

45:                 }

46:             };

47:

48:         Thread threadB = new Thread(runB, “threadB”);

49:         threadB.start();

50:     }

51: }

Trong phương thức main(), đối tượng BothInMethod được khởi tạo với một identifier là obj1(dòng 29). Tiếp theo, 2 thread được tạo ra để truy cập đồng thời vào phương thức doStuff(). Thread đầu tiên có tên là threadA, cái thứ hai là threadB. Sau khi threadA bắt đầu(dòng 38), nó gọi doStuff() và truyền vào giá trị 3(dòng 33). Khoảng 200 mili giây sau, threadB được bắt đầu và gọi phương thức doStuff() trên cùng một đối tượng và truyền vào giá trị 7.

Cả threadA và threadB cùng ở trong phương thức doStuff()(từ dòng 8->17) tại cùng một thời điểm; threadA vào trước, sau 200 mili giây thì threadB vào. Bên trong doStuff(), biến cục bộ num được tính toán thông qua tham số val và biến thành viên objID(dòng 10). Bởi vì, threadA và threadB đều gán một biến val khác nhau, nên giá trị biến num sẽ khác nhau cho mỗi thread. Phương thức sleep() được sử dụng trong doStuff() nhằm làm chậm lại để đảm bảo rằng cả hai thread đều ở trong cùng một phương thức của cùng một đối tượng một cách đồng thời.

Lưu ý: Nếu hai hay nhiều thread đều ở trong một phương thức đồng thời, thì mỗi thread phải có mỗi bảo sao chép các biến cục bộ.

Kết quả từ ví dụ trên:

threadA: objID=obj1 – entering doStuff()

threadA: objID=obj1 – in doStuff() – local variable num=10

threadB: objID=obj1 – entering doStuff()

threadB: objID=obj1 – in doStuff() – local variable num=18

threadA: objID=obj1 – leaving doStuff()

threadB: objID=obj1 – leaving doStuff()

Các thread được đồng bộ hoá trong Java sử dụng thông qua một monitor. Hãy nghĩ rằng, một monitor là một object cho phép một thread truy cập vào một tài nguyên. Chỉ có một thread sử dụng một monitor vào bất kỳ một khoảng thời gian nào. Các lập trình viên nói rằng, các thread sở hữu monitor vào thời gian đó. Monitor cũng được gọi là một semaphore.

Như chúng ta đã biết, khái niệm semaphore (monitor được Tony Hoare đề xuất) thường được sử dụng để điều khiển đồng bộ các hoạt động truy cập vào những tài nguyên dùng chung. Một luồng muốn truy cập vào một tài nguyên dùng chung (như biến dữ liệu) thì trước tiên nó phải yêu cầu để có được monitor riêng. Khi có được monitor thì luồng như có được “chìa khóa” để “mở cửa” vào miền “tranh chấp” (tài nguyên dùng chung) để sử dụng những tài nguyên đó.

Cơ chế monitor thực hiện hai nguyên tắc đồng bộ chính:

  • Không một luồng nào khác được phân monitor khi có một luồng đã yêu cầu và đang chiếm giữ. Những luồng có yêu cầu monitor sẽ phải chờ cho đến khi monitor được giải phóng.
  • Khi có một luồng giải phóng (ra khỏi) monitor, một trong số các luồng đang chờ monitor có thể truy cập vào tài nguyên dùng chung tương ứng với monitor đó.

Cuối cùng, tác nhiệm của việc yêu cầu một monitor xảy ra đằng sau “màn chắn” trong Java. Java xử lý tất cả các chi tiết đó cho bạn. Bạn phải đồng bộ hoá các thread trong chương trình của bạn nếu như có nhiều hơn một thread sử dụng cùng một tài nguyên.

Trong lập trình có hai cách để thực hiện đồng bộ:

  • Các hàm (hàm) được đồng bộ
  • Các khối được đồng bộ

1.Các hàm được đồng bộ(synchronized method)

Nếu method được đồng bộ hoá là một instance method(phân biệt với static method đối với class), thì method được đồng bộ hóa sẽ kích hoạt lock đi kèm với đối tượng của phương thức đó. Ngược lại, nếu method được đồng bộ hóa là static thì nó kích hoạt lock đi kèm với class định nghĩa method được đồng bộ hoá.

Ví dụ 2: Mô phỏng sử dụng từ khóa synchronized, và chỉ có một thread ở trong một phương thức tại một thời điểm.

1: public class OnlyOneInMethod extends Object {

2:     private String objID;

3:

4:     public OnlyOneInMethod(String objID) {

5:         this.objID = objID;

6:     }

7:

8:     public synchronized void doStuff(int val) {

9:         print(“entering doStuff()”);

10:         int num = val * 2 + objID.length();

11:         print(“in doStuff() – local variable num=” + num);

12:

13:         // slow things down to make observations

14:         try { Thread.sleep(2000); }

catch ( InterruptedException x ) { }

15:

16:         print(“leaving doStuff()”);

17:     }

18:

19:     public void print(String msg) {

20:         threadPrint(“objID=” + objID + “ – “ + msg);

21:     }

22:

23:     public static void threadPrint(String msg) {

24:         String threadName = Thread.currentThread().getName();

25:         System.out.println(threadName + “: “ + msg);

26:     }

27:

28:     public static void main(String[] args) {

29:         final OnlyOneInMethod ooim = new OnlyOneInMethod(“obj1”);

30:

31:         Runnable runA = new Runnable() {

32:                 public void run() {

33:                     ooim.doStuff(3);

34:                 }

35:             };

36:

37:         Thread threadA = new Thread(runA, “threadA”);

38:         threadA.start();

39:

40:         try { Thread.sleep(200); }

catch ( InterruptedException x ) { }

41:

42:         Runnable runB = new Runnable() {

43:                 public void run() {

44:                     ooim.doStuff(7);

45:                 }

46:             };

47:

48:         Thread threadB = new Thread(runB, “threadB”);

49:         threadB.start();

50:     }

51: }

Ở ví dụ trên, ta thấy trong phương thức doStuff() chỉ có một thread tại một thời điểm; threadA đi vào(dòng 1) và đi ra(dòng 3) khỏi phương thức doStuff() trước khi threadB được phép vào(dòng 4). Việc sử dụng modifier synchronized bảo vệ phương thức doStuff() và chỉ cho phép một thread ở trong nó tại một thời điểm.

Dưới đây là kết quả của ví dụ 2, bạn có thể so sánh kết quả của ví dụ 1 để thấy sự khác biệt.

threadA: objID=obj1 – entering doStuff()

threadA: objID=obj1 – in doStuff() – local variable num=10

threadA: objID=obj1 – leaving doStuff()

threadB: objID=obj1 – entering doStuff()

threadB: objID=obj1 – in doStuff() – local variable num=18

threadB: objID=obj1 – leaving doStuff()

2.Các khối được đồng bộ(synchronized statement block)

Các khối được đồng bộ có thể được sử dụng khi toàn bộ phương thức không cần synchronized hoặc khi muốn nhận lock trên một đối tượng khác. Cách sử dụng nó như sau:

synchronized(obj){

// block code

}

Ví dụ:

class Client{

BankAccount account;

// …

public void updateTransaction(){

synchronized(account){       // (1) Khối đồng bộ

account.update();        // (2)

}

}

}

Ngoài ra, có thể sử dụng statement block để thay thế cho phương thức được synchronized như sau:

public synchronized void setPoint(int x, int y) {

this.x = x;

this.y = y;

}

Thay thế bằng statement block như sau:

public void setPoint(int x, int y) {

synchronized ( this ) {

this.x = x;

this.y = y;

}

}

Bạn thấy bài viết này thế nào?

Các bài liên quan:
Tìm hiểu Thread trong JAVA-Phần 3
Tìm hiểu Thread trong JAVA-Phần 5