Cách xử lý bất đồng bộ java

Tài khoản ngân hàng của công ty X có 10 triệu. Tại một thời điểm, giám đốc rút thẻ thanh toán số tiền 5 triệu sau một bữa giao lưu với đối tác. Cùng lúc đó, tại văn phòng, nhân viên tài chính chuyển 7 triệu để mua sắm chuẩn bị cho sự kiện ngày mai. Chuyện gì sẽ xảy ra? Nếu cả hai giao dịch trên đều được thực hiện thì tổng cộng công ty X đã chi hết 12 triệu trong khi tài khoản mình chỉ có 10 triệu. Ngân hàng sẽ chịu lỗ 2 triệu.

Bài toán trên cũng chính là một ví dụ kinh điển mỗi khi chúng ta đề cập đến khía cạnh đối lập nhau là đồng bộ [Synchronized]bất đồng bộ [Unsynchronized]. Hai khái niệm đối lập nhau này cũng chính là chủ đề để chúng ta cùng tìm hiểu sâu hơn trong bài viết này.

Lý do phát sinh vấn đề

Nhìn lại bài toán ngân hàng, chúng ta có thể dễ dàng nhận ra được mấu chốt để phát sinh việc công ty X dùng quá số tiền so với tài khoản hiện có chính là việc có 2 giao dịch cùng một thời điểm.

Giả dụ nếu người giám đốc hoàn tất thanh toán tiền vào lúc 12 giờ trưa, người nhân viên tài chính chuyển tiền vào lúc 3 giờ chiều, lúc này tài khoản công ty đã cập nhật còn 5 triệu, thì sẽ không có việc nhầm lẫn gì ở đây. Đa luồng [Multi-thread] cùng truy cập vào một tài nguyên chính là nguyên nhân gây ra sự sai lệch của giả dụ đầu bài.

Do đây không phải là bài chuyên về luồng [Thread] nên mình sẽ chỉ nói về cách mà các luồng gây sai lệch. 

Ví dụ dưới đây sẽ giúp các bạn có cái nhìn rõ ràng hơn. Mình sẽ mô tả lại trường hợp ví dụ một cách tương đối. Bằng cách tạo ra hai luồng khác nhau cùng truy cập vào một tài nguyên, mình liên tục kiểm tra số tiền hiện tại rồi trừ đi một và in ra giá trị còn lại.

public class TestThread {
	   public static void main[String[] args] {
		    count c = new count[];
		    // Tạo 2 thread truy cập vào cùng tài nguyên trong 1 đối tượng
		    Thread t1 = new Thread[new Access["Thread 1", c]];
		    Thread t2 = new Thread[new Access["Thread 2", c]];
		    t1.start[];
		    t2.start[];
	    }
}

    class count {
	    int value = 10;
    }

class Access implements Runnable {
	String name;
	count c;

	public Access[String name, count c] {
		this.name = name;
		this.c = c;
	}

    //Có thể đặt từ khóa synchronized vào đây để xem kết quả sau khi đồng bộ
	public [synchronized] void run[] {
		for [int i = 0; i < 3; i++] {
			System.out.println[name + " " + "index " + i + " before: " + c.value];
			c.value--;
			System.out.println[name + " " + "index " + i + " after: " + c.value];
		 }
	}
}
​        

Sau khi chạy đoạn code trên, mình nhận được kết quả như sau.

Chúng ta có thể thấy được ở lượt thứ 0 của Thread 2 đã xảy ra lỗi. Giá trị ban đầu vốn là 10, sau khi trừ đi 1 thì lại chỉ còn 8.

Lý do là bởi vì trong thời gian giữa lệnh in ra giá trị ban đầu và lệnh in ra giá trị sau khi trừ của Thread 2 thì Thread 1 đã thực hiện giảm giá trị của biến value.

Việc sử dụng đa luồng giúp cho việc thực thi các hành động diễn ra nhanh hơn bằng cách để các luồng chạy đan xen lẫn nhau. Nhưng cũng chính vì đó mà nếu không cẩn thận, đa luồng sẽ ảnh hưởng đến việc thực thi hành động của luồng khác thông qua các tài nguyên dùng chung như biến value ở chương trình mẫu bên trên.

Để tránh việc gây nhiễu giữa các luồng với nhau, khái niệm đồng bộbất đồng bộ đã ra đời.

Bất đồng bộ cũng tựa như cách mà chương trình mẫu bên trên thực hiện, mặc kệ các luồng thích làm gì thì làm.

Đồng bộ, phiên bản trái ngược với bất đồng bộ, giữ cho giá trị của các tài nguyên dùng chung không bị sai lệch.

Bạn đã từng đi qua trạm thu phí bao giờ chưa? Trong quá trình tham gia giao thông, các phương tiện có thể vượt mặt nhau để đi trước nhưng khi đến trạm thu phí thì các xe phải xếp hàng vượt trạm từng chiếc một. Xong chiếc này thì sẽ đến chiếc khác.

Quy tắc của đồng bộ cũng như vậy, bằng cách gọi từ khóa synchronized, một bộ phận tài nguyên sẽ bị khóa lại chỉ cho phép một luồng sử dụng trong một thời điểm. Khi nào luồng đó thực hiện xong, quyền sử dụng tài nguyên sẽ được giao lại cho luồng khác.

Các kiểu đồng bộ và làm sao để thực hiện chúng?

Việc thực hiện đồng bộ có thể được thực hiện trên đối tượng [đồng bộ phương thức, đồng bộ khối] và trên lớp.

  1. Đồng bộ phương thức khóa một bộ phận tài nguyên thuộc về đối tượng. Trong một thời điểm, chỉ có một luồng có quyền tương tác với phương thức được đánh dấu đồng bộ. Một đối tượng có thể chứa nhiều phương thức khác nhau, không phải tất cả mọi phương thức đều bị đồng bộ. Các phương thức không đồng bộ có thể được truy cập đa luồng như thường.

    class X{
        // Phương thức method1 được gọi đồng bộ 
        // Chỉ được truy cập bởi một luồng vào một thời điểm 
    	synchronized void method1[] {
    	    //Thực hiện một đoạn lệnh
    	}
    
    	// Phương thức method2 không được đồng bộ
    	// Chấp nhận đa luồng
    	void method2[]	{
    	    //Thực hiện một đoạn lệnh
        }
    }
                    
  2. Đồng bộ khối về cơ bản giống với đồng bộ phương thức nhưng thay vì đặt nguyên một phương thức vào trạng thái khóa thì chỉ đặt đoạn lệnh nào có khả năng gây sai lệch.

    class Y{
    	void method[]{
    	    //Đoạn lệnh không cần đồng bộ
    
    	    // từ khóa this có tác dụng thông báo đồng bộ dựa trên đối tượng hiện tại
    	    synchronized[this]{
    	        // Đoạn lệnh cần đồng bộ
            }
    
            //Đoạn lệnh không cần đồng bộ
         }
    }
                    
  3. Trong khi đó thì đồng bộ lớp khóa toàn bộ tài nguyên của lớp đó. Tất cả phương thức bên trong đó chỉ có thể được luồng hiện tại sử dụng. Bên dưới sẽ là ví dụ cho cách dùng này.

    public class TestThread {
        public static void main[String[] args] {
            MyThread t1 = new MyThread["Thread 1_1", 1];
            MyThread t2 = new MyThread["Thread 1_2", 10];
            MyThread t3 = new MyThread["Thread 1_3", 100];
            t1.start[];
            t2.start[];
            t3.start[];
        }
    }
    
    class Table {
        synchronized static void print[String name, int n] {
            for [int i = 1; i 

Chủ Đề