Hướng dẫn Java Design Pattern – Object Pool

Trong OOP, một class có thể có rất nhiều instance nhưng ngược lại Singleton là một dạng class mà chỉ hỗ trợ tối đa một instance duy nhất và một đối tượng khi đã được khởi tạo sẽ tồn tại suốt vòng đời chương trình. Trong một số trường hợp, chúng ta cần khởi tạo và sử dụng một tập hợp các đối tượng. Khi với số lượng lớn các đối tượng giống nhau, thì việc khởi tạo nhiều lần sẽ gây lãng phí không cần thiết. Chúng ta cũng có thể sử dụng Prototype Pattern để cãi thiện performance bằng cách cloning object. Tuy nhiên, không phải lúc nào object cũng có thể được clone đầy đủ. Trong những trường hợp như vậy, chúng ta có thể dùng Object pool pattern.

1. Object Pool Pattern là gì?

Object Pool is a creational design pattern. Object Pool Pattern says that “to reuse the object that are expensive to create”.

Object Pool Pattern là một trong những Creational pattern. Nó không nằm trong danh sách các Pattern được giới thiệu bởi GoF. Object Pool Pattern cung cấp một kỹ thuật để tái sử dụng objects thay vì khởi tạo không kiểm soát.

Ý tưởng của Object Pooling là: chúng ta dùng Object Pool Pattern quản lý một tập hợp các objects mà sẽ được tái sử dụng trong chương trình. Khi client cần sử dụng object, thay vì tạo ra một đối tượng mới thì client chỉ cần đơn giản yêu cầu Object pool lấy một đối tượng đã có sẵn trong object pool. Sau khi object được sử dụng nó sẽ không hủy mà sẽ được trả về pool cho client khác sử dụng. Nếu tất cả các object trong pool được sử dụng thì client phải chờ cho tới khi object được trả về pool.

Object pool thông thường hoạt động theo kiểu: tự tạo đối tượng mới nếu chưa có sẵn hoặc khởi tạo trước 1 object pool chứa một số đối tượng hạn chế trong đó.

2. Cài đặt Object Pool Pattern như thế nào?

2.1. Cài đặt

Một Object Pool Pattern bao gồm các thành phần cơ bản sau:

  • Client : một class yêu cầu khởi tạo đối tượng PooledObject để sử dụng.
  • PooledObject : một class mà tốn nhiều thời gian và chi phí để khởi tạo. Một class cần giới hạn số lượng đối tượng được khởi tạo trong ứng dụng.
  • ObjectPool : đây là lớp quan trọng nhất trong Object Pool Pattern. Lớp này lưu giữ danh sách các PooledObject đã được khởi tạo, đang được sử dụng. Nó cung cấp các phương thức cho việc lấy đối tượng từ Pool và trả đối tượng sau khi sử dụng về Pool.

2.2. Ví dụ Object Pool thông qua ứng dụng Taxi

Một hãng taxi A chỉ hữu hạn N chiếc taxi, hãng taxi chịu trách nhiệm quản lý trạng thái các xe (đang rảnh hay đang chở khách), phân phối các xe đang rảnh đi đón khách, chăm sóc, kéo dài thời gian chờ đợi của khách hàng cho trong trường hợp tất cả các xe đều đang bận (để chờ một trong số các xe đó rảnh thì điều đi đón khách luôn), hủy khi việc chờ đợi của khách hàng là quá lâu.

Ta mô phỏng và thiết kế thành các lớp sau:

  • Taxi: đại diện cho một chiếc taxi, là một class định nghĩa các thuộc tính và phương thức của một taxi.
  • TaxiPool: Đại diện cho công ty taxi, có:
    • Phương thức getTaxi() : để lấy về một thể hiện Taxi đang ở trạng thái rảnh, có thể throw ra một exception nếu chờ lâu mà không lấy được thể hiện.
    • Phương thức release() : để trả thể hiện Taxi về Pool sau khi đã phục vụ xong.
    • Thuộc tính available : lưu trữ danh sách Taxi rãnh, đang chờ phục vụ.
    • Thuộc tính inUse : lưu trữ danh sách Taxi đang bận phục vụ.
  • ClientThread: đại diện cho khách hàng sử dụng dịch vụ Taxi, mô phỏng việc gọi, chở và trả khách.

Trong đoạn code bên dưới, tôi sẽ cài đặt mô phỏng với TaxiPool quản lý được 4 taxi, cùng lúc có 8 cuộc gọi của khách hàng đến công ty để gọi xe, thời gian mỗi taxi đến địa điểm chở khách là 200ms, mỗi taxi chở khách trong khoảng thời gian từ 1000ms đến 1500ms (ngẫu nhiên), mỗi khách hàng chịu chờ tối đa 1200ms trước khi hủy.

Taxi:

package com.maixuanviet.patterns.creational.objecpool.taxi;

public class Taxi {

	private String name;

	public Taxi(String name) {
		super();
		this.name = name;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	@Override
	public String toString() {
		return "Taxi [name=" + name + "]";
	}
}

TaxiPool:

package com.maixuanviet.patterns.creational.objecpool.taxi;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * Lazy pool
 * 
 * @author maixuanviet
 */
public class TaxiPool {

	private static final long EXPIRED_TIME_IN_MILISECOND = 1200; // 1.2s
	private static final int NUMBER_OF_TAXI = 4;

	private final List<Taxi> available = Collections.synchronizedList(new ArrayList<>());
	private final List<Taxi> inUse = Collections.synchronizedList(new ArrayList<>());

	private final AtomicInteger count = new AtomicInteger(0);
	private final AtomicBoolean waiting = new AtomicBoolean(false);

	public synchronized Taxi getTaxi() {
		if (!available.isEmpty()) {
			Taxi taxi = available.remove(0);
			inUse.add(taxi);
			return taxi;
		}
		if (count.get() == NUMBER_OF_TAXI) {
			this.waitingUntilTaxiAvailable();
			return this.getTaxi();
		}
		Taxi taxi = this.createTaxi();
		inUse.add(taxi);
		return taxi;
	}

	public synchronized void release(Taxi taxi) {
		inUse.remove(taxi);
		available.add(taxi);
		System.out.println(taxi.getName() + " is free");
	}

	private Taxi createTaxi() {
		waiting(200); // The time to create a taxi
		Taxi taxi = new Taxi("Taxi " + count.incrementAndGet());
		System.out.println(taxi.getName() + " is created");
		return taxi;
	}

	private void waitingUntilTaxiAvailable() {
		if (waiting.get()) {
			waiting.set(false);
			throw new TaxiNotFoundException("No taxi available");
		}
		waiting.set(true);
		waiting(EXPIRED_TIME_IN_MILISECOND);
	}
	
	private void waiting(long numberOfSecond) {
		try {
			TimeUnit.MILLISECONDS.sleep(numberOfSecond);
		} catch (InterruptedException e) {
			e.printStackTrace();
			Thread.currentThread().interrupt();
		}
	}
}

ClientThread:

package com.maixuanviet.patterns.creational.objecpool.taxi;

import java.util.Random;
import java.util.concurrent.TimeUnit;

public class ClientThread implements Runnable {
	
	private TaxiPool taxiPool;
	
	public ClientThread(TaxiPool taxiPool) {
		this.taxiPool = taxiPool;
	}

	@Override
	public void run() {
		takeATaxi();
	}

	private void takeATaxi() {
		try {
			System.out.println("New client: " + Thread.currentThread().getName());
			Taxi taxi = taxiPool.getTaxi();
			
			TimeUnit.MILLISECONDS.sleep(randInt(1000, 1500)); 
			
			taxiPool.release(taxi);
			System.out.println("Served the client: " + Thread.currentThread().getName());
		} catch (InterruptedException | TaxiNotFoundException e) {
			System.out.println(">>>Rejected the client: " + Thread.currentThread().getName());
		}
	}
	
	public static int randInt(int min, int max) {
	    return new Random().nextInt((max - min) + 1) + min;
	}
}

TaxiNotFoundException:

package com.maixuanviet.patterns.creational.objecpool.taxi;

public class TaxiNotFoundException extends RuntimeException {

	private static final long serialVersionUID = -6670953536653728443L;

	public TaxiNotFoundException(String message) {
		System.out.println(message);
	}
}

TaxiApp:

package com.maixuanviet.patterns.creational.objecpool.taxi;

public class TaxiApp {

	public static final int NUM_OF_CLIENT = 8;

	public static void main(String[] args) {
		TaxiPool taxiPool = new TaxiPool();
		for (int i = 1; i <= NUM_OF_CLIENT; i++) {
			Runnable client = new ClientThread(taxiPool);
			Thread thread = new Thread(client);
			thread.start();
		}
	}
}
&#91;/code&#93;
<!-- /wp:shortcode -->

<!-- wp:paragraph -->
<p>Kết quả thực thi chương trình trên:</p>
<!-- /wp:paragraph -->

<!-- wp:shortcode -->

New client: Thread-0
New client: Thread-1
New client: Thread-2
New client: Thread-3
New client: Thread-4
New client: Thread-5
New client: Thread-6
New client: Thread-7
Taxi 1 is created
Taxi 2 is created
Taxi 3 is created
Taxi 4 is created
Taxi 1 is free
Served the client: Thread-1
Taxi 2 is free
Served the client: Thread-7
No taxi available
>>>Rejected the client: Thread-0
Taxi 3 is free
Served the client: Thread-6
Taxi 4 is free
Served the client: Thread-5
Taxi 2 is free
Served the client: Thread-4
Taxi 1 is free
Served the client: Thread-3
Taxi 3 is free
Served the client: Thread-2

Nhận xét:

  • Ưu điểm của việc cài đặt Pool là việc tận dụng được các tài nguyên đã được cấp phát. Với ví dụ về taxi ở trên với 4 taxi, trong nhiều trường hợp vẫn có thể đáp ứng được nhiều hơn 4 yêu cầu cùng một lúc. Nó làm tăng hiệu năng hệ thống ở điểm không cần phải khởi tạo quá nhiều thể hiện (trong nhiều trường hợp việc khởi tạo này mấy nhiều thời gian), tận dụng được các tài nguyên đã được khởi tạo (tiết kiệm bộ nhớ, không mất thời gian hủy đối tượng).
  • Việc cài đặt Pool có thể linh động hơn nữa bằng cách đặt ra 2 giá trị N và M. Trong đó: N là số lượng thể hiện tối thiểu (trong những lúc rảnh rỗi), M là số thể hiện tối đa (lúc cần huy động nhiều thể hiện nhất mà phần cứng đáp ứng được). Sau khi qua trạng thái cần nhiều thể hiện, Pool có thể giải phóng bớt một số thể hiện không cần thiết.

2.3. Ví dụ Object Pool thông qua Connection Pooling

Khi làm việc với cơ sở dữ liệu hay cho những hệ thống tương đối lớn ở các công ty, thì vấn đề performance rất quan trọng. Nếu mỗi request đến chúng ta phải mở và đóng kết nối thủ công thì rất khó quản lý, điều quan trọng hơn nữa đó là cứ mỗi lần open và close connection mất khoảng từ 2-3s thì chắc chắn rằng hiệu năng hoạt động của ứng dụng web không tốt. Để giải quyết được vấn đề này, chúng ta sẽ dùng kỹ thuật connection pool để quản lý và chia sẻ số kết nối. Connection Pool cũng là một trong các ứng dụng của Object Pool Pattern.

2.3.1. Connection pooling là gì?

Connection pool (vùng kết nối) : là kỹ thuật cho phép tạo và duy trì 1 tập các kết nối dùng chung nhằm tăng hiệu suất cho các ứng dụng bằng cách sử dụng lại các kết nối khi có yêu cầu thay vì việc tạo kết nối mới.

2.3.2. Cách làm việc của Connection pooling?

Connection Pool Manager (CPM) là trình quản lý vùng kết nối, một khi ứng dụng được chạy thì Connection pool tạo ra một vùng kết nối, trong vùng kết nối đó có các kết nối do chúng ta tạo ra sẵn. Và như vậy, một khi có một request đến thì CPM kiểm tra xem có kết nối nào đang rỗi không? Nếu có nó sẽ dùng kết nối đó còn không thì nó sẽ đợi cho đến khi có kết nối nào đó rỗi hoặc kết nối khác bị timeout. Kết nối sau khi sử dụng sẽ không đóng lại ngay mà sẽ được trả về CPM để dùng lại khi được yêu cầu trong tương lai.

2.3.3. Ví dụ

Một connection pool có tối đa 10 connection trong pool. Bây giờ user kết nối tới database (DB), hệ thống sẽ kiểm tra trong connection pool có kết nối nào đang rảnh không?

  • Trường hợp chưa có kết nối nào trong connection pool hoặc tất cả các kết nối đều bận (đang được sử dụng bởi user khác) và số lượng connection trong connection < 10 thì sẽ tạo một connection mới tới DB để kết nối tới DB đồng thời kết nối đó sẽ được đưa vào connection pool.
  • Trường hợp tất cả các kết nối đang bận và số lượng connection trong connection pool = 10 thì người dùng phải đợi cho các user dùng xong để được dùng.

Sau khi một kết nối được tạo và sử dụng xong nó sẽ không đóng lại mà sẽ duy trì trong connection pool để dùng lại cho lần sau và chỉ thực sự bị đóng khi hết thời gian timeout (lâu quá không dùng đến nữa).

Chi tiết các bạn tham khảo thêm tại link sau: https://ejbvn.wordpress.com/category/week-2-entity-beans-and-message-driven-beans/day-09-using-jdbc-to-connect-to-a-database/

Source code về cách tạo Connection Pool các bạn tham khảo thêm tại đây: https://sourcemaking.com/design_patterns/object_pool/java

2.4. Ví dụ Object Pool thông qua Thread Pool

Thread Pool cũng là một trong các ứng dụng của Object Pool Pattern.

Tạo ra một Thread mới là một hoạt động tốn kém bởi vì nó đòi hỏi hệ điều hành cung cấp tài nguyên để có thể thực thi task (tác vụ). ThreadPool được dùng để giới hạn số lượng Thread được chạy bên trong ứng dụng của chúng ta trong cùng một thời điểm.

Thay vì tạo các luồng mới khi các task (nhiệm vụ) mới đến, một ThreadPool sẽ giữ một số luồng nhàn rỗi (no task) đã sẵn sàng để thực hiện tác vụ nếu cần. Sau khi một thread hoàn thành việc thực thi một tác vụ, nó sẽ không chết. Thay vào đó nó vẫn không hoạt động trong ThreadPool và chờ đợi được lựa chọn để thực hiện nhiệm vụ mới.

2.5. Một vài lưu ý khi triển khai Object Pool

2.5.1. Xác định số lượng tối đa các đối tượng được khởi tạo trong Pool?

Tùy vào ứng dụng, chúng ta cần xác định con số này sao cho hợp lý để đảm bảo không khởi tạo quá dư thừa đối tượng gây lãng phí tài nguyên, hay quá ít làm cho các ứng dụng client phải chờ lâu hay bị lỗi.

2.5.2. Thời gian timeout?

Để quản lý thời gian timeout bạn cần xác định:

  • Khi một đối tượng không được sử dụng trong một thời gian xác định có cần thiết hủy bỏ để giải phóng tài nguyên hay không? Chẳng hạn: nếu giới hạn số lượng tối thiểu là 4, số lượng tối đa là 100. Điều này có nghĩa là có ít nhất 4 đối tượng sẵn dùng trong Object Pool, tối đa là 100 đối tượng được tạo ra và được quản lý trong pool. Đối tượng không được sử dụng sau khoảng thời gian timeout, thì sẽ được hủy bỏ cho tới khi còn lại 4 đối tượng.
  • Khi một client giữ một object quá lâu mà không trả về object pool thì có cần thiết set timeout để trả về cho đối tượng khác sử dụng không? Chẳng hạn: một client1 cần sử dụng object trong khoảng thời gian 10 phút, một client2 cần sử dụng trong 20 giây. Khi client1 yêu cầu sử dụng trước, nếu không set timeout thì client2 phải chờ 10 phút mới được sử dụng trong 20 giây.
  • Khi một client chờ quá lâu thì sẽ xử lý như thế nào? Chờ đến khi có tài nguyên sử dụng hay sẽ throw ngoại lệ.

2.5.3. Làm gì khi Pool không chứa đối tượng nào?

Chúng ta có thể sử dụng một trong ba chiến lược để xử lý một yêu cầu từ client khi trong object pool không chứa đối tượng nào (rỗng):

  • Tạo mới: khởi tạo thêm một đối tượng mới và trả về cho client nếu nó chưa vượt quá số lượng đối tượng được phép khởi tạo.
  • Chờ: Trong một môi trường đa luồng, một object pool có thể block các yêu cầu từ client cho đến khi một luồng khác trả về một đối tượng có thể sử dụng vào object pool.
  • Trả lỗi: không cung cấp một đối tượng và ngay lập tức trả lại lỗi cho client. Hoặc chờ một khoảng thời gian (timeout) và trả lại lỗi cho client.

2.5.4. Đảm bảo trạng thái của object không bị thay đổi khi trả về Object Pool?

Khi triển khai mô hình Object pool, chúng ta phải cẩn thận để đảm bảo rằng trạng thái của các đối tượng quay trở lại object pool phải được đặt ở trạng thái hợp lý cho việc sử dụng tiếp theo của đối tượng. Nếu không kiểm soát được điều này, đối tượng sẽ thường ở trong một số trạng thái mà chương trình client không mong đợi và có thể làm cho chương trình client lỗi (failed), không nhất quán, rò rỉ thông tin.

3. Lợi ích của Object Pool Pattern là gì?

  • Tăng hiệu suất của ứng dụng.
  • Hiệu quả trong một vài tình huống mà tốc độ khởi tạo một object là cao.
  • Quản lý các kết nối và cung cấp một cách để tái sử dụng và chia sẻ chúng.
  • Có thể giới hạn số lượng tối đa các đối tượng có thể được tạo ra.

4. Sử dụng Object Pool Pattern khi nào?

Objects pool được sử dụng khi:

  • Khi cần tạo và hủy một số lượng lớn các đối tượng trong thời gian ngắn, liên tục.
  • Khi cần sử dụng các object tương tự thay vì khởi tạo một object mới không có kiểm soát.
  • Các đối tượng tốn nhiều chi phí để tạo ra.
  • Khi có một số client cần cùng một tài nguyên tại các thời điểm khác nhau.

Một vài thư viện sử dụng Object Pool trong Java:

Như vậy là chúng ta đã đi qua một số Design Pattern về Creational pattern. Trong các bài viết tiếp theo chúng ta sẽ cùng tìm hiểu về Structuaral Pattern.