Thread Safety (Luồng an toàn) trong Java và đồng bộ hóa ( Synchronized) các luồng trong java
Thread Safety trong Java là một chủ đề rất quan trọng. Java cung cấp hỗ trợ môi trường đa luồng bằng cách sử dụng Thread Java, chúng ta biết rằng nhiều luồng được tạo từ cùng một biến đối tượng chia sẻ dữ liệu và điều này có thể dẫn đến sự không nhất quán dữ liệu khi các luồng được sử dụng để đọc và cập nhật dữ liệu được chia sẻ.Trong bài viết này, chúng ta sẽ tìm hiểu về Thread Safety và cách tạo Thread Safety bằng đồng bộ hóa ( Synchronized) trong Java
Thread Safety trong Java là một chủ đề rất quan trọng. Java cung cấp hỗ trợ môi trường đa luồng bằng cách sử dụng Thread Java, chúng ta biết rằng nhiều luồng được tạo từ cùng một biến đối tượng chia sẻ dữ liệu và điều này có thể dẫn đến sự không nhất quán dữ liệu khi các luồng được sử dụng để đọc và cập nhật dữ liệu được chia sẻ
Thread Safety ( An toàn luồng)
Lý do cho sự không nhất quán dữ liệu là bởi vì việc cập nhật bất kỳ giá trị trường nào là một quá trình, nó đòi hỏi ba bước; đầu tiên để đọc giá trị hiện tại, thứ hai để thực hiện các thao tác cần thiết để có được giá trị cập nhật và thứ ba để gán giá trị cập nhật cho tham chiếu trường.
Hãy kiểm tra điều này với một chương trình đơn giản trong đó nhiều luồng đang cập nhật dữ liệu được chia sẻ.
package com.journaldev.threads;
public class ThreadSafety {
public static void main(String[] args) throws InterruptedException {
ProcessingThread pt = new ProcessingThread();
Thread t1 = new Thread(pt, "t1");
t1.start();
Thread t2 = new Thread(pt, "t2");
t2.start();
//wait for threads to finish processing
t1.join();
t2.join();
System.out.println("Processing count="+pt.getCount());
}
}
class ProcessingThread implements Runnable{
private int count;
@Override
public void run() {
for(int i=1; i < 5; i++){
processSomething(i);
count++;
}
}
public int getCount() {
return this.count;
}
private void processSomething(int i) {
// processing some job
try {
Thread.sleep(i*1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
Trong chương trình vòng lặp ở trên, số lượng được tăng thêm 1 bốn lần và vì chúng ta có hai luồng, giá trị của nó phải là 8 sau khi cả hai luồng thực hiện xong. Nhưng khi bạn sẽ chạy chương trình trên nhiều lần, bạn sẽ thấy giá trị đếm thay đổi trong khoảng 6,7,8.
Thread Safety trong Java
Thread Safety trong java là quá trình làm cho chương trình của chúng ta an toàn để sử dụng trong môi trường đa luồng, có nhiều cách khác nhau để chúng ta có thể làm cho luồng chương trình của mình an toàn.
- Đồng bộ hóa (Synchronization) là công cụ dễ dàng nhất và được sử dụng rộng rãi nhất cho Thread Safety trong java.
- Sử dụng các lớp Atomic Wrapper từ gói java.util.concurrent.atomic. Ví dụ: AtomicInteger
- Sử dụng các khóa từ gói java.util.concurrent.locks.
- Sử dụng các lớp thread safe collection ( bộ sư tập luồng an toàn) , xem bài viết này sử dụng ConcurrentHashMap để biết safe thread.
- Sử dụng từ khóa “volatile” với các biến để làm cho mọi luồng đọc dữ liệu từ bộ nhớ, không đọc từ bộ đệm của luồng.
Đồng bộ hóa Java (Java Synchronized)
Đồng bộ hóa là công cụ sử dụng để chúng ta có thể đạt được sự an toàn của luồng, JVM đảm bảo rằng mã được đồng bộ hóa sẽ chỉ được thực hiện bởi một luồng tại một thời điểm. Từ khóa synchronized được sử dụng để tạo mã được đồng bộ hóa và bên trong nó sử dụng các khóa trên Object hoặc Class để đảm bảo chỉ có một luồng đang thực thi mã được đồng bộ hóa.
- Đồng bộ hóa hoạt động trên việc khóa và mở khóa tài nguyên trước khi bất kỳ luồng nào đi vào mã được đồng bộ hóa, nó phải thu được khóa trên Object và khi việc thực thi mã kết thúc, nó sẽ mở khóa tài nguyên có thể bị khóa bởi các luồng khác. Trong khi đó, các luồng khác đang ở trạng thái chờ để khóa tài nguyên được đồng bộ hóa.
- Chúng ta có thể sử dụng từ khóa synchronized theo hai cách, một là tạo ra một phương thức hoàn chỉnh được đồng bộ hóa và một cách khác là tạo khối đồng bộ hóa.
- Khi một phương thức được đồng bộ hóa, nó sẽ khóa Object , nếu phương thức tĩnh, nó sẽ khóa Class , do đó, tốt nhất là sử dụng khối được đồng bộ hóa để khóa các phần duy nhất của phương thức cần đồng bộ hóa.
- Trong khi tạo một khối được đồng bộ hóa, chúng ta cần cung cấp tài nguyên mà khóa sẽ được lấy, đó có thể là XYZ. Class hoặc bất kỳ trường Object nào của lớp.
- synchronized(this) sẽ khóa Object trước khi vào khối được đồng bộ hóa.
- Bạn nên sử dụng mức khóa thấp nhất , ví dụ, nếu có nhiều khối được đồng bộ hóa trong một lớp và một trong số chúng đang khóa Đối tượng, thì các khối được đồng bộ hóa khác cũng sẽ không có sẵn để thực hiện bởi các luồng khác. Khi chúng ta khóa một Object, nó sẽ có được khóa trên tất cả các trường của Object.
- Đồng bộ hóa cung cấp tính toàn vẹn dữ liệu về chi phí hiệu năng, do đó, nó chỉ được sử dụng khi thực sự cần thiết.
- Đồng bộ hóa chỉ hoạt động trong cùng một JVM, vì vậy nếu bạn cần khóa một số tài nguyên trong nhiều môi trường JVM, nó sẽ không hoạt động và bạn có thể phải quan tâm một số cơ chế khóa toàn cầu.
- Đồng bộ hóa có thể dẫn đến bế tắc, xem bài đăng về bế tắc trong java và cách tránh chúng .
- Từ khóa synchronized không thể được sử dụng cho các hàm tạo và biến.
- Tốt nhất là tạo một Đối tượng riêng giả để sử dụng cho khối được đồng bộ hóa để tham chiếu của nó không thể bị thay đổi bởi bất kỳ mã nào khác. Ví dụ: nếu bạn có một phương thức setter cho Object mà bạn đang đồng bộ hóa, thì tham chiếu của nó có thể được thay đổi bởi một số mã khác dẫn đến việc thực thi song song của khối được đồng bộ hóa.
- Chúng ta không nên sử dụng bất kỳ đối tượng nào được duy trì trong một nhóm không đổi, ví dụ Chuỗi không nên được sử dụng để đồng bộ hóa vì nếu bất kỳ mã nào khác cũng bị khóa trên cùng một Chuỗi, nó sẽ cố gắng lấy khóa trên cùng một đối tượng tham chiếu từ nhóm Chuỗi và mặc dù cả hai mã không liên quan, chúng sẽ khóa lẫn nhau.
Dưới đây là những thay đổi mã chúng ta cần thực hiện trong chương trình trên để làm cho nó an toàn theo luồng.
//dummy object variable for synchronization
private Object mutex=new Object();
...
//using synchronized block to read, increment and update count value synchronously
synchronized (mutex) {
count++;
}
Chúng ta hãy xem một số ví dụ đồng bộ hóa và những gì chúng ta có thể học hỏi từ chúng.
public class MyObject {
// Locks on the object's monitor
public synchronized void doSomething() {
// ...
}
}
// Hackers code
MyObject myObject = new MyObject();
synchronized (myObject) {
while (true) {
// Indefinitely delay myObject
Thread.sleep(Integer.MAX_VALUE);
}
}
Lưu ý rằng mã của hacker đang cố gắng khóa phiên bản myObject và một khi nó bị khóa, nó sẽ không bao giờ giải phóng nó khiến phương thức doSomething () bị chặn khi chờ khóa, điều này sẽ khiến hệ thống gặp bế tắc và gây ra từ chối dịch vụ ( DoS).
public class MyObject {
public Object lock = new Object();
public void doSomething() {
synchronized (lock) {
// ...
}
}
}
//untrusted code
MyObject myObject = new MyObject();
//change the lock Object reference
myObject.lock = new Object();
Lưu ý rằng đối tượng khóa là công khai và bằng cách thay đổi tham chiếu của nó, chúng ta có thể thực thi song song khối được đồng bộ hóa trong nhiều luồng. Một trường hợp tương tự là đúng nếu bạn có Object riêng nhưng có phương thức setter để thay đổi tham chiếu của nó.
public class MyObject {
//locks on the class object's monitor
public static synchronized void doSomething() {
// ...
}
}
// hackers code
synchronized (MyObject.class) {
while (true) {
Thread.sleep(Integer.MAX_VALUE); // Indefinitely delay MyObject
}
}
Lưu ý rằng mã hacker đang bị khóa trên màn hình lớp và không giải phóng nó, nó sẽ gây ra bế tắc và DoS trong hệ thống.
Dưới đây là một ví dụ khác trong đó nhiều luồng đang hoạt động trên cùng một chuỗi Chuỗi và sau khi được xử lý, nối thêm tên luồng vào giá trị mảng.
package com.journaldev.threads;
import java.util.Arrays;
public class SyncronizedMethod {
public static void main(String[] args) throws InterruptedException {
String[] arr = {"1","2","3","4","5","6"};
HashMapProcessor hmp = new HashMapProcessor(arr);
Thread t1=new Thread(hmp, "t1");
Thread t2=new Thread(hmp, "t2");
Thread t3=new Thread(hmp, "t3");
long start = System.currentTimeMillis();
//start all the threads
t1.start();t2.start();t3.start();
//wait for threads to finish
t1.join();t2.join();t3.join();
System.out.println("Time taken= "+(System.currentTimeMillis()-start));
//check the shared variable value now
System.out.println(Arrays.asList(hmp.getMap()));
}
}
class HashMapProcessor implements Runnable{
private String[] strArr = null;
public HashMapProcessor(String[] m){
this.strArr=m;
}
public String[] getMap() {
return strArr;
}
@Override
public void run() {
processArr(Thread.currentThread().getName());
}
private void processArr(String name) {
for(int i=0; i < strArr.length; i++){
//process data and append thread name
processSomething(i);
addThreadName(i, name);
}
}
private void addThreadName(int i, String name) {
strArr[i] = strArr[i] +":"+name;
}
private void processSomething(int index) {
// processing some job
try {
Thread.sleep(index*1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
Đây là đầu ra khi ta chạy chương trình trên.
Time taken= 15005
[1:t2:t3, 2:t1, 3:t3, 4:t1:t3, 5:t2:t1, 6:t3]
Các giá trị mảng Chuỗi bị hỏng do dữ liệu được chia sẻ và không đồng bộ hóa. Đây là cách chúng ta có thể thay đổi phương thức addThreadName () để làm cho chương trình của chúng ta an toàn.
private Object lock = new Object();
private void addThreadName(int i, String name) {
synchronized(lock){
strArr[i] = strArr[i] +":"+name;
}
}
Sau thay đổi này, chương trình của chúng tôi hoạt động tốt và đây là đầu ra chính xác của chương trình.
Time taken= 15004
[1:t1:t2:t3, 2:t2:t1:t3, 3:t2:t3:t1, 4:t3:t2:t1, 5:t2:t1:t3, 6:t2:t1:t3]
Theo dõi VnCoder trên Facebook, để cập nhật những bài viết, tin tức và khoá học mới nhất!