Use Java ThreadLocal with caution

  Pi Ke        2015-11-03 07:31:57       23,704        0          English  简体中文  Tiếng Việt 

Theo tài liệu của Oracle, ThreadLocal là một lớp cung cấp các biến cục bộ của luồng. Các biến này khác với các biến thông thường ở chỗ mỗi luồng truy cập vào một biến (thông qua phương thức get hoặc set của nó) đều có bản sao riêng, được khởi tạo độc lập của biến đó. Các thể hiện ThreadLocal thường là các trường tĩnh riêng tư trong các lớp muốn liên kết trạng thái với một luồng. Tóm lại, các biến ThreadLocal là các biến thuộc về một luồng, không phải là một lớp hoặc một thể hiện của một lớp.

Một cách sử dụng phổ biến của ThreadLocal là khi bạn muốn truy cập một số đối tượng không an toàn cho luồng trong các luồng mà không cần sử dụng các cơ chế đồng bộ hóa như khối synchronized và khóa. Các biến này sẽ không chia sẻ trạng thái giữa các luồng khác nhau, vì vậy không có vấn đề đồng bộ hóa; trong khi đó, do mỗi luồng chỉ có một thể hiện của đối tượng ThreadLocal, nên nó tiết kiệm bộ nhớ.

Ví dụ, lớp bên dưới tạo ra các định danh duy nhất cục bộ cho mỗi luồng. ID của luồng được gán lần đầu tiên khi nó gọi ThreadId.get() và không thay đổi trong các lần gọi tiếp theo.

 import java.util.concurrent.atomic.AtomicInteger;

 public class ThreadId {
     // Số nguyên nguyên tử chứa ID luồng tiếp theo cần gán
     private static final AtomicInteger nextId = new AtomicInteger(0);

     // Biến cục bộ luồng chứa ID của mỗi luồng
     private static final ThreadLocal threadId =
         new ThreadLocal() {
             @Override protected Integer initialValue() {
                 return nextId.getAndIncrement();
         }
     };

     // Trả về ID duy nhất của luồng hiện tại, gán nếu cần
     public static int get() {
         return threadId.get();
     }
 }

Mỗi luồng giữ một tham chiếu ngầm đến bản sao của biến cục bộ luồng miễn là luồng còn hoạt động và thể hiện ThreadLocal có thể truy cập được; sau khi một luồng kết thúc, tất cả các bản sao của các thể hiện cục bộ luồng của nó đều bị thu gom rác (trừ khi có các tham chiếu khác đến các bản sao này).

ThreadLocal sẽ giữ tham chiếu đến đối tượng mà nó tham chiếu miễn là luồng còn hoạt động, nên có nguy cơ rò rỉ bộ nhớ trong một số ứng dụng, đặc biệt là trong các ứng dụng Java EE. Như trong các ứng dụng Java EE, để cải thiện hiệu suất, nhóm luồng sẽ được sử dụng nhiều khả năng, điều này có nghĩa là một luồng sẽ không bị chấm dứt khi nó hoàn thành nhiệm vụ của mình, thay vào đó nó sẽ được trả về nhóm luồng và chờ yêu cầu khác. Điều này có nghĩa là nếu một lớp có đối tượng ThreadLocal được định nghĩa và nó được tải trong luồng, thì ThreadLocal sẽ không bao giờ bị GC cho đến khi ứng dụng kết thúc. Điều này sẽ gây ra rò rỉ bộ nhớ. Vì vậy, cách tốt nhất là dọn dẹp tham chiếu ThreadLocal bằng cách gọi phương thức remove() của ThreadLocal.

Nếu bạn không dọn dẹp khi hoàn tất, bất kỳ tham chiếu nào mà nó giữ đến các lớp được tải là một phần của ứng dụng web được triển khai sẽ vẫn nằm trong heap vĩnh viễn và sẽ không bao giờ được thu gom rác. Triển khai/hủy triển khai lại ứng dụng web sẽ không dọn dẹp tham chiếu của mỗi Thread đến lớp(các lớp) của ứng dụng web của bạn vì Thread không phải là thứ thuộc sở hữu của ứng dụng web của bạn. Mỗi lần triển khai tiếp theo sẽ tạo ra một thể hiện mới của lớp sẽ không bao giờ được thu gom rác.

Bạn sẽ gặp phải các ngoại lệ hết bộ nhớ do java.lang.OutOfMemoryError: PermGen space và sau khi tìm kiếm trên Google, có thể chỉ tăng -XX:MaxPermSize thay vì sửa lỗi.

Dưới đây là một ví dụ đơn giản về cách rò rỉ bộ nhớ có thể xảy ra khi sử dụng ThreadLocal. Bản demo này sẽ mô phỏng một container tạo ra một vài luồng để xử lý nhiều yêu cầu. Mỗi yêu cầu sẽ tham chiếu đến một đối tượng ThreadLocal

Đầu tiên, chúng ta sẽ tạo một đối tượng nặng có tham chiếu đến một trường có kích thước 10MB và nó sẽ được xử lý bởi mỗi yêu cầu được tạo sau đó.

public class HeavyObject {
	int limit = 10*1024*1024;
	byte[] bytes = new byte[limit];

	public HeavyObject(){
		for(int j=0; j<limit; j++){
			bytes[j] = 10;
		}
	}
}

Tiếp theo, chúng ta sẽ tạo một lớp yêu cầu sẽ được chuyển đến luồng worker và được xử lý.

public class Request {
	ThreadLocal local = new ThreadLocal();
	
	public void setLocal(HeavyObject object){
		local.set(object);;
	}
	
	public void showLocal(){
		System.out.println(local.get());
	}
}

Tiếp theo sẽ là lớp WorkerThread để xử lý tất cả các yêu cầu.

public class WorkerThread implements Runnable{
	private ThreadLocal local = new ThreadLocal<Byte[]>();
	private volatile boolean isStopped = false;
	
	public void run(){
		System.out.println("Đang chạy...");
		
		while(!isStopped){
			try {
				System.out.println(local.get());
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
	}
	
	public void setList(byte[] bytes){
		local.set(bytes);
	}
	
	public void handleRequest(Request request){
		request.showLocal();
	}
	
	public void end(){
		isStopped = true;
	}
}

Cuối cùng, chúng ta sẽ tạo lớp kiểm thử chính.

public class ThreadLocalTest {
	public static void main(String[] args){
		int count = 5;
		int i = 0;
		long totalEatenMemory = 0;
		long maxEatenMemory = Long.MIN_VALUE;
		long minEatenMemory = Long.MAX_VALUE;
		
		long startTime = System.nanoTime();
		while(++i <= count){
			long eatenMmeory = test();
			if(eatenMmeory > maxEatenMemory){
				maxEatenMemory = eatenMmeory;
			}
			if(eatenMmeory < minEatenMemory){
				minEatenMemory = eatenMmeory;
			}
			totalEatenMemory += eatenMmeory;
		}
		long endTime = System.nanoTime();
		
		System.out.println("Số lần chạy thử nghiệm : "+count+"; Trung bình bộ nhớ đã dùng : "+(totalEatenMemory*1.0/(count*1000))+" KB");
		System.out.println("Bộ nhớ đã dùng tối đa : "+maxEatenMemory);
		System.out.println("Bộ nhớ đã dùng tối thiểu : "+minEatenMemory);
		System.out.println("Tổng thời gian đã trôi qua : "+((endTime-startTime)*1.0/1000000L)+"ms");
		System.out.println("Hoàn tất chạy");
	}
	
	private static long test(){
		int count = 200;
		int i = 0;
		
		WorkerThread worker = new WorkerThread();
		Thread thread = new Thread(worker);
		thread.start();
		try{
			long initialMemory = Runtime.getRuntime().freeMemory();
			while(++i <= count){
				System.out.println("Bộ nhớ trống "+Runtime.getRuntime().freeMemory()+" trước khi đếm "+i);
				HeavyObject object = new HeavyObject();
				System.out.println("Bộ nhớ trống "+Runtime.getRuntime().freeMemory()+" sau khi đếm "+i);
				Request request = new Request();
				request.setLocal(object);
				worker.handleRequest(request);
				System.out.println("Bộ nhớ trống "+Runtime.getRuntime().freeMemory()+" tại đếm "+i);
			}
			long endMemory = Runtime.getRuntime().freeMemory();
			long eatenMemory = (initialMemory-endMemory);
			System.out.println("Bộ nhớ đã dùng : "+eatenMemory);
			return eatenMemory;
		} catch (Exception ex){
			ex.printStackTrace();
		} finally {
			worker.end();
		}
		return 0;
	}
}

Lớp kiểm thử chính sẽ tạo ra một vài WorkerThread và mỗi WorkerThread sẽ xử lý một vài yêu cầu tham chiếu đến một số đối tượng ThreadLocal. Vì WorkerThread không bao giờ dừng lại trước khi thoát chương trình này, nên tất cả các đối tượng ThreadLocal được tạo sẽ tồn tại trong heap và heap bị chiếm dụng ngày càng nhiều với các yêu cầu được tạo ra và cuối cùng bạn sẽ gặp OutOfMemoryError.

Bây giờ để so sánh, chúng ta tạo một lớp NormalLocal sẽ được tham chiếu trong Request. Lần này, không có ThreadLocal nào được tạo trong lớp Request.

public class NormalLocal {
	HeavyObject object = null;
	
	public void set(HeavyObject object){
		this.object = object;
	}
	
	public Object get(){
		return this.object;
	}
}

Bây giờ lớp Request mới trông như thế này:

public class Request {
	NormalLocal local = new NormalLocal();
	
	public void setLocal(HeavyObject object){
		local.set(object);;
	}
	
	public void showLocal(){
		System.out.println(local.get());
	}
}

Với thay đổi này, sẽ không còn xảy ra OutOfMemoryError nữa vì bộ nhớ được GC khi yêu cầu không còn được sử dụng nữa.

Nếu bạn gặp phải các vấn đề này, bạn có thể xác định luồng và lớp nào đang giữ các tham chiếu này bằng cách sử dụng Trình phân tích bộ nhớ của Eclipse.

Vì vậy, hãy cẩn thận khi bạn sử dụng ThreadLocal để làm việc dễ dàng hơn. Nếu không, không chỉ bộ nhớ của bạn mà chính bạn cũng có thể bị nó "cắn".

THREADLOCAL  JAVA  MEMORY LEAK 

       

  RELATED


  0 COMMENT


No comment for this article.



  RANDOM FUN

This is how you check publications today