🧯Synchronized

Synchronized trong Java là một từ khóa (keyword) được sử dụng để đồng bộ hóa các phương thức hoặc khối lệnh trong môi trường đa luồng (multithreading).

Khi một phương thức hoặc khối lệnh được khai báo là synchronized, nó chỉ có thể được thực thi bởi một luồng (thread) duy nhất vào một thời điểm. Các thread trong Java khác cần phải chờ đợi cho đến khi luồng hiện tại kết thúc thực thi phương thức hoặc khối lệnh đó mới có thể thực thi.

Một số đặc điểm nổi bật của Synchronized trong Java:

  • Đảm bảo tính đúng đắn của dữ liệu

  • Đảm bảo an toàn của dữ liệu

  • Tăng hiệu suất

  • Không đảm bảo tuân thủ thứ tự

Tại sao cần sử dụng từ khóa Synchronized trong Java?

Trong môi trường đa luồng, khi nhiều luồng cùng truy cập vào các tài nguyên chung như các biến, đối tượng, file,… thì có thể xảy ra các lỗi như race condition, deadlock, hoặc các giá trị không đồng bộ (inconsistent) của dữ liệu. Điều này có thể dẫn đến các lỗi không mong muốn, và làm ảnh hưởng đến tính đúng đắn và hiệu suất của chương trình.

Để giải quyết vấn đề này, Java cung cấp từ khóa synchronized để đảm bảo rằng chỉ một luồng có thể truy cập vào phương thức hoặc khối mã được đánh dấu synchronized tại một thời điểm. Điều này giúp giảm thiểu xung đột và đảm bảo tính ổn định của ứng dụng.

Cách hoạt động của Synchronized trong Java

Khi một luồng truy cập vào một phương thức hoặc khối mã được đánh dấu synchronized, nó sẽ lấy khóa (lock) trên đối tượng chứa phương thức hoặc khối mã đó. Khi luồng này giữ khóa, không luồng nào khác có thể truy cập vào phương thức hoặc khối mã synchronized của đối tượng đó. Sau khi luồng hoàn thành công việc, nó sẽ giải phóng khóa và cho phép luồng khác tiếp tục truy cập.

Để hiểu rõ hơn, hãy tưởng tượng khóa như là một chìa khóa mở cửa. Chỉ có một chìa khóa và chỉ một người có thể sử dụng chìa khóa đó để mở cửa vào một thời điểm. Khi một người đang sử dụng chìa khóa, người khác phải chờ đợi cho đến khi người đầu tiên hoàn thành công việc của mình và trả lại chìa khóa.

Ngoài ra, trong Java cũng hỗ trợ khóa đồng bộ hóa (synchronized block), cho phép đồng bộ hóa chỉ một phần của một phương thức hoặc khối lệnh. Khi sử dụng synchronized block, chỉ phần được khai báo trong khối lệnh đó mới được đồng bộ hóa, giúp tăng hiệu suất của chương trình.

Process Synchronization và Thread Synchronization trong Java

Process Synchronization trong Java

Process Synchronization là quá trình đồng bộ hóa các tiến trình (process) độc lập trong một hệ thống đa tiến trình. Process Synchronization thường được thực hiện bằng các phương pháp như Semaphore (truy cập vào một tài nguyên cụ thể), Mutex (truy cập vào một tài nguyên duy nhất), Monitor (truy cập vào các phương thức hoặc biến được đánh dấu là đồng bộ hóa), hay Barrier ( đồng bộ hóa quá trình thực hiện của các tiến trình).

Thread Synchronization trong Java

Trong khi process synchronization tập trung vào việc đồng bộ hóa các tiến trình độc lập, thread synchronization liên quan đến việc đồng bộ hóa các luồng (thread) trong một tiến trình. Thread Synchronization thường được thực hiện bằng việc sử dụng các từ khóa như synchronized hoặc ReentrantLock. Thread Synchronization trong Java có thể được chia làm 3 loại chính:

String Synchronization

Chuỗi (string) trong Java là một đối tượng không thể thay đổi (immutable). Điều này có nghĩa là bạn không thể thay đổi giá trị của một chuỗi sau khi nó được tạo ra. Đồng bộ hóa chuỗi là việc sử dụng chuỗi làm khóa trong phương thức hoặc khối mã synchronized.

Tuy nhiên, sử dụng đồng bộ hóa chuỗi không được khuyến khích, vì nó có thể gây ra các vấn đề liên quan đến hiệu suất và tiềm ẩn rủi ro về deadlock. Thay vào đó, bạn nên sử dụng các đối tượng khác làm khóa để đồng bộ hóa mã nguồn.

Mutual Exclusion

Mutual Exclusion là một khái niệm quan trọng trong đồng bộ hóa. Nó đảm bảo rằng chỉ có một luồng có thể truy cập vào một phần mã hoặc tài nguyên chia sẻ tại một thời điểm. Điều này giúp tránh xung đột và đảm bảo tính ổn định của ứng dụng.

Trong Java, từ khóa synchronized được sử dụng để đạt được mutual exclusion. Khi một luồng truy cập vào phương thức hoặc khối mã được đánh dấu synchronized,

Inter-Thread Communication

Trong lập trình đa luồng, đôi khi cần thiết cho các luồng phải giao tiếp với nhau để hoàn thành một công việc chung. Java cung cấp một số phương thức để thực hiện giao tiếp giữa các luồng, bao gồm wait(), notify() và notifyAll().

Phân loại Synchronized trong Java

Trong Java, có ba loại synchronized: synchronized method, synchronized block và static synchronization.

Synchronized method trong Java

Synchronized method là một phương thức được đánh dấu với từ khóa synchronized. Khi một luồng truy cập vào synchronized method, nó sẽ lấy khóa trên đối tượng chứa phương thức đó.

Cách định nghĩa và sử dụng synchronized method

Để định nghĩa một synchronized method, sử dụng từ khóa synchronized trước kiểu trả về của phương thức:

public synchronized void methodName() {
// code here

}

Khi sử dụng synchronized method, hãy lưu ý rằng chỉ nên áp dụng cho các phương thức liên quan đến truy cập vào tài nguyên chia sẻ giữa các luồng. Việc sử dụng synchronized method không cần thiết sẽ làm giảm hiệu suất của ứng dụng.

Ví dụ minh họa


class Example {
private int count = 0;

public synchronized void increment() {

count++;

}

public synchronized int getCount() {

return count;

}

}

Lớp Example có hai phương thức increment() và getCount() được khai báo là synchronized. Điều này đảm bảo rằng các thao tác tăng giá trị và truy xuất giá trị của biến count sẽ được thực hiện đồng bộ hóa và chỉ có một luồng được phép truy cập vào phương thức này vào một thời điểm.

Synchronized block trong Java

Synchronized block là một khối mã được đánh dấu với từ khóa synchronized. Điều này cho phép đồng bộ hóa một phần nhỏ của mã thay vì toàn bộ phương thức, giúp cải thiện hiệu suất.

Cách định nghĩa và sử dụng synchronized block

Để định nghĩa một synchronized block, sử dụng từ khóa synchronized theo sau bởi một đối tượng được sử dụng làm khóa:

synchronized (object) {
// Khối lệnh được bảo vệ bởi synchronized

}

Synchronized block được sử dụng để đồng bộ hóa chỉ một phần của một phương thức hoặc một khối lệnh, giúp tăng hiệu suất của chương trình.

Nó cũng cho phép các đối tượng khác nhau được sử dụng để đồng bộ hóa, giúp tránh tình trạng blocking ở mức đối tượng toàn cục (global lock) và giảm thiểu sự cạnh tranh truy cập vào khối lệnh được bảo vệ.

Ví dụ minh họa

class Example {
private Object lock = new Object();

private int count = 0;

public void increment() {

synchronized (lock) {

count++;

}

}

public int getCount() {

synchronized (lock) {

return count;

}

}

}

Lớp Example có hai phương thức increment() và getCount() được đồng bộ hóa bằng synchronized block.

Static Synchronization

Static synchronization trong Java là cơ chế đồng bộ hóa được áp dụng cho các phương thức hoặc khối lệnh static, sử dụng từ khóa synchronized với đối tượng khóa (lock object) là lớp hoặc đối tượng Class.

Cách định nghĩa và sử dụng static synchronization

Để định nghĩa một phương thức tĩnh được đồng bộ hóa, sử dụng từ khóa synchronized trước kiểu trả về của phương thức tĩnh:

public static synchronized void methodName() {
// code here

}

Để sử dụng static synchronization với một khối mã, bạn cần chỉ định lớp làm khóa:

public static void methodName() {
synchronized (MyClass.class) {

// code here

}

}

Ví dụ minh hoạ

public class Example {

private static int count = 0;

public static synchronized void increment() {

count++;

}

public static synchronized int getCount() {

return count;

}

}

Hai phương thức increment() và getCount() được đánh dấu là static và synchronized.

  • Khi sử dụng synchronized, hãy cẩn thận với việc lựa chọn đối tượng khóa (lock object). Sử dụng cùng một đối tượng khóa cho các tài nguyên không liên quan có thể dẫn đến hiệu suất kém và deadlock.

  • Tránh sử dụng đồng bộ hóa chuỗi (string synchronization) vì điều này có thể gây ra các vấn đề về hiệu suất và deadlock.

  • Hạn chế sử dụng synchronized trong phương thức toàn cục, điều này có thể giảm hiệu suất và khả năng mở rộng của ứng dụng.

Vấn đề khi sử dụng synchronized trong Java

Synchronized là một từ khóa quan trọng trong Java được sử dụng để đồng bộ hóa quy trình và tránh xung đột dữ liệu giữa các luồng (threads). Tuy nhiên, khi sử dụng synchronized trong Java, có thể xảy ra một số vấn đề như sau:

Hiệu suất và khả năng mở rộng ứng dụng

Việc sử dụng từ khóa synchronized có thể ảnh hưởng đến hiệu suất và khả năng mở rộng của ứng dụng. Khi một phương thức hoặc khối mã được đánh dấu synchronized, chỉ một luồng có thể truy cập vào nó tại một thời điểm, điều này có thể gây ra đợi chờ cho các luồng khác muốn truy cập vào phương thức hoặc khối mã đó, dẫn đến giảm hiệu suất. Ngoài ra, việc sử dụng synchronized không phải lúc nào cũng là cách tối ưu nhất để đảm bảo tính đồng bộ hóa.

Để giảm thiểu ảnh hưởng của synchronized đến hiệu suất, bạn có thể áp dụng đồng bộ hóa tối thiểu. Thay vì sử dụng synchronized trên toàn bộ phương thức, bạn chỉ nên đồng bộ hóa những phần mã quan trọng, cần phải được bảo vệ khỏi truy cập đồng thời của nhiều luồng.

Lỗi Deadlock

Deadlock là một tình huống xảy ra khi hai hoặc nhiều luồng chờ đợi nhau để giải phóng các khóa (lock) mà chúng đang nắm giữ, dẫn đến việc các luồng bị treo vô thời hạn và không thể tiếp tục thực thi. Deadlock thường xảy ra khi các luồng sử dụng đồng bộ hóa không đúng cách trong môi trường đa luồng.

Để giải quyết vấn đề này, chúng ta cần đảm bảo rằng các luồng luôn được yêu cầu khóa theo cùng một thứ tự. Nếu các luồng yêu cầu khóa không theo cùng một thứ tự, chúng ta có thể sử dụng phương thức tryLock() thay vì sử dụng synchronized.

Lỗi Starvation

Starvation trong Java là một trạng thái mà một luồng không thể tạo ra hoặc tiếp tục thực hiện một nhiệm vụ vì đang bị luồng khác chiếm giữ tài nguyên mà nó cần. Việc này có thể xảy ra khi một số luồng được ưu tiên hơn so với các luồng khác và luôn chiếm giữ tài nguyên cần thiết, dẫn đến các luồng khác không có đủ tài nguyên để hoàn thành công việc của chúng.

Để giải quyết vấn đề này, chúng ta cần sử dụng phương thức wait() và notify() hoặc notifyAll() để đồng bộ hóa các luồng. Điều này giúp các luồng có thể chờ đợi và nhận khóa khi chúng có sẵn.

Livelock

Livelock xảy ra khi hai hoặc nhiều luồng tạo ra sự phản hồi liên tục cho nhau mà không thực sự làm gì, chúng có thể bị kẹt trong một vòng lặp vô tận. Điều này gây ra một trạng thái gọi là livelock và có thể ảnh hưởng đến hiệu suất của ứng dụng..

Để giải quyết vấn đề này, chúng ta cần sử dụng các phương thức khác nhau để đồng bộ hóa các luồng và tránh việc chúng tạo ra sự phản hồi liên tục cho nhau.

Các phương pháp đồng bộ hóa khác

Ngoài synchronized, Java còn cung cấp một số phương pháp đồng bộ hóa khác như:

  • ReentrantLock: Một khóa đồng bộ hóa cho phép cùng một luồng giữ khóa nhiều lần và cung cấp các tính năng như fairness policy, interruptible lock acquisition và timeout.

  • AtomicInteger: Một lớp số nguyên nguyên tử (atomic) hỗ trợ các thao tác nguyên tử trên số nguyên, giúp đảm bảo tính đồng bộ hóa mà không cần sử dụng synchronized.

  • ConcurrentHashMap: Một lớp bản đồ (map) hỗ trợ đồng bộ hóa mà không cần khóa toàn bộ cấu trúc dữ liệu, giúp cải thiện hiệu suất.

Phương pháp

Ưu điểm

Nhược điểm

Trường hợp sử dụng

synchronized

Dễ sử dụng, tự động giải phóng khóa khi hoàn thành

Có thể gây ra deadlock, hiệu suất thấp hơn

Khi đồng bộ hóa dữ liệu trong một luồng

Khi đồng bộ hóa truy cập vào tài nguyên chung

ReentrantLock

Cung cấp các tính năng nâng cao, linh hoạt

Phức tạp hơn, cần giải phóng khóa thủ công

Khi cần tùy chỉnh hơn về thời gian chờ đợi khi không lấy được lock

Khi cần tạo nhiều điều kiện để đồng bộ hóa

AtomicInteger

Hiệu suất cao, không cần sử dụng khóa

Chỉ hỗ trợ số nguyên

Khi cần giới hạn số lượng luồng được phép truy cập vào tài nguyên cùng một thời điểm

ConcurrentHashMap

Hiệu suất cao hơn

Không có tính năng sorting và strict consistency

Khi cần đợi một hay nhiều luồng khác thực hiện xong trước khi bắt đầu thực thi

Khi lựa chọn phương pháp đồng bộ hóa, bạn cần xem xét các yếu tố như hiệu suất, tính linh hoạt và độ phức tạp của mã nguồn. Trong một số trường hợp, việc sử dụng các phương pháp đồng bộ hóa khác như ReentrantLock hay ConcurrentHashMap có thể mang lại hiệu suất cao hơn và tránh được một số vấn đề liên quan đến việc sử dụng từ khóa synchronized.

Last updated