Đa luồng (multithreading) và đa nhiệm (Concurrency)
- 20-11-2023
- Toanngo92
- 0 Comments
Mục lục
Multithreading và Đa Nhiệm
Một luồng thực hiện một công việc cụ thể và là đơn vị thực thi nhỏ nhất trong một chương trình. Trong khi đa nhiệm là khả năng của hệ điều hành thực hiện hai hoặc nhiều nhiệm vụ, multithreading có thể được định nghĩa là việc chạy đồng thời hai hoặc nhiều phần của cùng một chương trình. Đa nhiệm có thể dựa trên quá trình hoặc dựa trên luồng. Trong đa nhiệm dựa trên quá trình, hai hoặc nhiều chương trình chạy đồng thời. Trong đa nhiệm dựa trên luồng, một chương trình thực hiện hai hoặc nhiều nhiệm vụ cùng một lúc.
Có hai loại đa nhiệm cơ bản đang được sử dụng trong các hệ điều hành. Chúng là:
- Preemptive (Chia Thời Gian): Trong trường hợp này, hệ điều hành kiểm soát đa nhiệm bằng cách gán thời gian CPU cho mỗi chương trình đang chạy. Phương pháp này đã được sử dụng trong Windows 95 và 98.
- Cooperative (Hợp Tác): Trong phương pháp này, các ứng dụng liên quan tự nguyện nhường thời gian của họ cho nhau. Phương pháp này đã được sử dụng trong Windows 3.x.
Multithreading là một kỹ thuật tương tự như đa nhiệm và bao gồm việc tạo ra một hoặc nhiều luồng trong một chương trình để cho phép nhiều nhiệm vụ chạy đồng thời hoặc song song. Multithreading cũng hỗ trợ các tính năng sau:
- Quản lý nhiều nhiệm vụ đồng thời.
- Phân biệt giữa các nhiệm vụ có ưu tiên khác nhau.
- Cho phép giao diện người dùng vẫn phản ứng, trong khi cấp thời gian cho các nhiệm vụ nền.
Hãy xem xét một trang web có cả hình ảnh và văn bản là nội dung của nó. Khi trang web được tải lên máy tính của người dùng, cả hai nội dung đều phải được hiển thị cùng một lúc. Hiển thị hình ảnh hoặc văn bản là một nhiệm vụ riêng biệt nhưng phải xảy ra đồng thời. Đây là nơi multithreading đến hình.
Multithreading trong Java
Multithreading là một hình thức chuyên biệt của đa nhiệm. Sự khác biệt giữa multithreading và đa nhiệm đã được cung cấp trong Bảng dưới.
Multithreading | Đa Nhiệm |
---|---|
Trong chương trình đa luồng, hai hoặc nhiều luồng có thể chạy đồng thời. | Trong môi trường đa nhiệm, hai hoặc nhiều tiến trình chạy đồng thời. |
Multithreading đòi hỏi ít overhead. Trong trường hợp multithreading, các luồng là các tiến trình nhẹ. Các luồng có thể chia sẻ cùng một không gian địa chỉ và truyền thông giữa các luồng ít tốn kém hơn so với truyền thông giữa các tiến trình. | Đa nhiệm đòi hỏi nhiều overhead hơn. Các tiến trình là các nhiệm vụ nặng nề yêu cầu không gian địa chỉ riêng biệt. Giao tiếp giữa các tiến trình rất tốn kém và việc chuyển đổi ngữ cảnh từ một tiến trình sang tiến trình khác cũng tốn kém. |
Vai trò của Đa Luồng
Đa luồng là được sử dụng trong thực tế vì các lý do sau:
- Tăng Hiệu Suất trong Hệ Thống Một Bộ Xử Lý: Đa luồng nâng cao hiệu suất của các hệ thống một bộ xử lý bằng cách giảm thời gian đợi CPU không hoạt động. Trong một chương trình có một luồng, CPU có thể ở trạng thái không hoạt động khi đang chờ các thao tác I/O hoặc các nhiệm vụ khác hoàn thành. Đa luồng cho phép CPU chuyển sang một luồng khác và tận dụng công suất xử lý của nó trong các khoảng thời gian chờ đợi như vậy.
- Thực Hiện Nhanh Hơn: Đa luồng khuyến khích thực hiện nhanh hơn so với các ứng dụng với nhiều tiến trình. Điều này bởi vì các luồng trong một tiến trình chia sẻ không gian dữ liệu chung, làm cho giao tiếp và chia sẻ dữ liệu trở nên hiệu quả hơn. Ngược lại, các tiến trình có các bộ dữ liệu riêng biệt của mình, dẫn đến chi phí cao về giao tiếp.
- Xử Lý Song Song: Đa luồng giới thiệu khái niệm xử lý song song, trong đó nhiều luồng trong một ứng dụng có thể thực hiện đồng thời. Điều này đặc biệt hữu ích đối với các ứng dụng cần phục vụ một lượng lớn người dùng đồng thời. Mỗi luồng có thể xử lý một nhiệm vụ hoặc người dùng cụ thể, cho phép tăng tính phản hồi và tận dụng tốt nguồn lực.
Ngôn ngữ lập trình Java cung cấp hỗ trợ mạnh mẽ cho đa luồng thông qua lớp Thread
và giao diện Runnable
. Các tính năng này giúp nhà phát triển tạo ra các ứng dụng hiệu quả và linh hoạt có thể tận dụng tối đa tài nguyên hệ thống có sẵn.
Ví dụ:
/*
* Creating multiple threads using a class derived from Thread class
*/
package test;
// MultipleThreads is created as a subclass of the Thread class
public class MultipleThreads extends Thread {
// Variable to store the name of the thread
String name;
// This method of the Thread class is overridden to specify the action
// that will be done when the thread begins execution.
public void run() {
while (true) {
name = Thread.currentThread().getName();
System.out.println(name);
try {
// Sleep for 500 milliseconds
Thread.sleep(500);
} catch (InterruptedException e) {
break;
}
}
// End of while loop
}
// This is the entry point for the MultipleThreads class.
public static void main(String args[]) {
MultipleThreads t1 = new MultipleThreads();
MultipleThreads t2 = new MultipleThreads();
t1.setName("Thread1");
t2.setName("Thread2");
t1.start();
t2.start();
System.out.println("Number of threads running: " + Thread.activeCount());
}
}
Việc thực thi của luồng dừng lại ngay sau khi hoàn thành việc thực thi của phương thức run()
. Khi đã dừng, việc thực thi của luồng không thể khởi động lại bằng cách sử dụng phương thức start()
. Thêm vào đó, phương thức start()
không thể được gọi trên một luồng đã đang chạy. Điều này cũng sẽ gây ra một ngoại lệ có kiểu là IllegalThreadStateException
.
Luồng tiêu thụ một lượng lớn bộ nhớ (RAM); do đó, luôn khuyến nghị gán các tham chiếu về null
khi một luồng đã hoàn thành việc thực thi của nó. Nếu một đối tượng Thread
được tạo ra nhưng không gọi phương thức start()
, nó sẽ không đủ điều kiện để thu gom rác (garbage collection), ngay cả khi ứng dụng cơ sở đã loại bỏ tất cả các tham chiếu đến luồng.
Here is the corrected and translated version of your paragraph in Vietnamese:
Các Phương thức Khác của Lớp Thread
Lớp Thread
hỗ trợ các phương thức để kiểm tra trạng thái sống của một luồng và để làm cho luồng hiện tại đợi cho đến khi luồng gọi nó chấm dứt, tương ứng.
Phương thức isAlive()
Luồng được sử dụng để bắt đầu ứng dụng nên là luồng cuối cùng chấm dứt. Điều này chỉ ra rằng ứng dụng đã kết thúc. Điều này có thể đảm bảo bằng cách dừng thực thi của luồng chính trong khoảng thời gian dài hơn bên trong chính luồng chính. Ngoài ra, cần đảm bảo rằng tất cả các luồng con đã chấm dứt trước khi luồng chính. Tuy nhiên, làm thế nào để đảm bảo rằng luồng chính biết về trạng thái của các luồng khác? Có cách để tìm hiểu xem một luồng đã chấm dứt hay chưa. Đầu tiên, bằng cách sử dụng phương thức isAlive().
Một luồng được xem là sống khi nó đang chạy. Lớp Thread bao gồm một phương thức có tên là isAlive(). Phương thức này được sử dụng để tìm hiểu xem một luồng cụ thể có đang chạy hay không. Nếu luồng đó đang sống, thì giá trị boolean true
sẽ được trả về. Nếu phương thức isAlive() trả về false, điều đó hiểu là luồng đó đang ở trạng thái mới hoặc trạng thái đã chấm dứt.
Cú pháp:
public final boolean isAlive();
Ví dụ:
public class IsAliveDemo extends Thread {
public static void main(String args[]) {
IsAliveDemo obj = new IsAliveDemo();
Thread t = new Thread(obj);
t.start();
System.out.println("Thread is alive: " + t.isAlive());
t.interrupt();
System.out.println("Thread is alive: " + t.isAlive());
}
}
Đoạn mã trình bày cách sử dụng phương thức isAlive(). Phương thức trả về giá trị boolean là true hoặc false tùy thuộc vào việc luồng đang chạy hay đã chấm dứt. Mã sẽ trả về false vì luồng đang ở trạng thái mới và không chạy.
Phương thức join()
Phương thức join() làm cho luồng hiện tại phải đợi cho đến khi luồng mà nó gọi chấm dứt.
Phương thức join() thực hiện các hoạt động sau:
- Phương thức này cho phép xác định thời gian tối đa mà chương trình phải đợi cho đến khi một luồng cụ thể chấm dứt.
- Nó ném InterruptedException nếu một luồng khác gián đoạn nó.
- Luồng gọi phải đợi cho đến khi luồng được chỉ định chấm dứt.
Cú pháp:
public final void join()
Ví dụ:
/*
* Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license
* Click nbfs://nbhost/SystemFileSystem/Templates/Classes/Class.java to edit this template
*/
package com;
/**
*
* @author toan1
*/
public class ThreadJoinDemo extends Thread {
public static void main(String[] args) {
ThreadJoinDemo objTh = new ThreadJoinDemo();
Thread t = new Thread(objTh);
t.start();
System.out.println("Number of threads running: " + Thread.activeCount());
System.out.println("I am in the main and waiting for the thread to finish");
try {
t.join(); // objTh is a Thread object
} catch (InterruptedException e) {
System.out.println("Main thread is interrupted");
}
}
@Override
public void run() {
// Code to be executed in the new thread
}
}
Phương thức join() của lớp Thread có thêm hai phiên bản nạp chồng khác:
void join(long timeout)
Trong loại phương thức join() này, một đối số kiểu long được truyền vào. Thời gian chờ được đo bằng mili giây. Điều này buộc luồng phải đợi cho đến khi luồng được chỉ định hoàn tất hoặc cho đến khi
số mili giây đã cho trôi qua.
void join(long timeout, int nanoseconds)
Trong loại phương thức join() này, các đối số kiểu long và integer được truyền vào. Thời gian chờ được đưa ra bằng mili giây cộng với một lượng nano giây xác định. Điều này buộc luồng phải đợi cho đến khi luồng được chỉ định hoàn tất hoặc cho đến khi thời gian chờ đã được trôi qua.
Ví dụ:
/*
* Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license
* Click nbfs://nbhost/SystemFileSystem/Templates/Classes/Class.java to edit this template
*/
package com;
/**
*
* @author toan1
*/
/** ThreadDemo inherits from Runnable interface */
class ThreadJoinDemo2 implements Runnable {
String name;
Thread objTh;
/* constructor of the class */
public ThreadJoinDemo2(String str) {
name = str;
objTh = new Thread(this, name);
System.out.println("New Thread is starting");
objTh.start();
}
public void run() {
try {
for (int count = 0; count < 2; count++) {
System.out.println(name + ": " + count);
Thread.sleep(1000);
}
} catch (InterruptedException e) {
System.out.println(name + " interrupted");
}
System.out.println(name + " exiting");
}
public static void main(String[] args) {
ThreadJoinDemo2 objNew1 = new ThreadJoinDemo2("one");
ThreadJoinDemo2 objNew2 = new ThreadJoinDemo2("two");
ThreadJoinDemo2 objNew3 = new ThreadJoinDemo2("three");
System.out.println("First thread is alive: " + objNew1.objTh.isAlive());
System.out.println("Second thread is alive: " + objNew2.objTh.isAlive());
System.out.println("Third thread is alive: " + objNew3.objTh.isAlive());
System.out.println("In the main method, waiting for the threads to finish");
try {
objNew1.objTh.join();
objNew2.objTh.join();
objNew3.objTh.join();
} catch (InterruptedException e) {
System.out.println("Main thread is interrupted");
}
System.out.println("First thread is alive: " + objNew1.objTh.isAlive());
System.out.println("Second thread is alive: " + objNew2.objTh.isAlive());
System.out.println("Third thread is alive: " + objNew3.objTh.isAlive());
System.out.println("Main thread is over and exiting");
}
}
Trong mã nguồn, ba đối tượng luồng được tạo trong phương thức main(). Phương thức isAlive() được gọi bởi ba đối tượng luồng để kiểm tra xem chúng còn sống hay đã chết. Sau đó, phương thức join() được gọi bởi mỗi đối tượng luồng. Phương thức join() đảm bảo rằng luồng chính là luồng cuối cùng chấm dứt.
Cuối cùng, phương thức isAlive() được gọi lại để kiểm tra xem các luồng vẫn còn sống hay đã chết.
Đồng bộ hóa Luồng
Trong các chương trình đa luồng, một số luồng có thể cùng một lúc cố gắng cập nhật cùng một tài nguyên, như một tệp. Điều này để lại tài nguyên trong trạng thái không xác định hoặc không nhất quán. Điều này được gọi là tình trạng đua (race condition).
Tình trạng đua (Race Conditions)
Nói chung, tình trạng đua trong một chương trình xảy ra khi:
- Hai hoặc nhiều luồng chia sẻ cùng một dữ liệu giữa chúng,
- Hai hoặc nhiều luồng cố gắng đọc và ghi dữ liệu chia sẻ cùng một lúc.
Tình trạng đua có thể được tránh bằng cách sử dụng các khối đồng bộ hóa. Đây là một khối mã được đánh dấu bằng từ khóa synchronized.
Các Khối và Phương thức Đồng bộ hóa
Hãy tưởng tượng một tình huống nơi mọi người đang đứng trong một hàng đợi ngoài một bốt điện thoại. Họ muốn thực hiện cuộc gọi và đang chờ đến lượt của họ. Điều này tương tự như cách đồng bộ hóa truy cập dữ liệu, nơi mỗi luồng muốn truy cập dữ liệu đợi đến lượt của nó. Tuy nhiên, quay lại ví dụ mà chúng ta đã mô tả trước đó, nếu không có hàng đợi và mọi người được phép vào một cách ngẫu nhiên, hai hoặc nhiều người sẽ cố gắng vào trong điện thoại cùng một lúc, dẫn đến sự hỗn loạn và hỗn độn. Điều này tương tự như một tình trạng đua có thể xảy ra với các luồng. Khi hai luồng cố gắng truy cập và thay đổi cùng một đối tượng và để lại đối tượng trong trạng thái không xác định, một tình trạng đua xảy ra. Java cung cấp từ khóa synchronized
để giúp tránh những tình huống như vậy. Khái niệm cốt lõi trong đồng bộ hóa với luồng Java là một điều được gọi là ‘monitor’. Một monitor là một đoạn mã được bảo vệ bởi một chương trình mutex gọi là mutex. Một tương tự trong đời sống thực cho một monitor có thể là cái bốt điện thoại đã mô tả trước đó, nhưng lần này có khóa. Chỉ có một người có thể ở bên trong bốt điện thoại vào một thời điểm và trong khi người đó ở bên trong, bốt sẽ bị khóa, ngăn chặn người khác từ việc vào.
Điện thoại bên trong bốt ở đây tương đương với một đối tượng trong thực tế, bốt là một monitor và khóa là mutex. Một đối tượng Java chỉ có một monitor và mutex đi kèm với nó.
Do đó, các khối đồng bộ hóa được sử dụng để ngăn chặn các tình trạng đua trong các ứng dụng Java. Khối đồng bộ hóa chứa mã được đánh dấu bằng từ khóa synchronized. Một khóa được gán cho đối tượng được đánh dấu bằng từ khóa synchronized. Khi một luồng gặp từ khóa synchronized
, nó khóa tất cả các cửa của đối tượng đó, ngăn chặn các luồng khác từ việc truy cập nó. Một khóa chỉ cho phép một luồng vào mã một lúc. Khi một luồng bắt đầu thực hiện một khối đồng bộ hóa, nó lấy khóa trên nó. Bất kỳ luồng nào khác sẽ không thể thực hiện mã cho đến khi luồng đầu tiên kết thúc và giải phóng khóa. Khóa dựa trên đối tượng và không phải trên phương thức.
Ví dụ:
Tạo lớp Account:
class Account {
private double balance = 0.0;
public void deposit(double amount) {
balance = balance + amount;
}
public void displayBalance() {
System.out.println("Balance is: " + balance);
}
}
Tạo lớp Transaction:
class Transaction implements Runnable {
private double amount;
private Account account;
private Thread t;
public Transaction(Account acc, double amt) {
account = acc;
amount = amt;
t = new Thread(this);
t.start();
}
// Synchronized block calls deposit method
public void run() {
synchronized (account) {
account.deposit(amount);
account.displayBalance();
}
}
}
Tạo lớp DepositAmount:
public class DepositAmount {
public static void main(String[] args) {
Account accObj = new Account();
Transaction t1 = new Transaction(accObj, 500.00);
Transaction t2 = new Transaction(accObj, 200.00);
}
}
Mã nguồn tạo một lớp Account
với các phương thức là deposit()
và displayBalance()
, những phương thức này sẽ thêm số tiền vào số dư hiện tại và hiển thị nó. Lớp Transaction
tạo một đối tượng Thread
và gọi phương thức run()
trên nó. Phương thức run()
chứa một khối đồng bộ hóa. Khối đồng bộ hóa này lấy đối tượng tài khoản, đó sẽ đóng vai trò như một đối tượng monitor. Điều này chỉ cho phép một luồng được thực thi bên trong khối đồng bộ hóa trên cùng một đối tượng monitor.
Phương thức Đồng bộ hóa
Phương thức đồng bộ hóa thu được một khóa trên đối tượng lớp. Điều này có nghĩa là tại một thời điểm chỉ có một luồng thu được một khóa trên phương thức, trong khi tất cả các luồng khác phải đợi để gọi phương thức đồng bộ hóa.
Hãy xem xét một tình huống nơi hai luồng cần thực hiện các hoạt động đọc và ghi trên một tệp duy nhất lưu trữ trên hệ thống. Nếu cả hai luồng đều cố gắng thay đổi dữ liệu tệp cho các hoạt động đọc hoặc ghi cùng một lúc, có thể để lại tệp trong trạng thái không nhất quán. Để ngăn điều này xảy ra, có thể định nghĩa một phương thức đồng bộ hóa. Mỗi luồng sẽ chiếm một khóa trên phương thức để thực hiện hoạt động tương ứng. Do đó, cả hai luồng không thể gọi phương thức cùng một lúc, vì nó sẽ bị khóa bởi luồng khác.
Cú pháp để khai báo một phương thức đồng bộ hóa như sau:
Cú pháp:
synchronized method(
// thân phương thức
)
Lưu ý: Constructors không thể được đồng bộ hóa.
package test;
class One {
// This method is synchronized to use the thread safely
synchronized void display(int num) {
System.out.print("" + num);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
System.out.println("Interrupted");
}
System.out.println(" done");
}
}
class Two extends Thread {
int number;
One objOne;
public Two(One one_num, int num) {
objOne = one_num;
number = num;
}
public void run() {
// Invoke the synchronized method
objOne.display(number);
}
}
public class SyncMethod {
public static void main(String[] args) {
One objOne = new One();
int digit = 10;
// Create three thread objects
Two objSynch1 = new Two(objOne, digit++);
Two objSynch2 = new Two(objOne, digit++);
Two objSynch3 = new Two(objOne, digit++);
objSynch1.start();
objSynch2.start();
objSynch3.start();
}
}
Ở đây, lớp One có một phương thức display()
nhận một tham số kiểu int. Số này được hiển thị với một hậu tố “done”. Phương thức Thread.sleep(1000)
tạm dừng luồng hiện tại sau khi phương thức display()
được gọi.
Hàm tạo của lớp Two nhận một tham chiếu đến một đối tượng t thuộc lớp One và một biến nguyên. Ở đây, một luồng mới cũng được tạo. Luồng này gọi phương thức run()
của đối tượng t. Lớp chính SynchDemo
khởi tạo lớp One như một đối tượng objOne
và tạo ba đối tượng thuộc lớp Two. Cùng một đối tượng objOne
được truyền cho mỗi đối tượng Two. Phương thức join()
làm cho luồng gọi đợi cho đến khi luồng được gọi chấm dứt. Không phải lúc nào cũng có thể đạt được đồng bộ hóa bằng cách tạo các phương thức đồng bộ hóa trong các lớp.
Lý do cho điều này như sau:
Hãy xem xét một trường hợp nơi người lập trình muốn đồng bộ hóa quyền truy cập vào các đối tượng của một lớp, mà không sử dụng các phương thức đồng bộ hóa. Đồng thời giả sử mã nguồn không có sẵn do hoặc bởi bên thứ ba tạo ra nó hoặc lớp được nhập từ thư viện tích hợp. Trong trường hợp như vậy, từ khóa synchronized
không thể được thêm vào các phương thức thích hợp bên trong lớp.
Do đó, vấn đề ở đây là làm thế nào để đảm bảo quyền truy cập vào một đối tượng của lớp này được đồng bộ hóa. Điều này có thể được đạt được bằng cách đặt tất cả các cuộc gọi đến các phương thức được định nghĩa bởi lớp này bên trong một khối đồng bộ hóa.
Khóa Nội tại (Intrinsic Lock) và Đồng bộ hóa (Synchronization)
Đồng bộ hóa được xây dựng xung quanh khái niệm của một máy chủ giám sát tích hợp được gọi là khóa nội tại hoặc khóa giám sát. Khóa giám sát này đảm bảo quyền truy cập độc quyền đối với các đối tượng luồng, tạo ra một mối quan hệ giữa hành động của luồng và bất kỳ quyền truy cập nào khác vào cùng một khóa.
Điều này giúp làm cho mối quan hệ giữa các luồng trở nên rõ ràng.
Mọi đối tượng đều kết nối với một khóa nội tại. Thông thường, một luồng sẽ chiếm khóa nội tại của đối tượng trước khi truy cập vào các trường của nó và sau đó, giải phóng khóa nội tại. Trong quá trình này, luồng sở hữu khóa nội tại. Không có luồng nào khác có thể chiếm cùng một khóa. Luồng khác sẽ bị chặn khi nó cố gắng chiếm khóa.
Khi một luồng giải phóng một khóa nội tại, một mối quan hệ “happens-before” được thiết lập giữa hành động đó và bất kỳ việc chiếm lại nào sau đó của cùng một khóa.
Khi một luồng gọi một phương thức được đồng bộ hóa, điều sau đây xảy ra:
- Tự động chiếm khóa nội tại cho đối tượng của phương thức đó.
- Giải phóng nó khi phương thức trả về.
Khóa được giải phóng ngay cả khi việc trả về là do một ngoại lệ không được nắm bắt.
Nếu một phương thức tĩnh được liên kết với một lớp, luồng sẽ nhận được khóa nội tại cho đối tượng Lớp liên kết với lớp đó. Do đó, khóa kiểm soát quyền truy cập vào các trường tĩnh của lớp khác nhau khỏi khóa cho bất kỳ trường hợp nào của lớp đó.
Mã đồng bộ hóa cũng có thể được tạo ra với các câu lệnh đồng bộ hóa. Các câu lệnh này nên chỉ định đối tượng cung cấp khóa nội tại.
Các câu lệnh đồng bộ hóa cũng giúp cải thiện đồng thời.
Cơ chế đợi- thông báo (Wait-Notify)
Java cũng cung cấp một cơ chế đợi và thông báo để hoạt động phối hợp với đồng bộ hóa. Cơ chế đợi-thông báo hoạt động như hệ thống đèn giao thông trong chương trình. Nó cho phép luồng cụ thể chờ đợi một khoảng thời gian cho luồng khác đang chạy và đánh thức nó khi cần thiết. Đối với các hoạt động này, nó sử dụng các phương thức wait(), notify(), và notifyAll().
Nói một cách khác, cơ chế đợi-thông báo là một quy trình được sử dụng để điều khiển các phương thức wait() và notify(). Cơ chế này đảm bảo rằng có sự chuyển giao mượt mà của một tài nguyên cụ thể giữa hai luồng cạnh tranh.
Nó cũng giám sát điều kiện trong một chương trình trong trường hợp một luồng:
Được phép chờ khóa của một khối đồng bộ hóa của tài nguyên hiện đang được sử dụng bởi một luồng khác.
Được thông báo để kết thúc trạng thái chờ đợi và có được khóa của khối đồng bộ hóa tài nguyên đó.
Phương thức wait()
Phương thức wait() khiến một luồng đợi cho một luồng khác phải giải phóng một tài nguyên. Nó buộc luồng đang chạy hiện tại phải giải phóng khóa hoặc giám sát mà nó đang giữ trên một đối tượng. Một khi tài nguyên đã được giải phóng, một luồng khác có thể có được khóa và bắt đầu chạy. Phương thức wait() chỉ có thể được gọi từ bên trong mã đồng bộ.
Cần lưu ý những điểm sau khi sử dụng phương thức wait():
- Luồng gọi phải từ bỏ CPU và khóa.
- Luồng gọi chuyển vào trạng thái chờ đợi của giám sát.
Cú pháp:
public final void wait()
Vấn đề “những nhà triết học ăn” là một ví dụ phổ biến được sử dụng trong thế giới lập trình Java để thể hiện đồng bộ hóa và kiểm soát song song. Theo ví dụ này, có năm nhà triết học và cách sống của họ là nghĩ và ăn xen kẽ nhau. Họ chia sẻ một cái bàn tròn và ngồi trên năm chiếc ghế. Có một bát cơm cho mỗi nhà triết học, nhưng chỉ có năm đôi đũa, không phải mười. Mỗi nhà triết học đều cần cả chiếc đũa phải và đôi chiếc trái để ăn. Một nhà triết học đói có thể chỉ ăn nếu cả đôi đũa đều có sẵn. Nếu không, nhà triết học phải chờ đến lượt của họ cho đến khi cả hai đôi đều sẵn.
Ví dụ:
Tạo lớp ChopStick:
/*
* Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license
* Click nbfs://nbhost/SystemFileSystem/Templates/Classes/Class.java to edit this template
*/
package com;
/**
*
* @author toan1
*/
import java.io.*;
import java.util.*;
class Chopstick {
boolean available;
Chopstick() {
this.available = true;
}
// Pickup the chopsticks
public synchronized void takeUp() {
// As long as someone is already using and the chopstick is not available
while (!available) {
try {
System.out.println("Waiting to eat");
// Enter the waiting queue
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// Received the chopstick so mark it as unavailable for others
available = false;
}
// Put down the chopsticks
public synchronized void putDown() {
// Finished eating then mark it as available so that other people can use
available = true;
notify(); // Notify waiting threads
}
}
Tạo lớp Philosopher
/*
* Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license
* Click nbfs://nbhost/SystemFileSystem/Templates/Classes/Class.java to edit this template
*/
package com;
/**
*
* @author toan1
*/
// Philosophers
class Philosopher extends Thread {
Chopstick left;
Chopstick right;
int ID;
// Parameterized Constructor
public Philosopher(Chopstick left, Chopstick right, int ID) {
this.left = left;
this.right = right;
this.ID = ID;
}
// Eating
public void eat() {
left.takeUp();
right.takeUp();
System.out.println(ID + " : The Philosopher is Dining");
}
// Thinking
public void think() {
left.putDown();
right.putDown();
System.out.println(ID + " : The Philosopher is Thinking");
}
public void run() {
while (true) {
eat();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
think();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
Tạo lớp DiningDemo
/*
* Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license
* Click nbfs://nbhost/SystemFileSystem/Templates/Classes/Class.java to edit this template
*/
package com;
import java.io.IOException;
/**
*
* @author toan1
*/
public class DiningDemo {
public static void main(String[] args) throws IOException {
int i;
// Chopsticks, 5 philosophers
Philosopher[] philosophers = new Philosopher[5];
Chopstick[] chopsticks = new Chopstick[5];
// instantiate
for (i = 0; i < 5; i++) {
chopsticks[i] = new Chopstick();
}
// instantiate
for (i = 0; i < 5; i++) {
philosophers[i] = new Philosopher(chopsticks[i], chopsticks[(i + 1) % 5], i + 1);
}
// Start the process
for (i = 0; i < 5; i++) {
philosophers[i].start();
}
}
}
Phương thức notify()
Phương thức notify() thông báo cho luồng đang chờ giám sát của một đối tượng. Phương thức này chỉ có thể được gọi bên trong một khối đồng bộ. Nếu có nhiều luồng đang chờ một đối tượng cụ thể, một trong số chúng được chọn để có được đối tượng. Lịch trình quyết định điều này dựa trên yêu cầu của chương trình.
Phương thức notify() hoạt động như sau:
- Luồng đang chờ di chuyển ra khỏi không gian chờ đợi của giám sát và vào trạng thái sẵn sàng.
- Luồng được thông báo bây giờ có đủ điều kiện để nhận lại khóa của giám sát trước khi có thể tiếp tục.
Cú pháp:
public final void notify()
Ví dụ:
Tạo lớp Message:
/*
* Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license
* Click nbfs://nbhost/SystemFileSystem/Templates/Classes/Class.java to edit this template
*/
package com;
/**
*
* @author toan1
*/
class Message {
private String message;
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}
Tạo lớp WaitNotifyExample
/*
* Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license
* Click nbfs://nbhost/SystemFileSystem/Templates/Classes/Class.java to edit this template
*/
package com;
/**
*
* @author toan1
*/
public class WaitNotifyExample {
public static void main(String[] args) {
final Message message = new Message();
// Creating and starting the waiting thread
Thread waitingThread = new Thread(() -> {
synchronized (message) {
try {
System.out.println("Waiting for a message...");
message.wait(); // The thread goes into a waiting state
System.out.println("Received: " + message.getMessage());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
// Creating and starting the notifying thread
Thread notifyingThread = new Thread(() -> {
synchronized (message) {
System.out.println("Preparing to send a message...");
message.setMessage("Hello, Wait/Notify!");
message.notify(); // Wakes up the waiting thread
System.out.println("Message sent.");
}
});
// Starting the threads
waitingThread.start();
notifyingThread.start();
}
}
Output:
Waiting for a message...
Preparing to send a message...
Message sent.
Received: Hello, Wait/Notify!
Thread.onSpinWait()
Tất cả các luồng tuân theo thứ tự thực thi. Luồng có độ ưu tiên thấp được thực thi trước, sau đó là các luồng có độ ưu tiên cao. Một luồng được tạo là một luồng daemon nếu và chỉ nếu luồng đó là luồng daemon và có độ ưu tiên thấp.
Thread.onSpinWait()
là một phương thức được giới thiệu lần đầu trong Java 9. Nó thông báo cho CPU rằng luồng hiện tại đang tiêu thụ nhiều chu kỳ CPU hơn và hiện tại không thể tiến triển. Sau đó, CPU cấp thêm tài nguyên cho các luồng khác tại chi phí của việc tải lên lịch trình hệ điều hành và rút luồng hiện tại khỏi phương thức.
Sử dụng onSpinWait()
:
Phù hợp khi luồng phải chờ đợi trong một khoảng thời gian dài cho một sự kiện bên ngoài xảy ra.
Cho phép các sự kiện đã xảy ra kết thúc nhanh chóng, do đó tránh thời gian chờ đợi lâu.
Trong trường hợp của mẫu phương thức wait () và notify (), luồng khác đợi cho đến khi luồng đang đợi hoặc ngủ tỉnh để thức dậy. Đôi khi, luồng khác không biết rằng tất cả các luồng đang đợi khác vẫn chưa được thông báo, nếu người phát triển không kiểm soát được luồng khác. Không có cách nào để được thông báo. Trong trường hợp như vậy, onSpinWait()
được sử dụng.
Ví dụ:
public class SpinWaitExample {
public static void main(String[] args) {
// Creating and starting two threads
Thread thread1 = new MyThread();
Thread thread2 = new MyThread();
thread1.start();
thread2.start();
}
}
class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("Thread " + getId() + " is running: " + i);
// Perform some work
for (int j = 0; j < 100000; j++) {
// Using onSpinWait() to indicate that the thread is actively waiting
Thread.onSpinWait();
}
}
}
}
Deadlocks
Khái niệm deadlock mô tả một tình huống khi hai hoặc nhiều luồng bị chặn mãi mãi, đang chờ đợi nhau để giải phóng một tài nguyên. Đôi khi xảy ra rằng hai luồng bị chặn vào các tài nguyên tương ứng của mình, đang chờ đợi các khóa tương ứng để trao đổi tài nguyên giữa chúng. Trong tình huống đó, trạng thái chờ đợi tiếp tục mãi mãi vì cả hai đều trong tình trạng hoang mang về việc cái nào sẽ rời khỏi khóa và cái nào sẽ vào khóa. Đây là tình huống deadlock trong một chương trình Java dựa trên luồng. Tình huống deadlock đưa đến việc thực thi chương trình tới ngừng lại.
Hình dưới mô tả một điều kiện deadlock.
Ghi chú: Luồng A có thể khóa cấu trúc dữ liệu X, và luồng B có thể khóa cấu trúc dữ liệu Y. Sau đó, nếu A cố gắng khóa Y và B cố gắng khóa X, cả A và B sẽ chờ mãi mãi – B sẽ chờ A mở khóa X và A sẽ chờ B mở khóa Y.
Việc gỡ lỗi sự cố deadlock là khá khó khăn vì nó xảy ra rất hiếm. Ví dụ dưới mô tả DeadLock:
// Demonstrating Deadlock.
// DeadlockDemo implements the Runnable interface.
public class DeadlockDemo implements Runnable {
private DeadlockDemo grabber;
public static void main(String[] args) {
DeadlockDemo objDead1 = new DeadlockDemo();
DeadlockDemo objDead2 = new DeadlockDemo();
Thread objTh1 = new Thread(objDead1);
Thread objTh2 = new Thread(objDead2);
objDead1.grabber = objDead2;
objDead2.grabber = objDead1;
objTh1.start();
objTh2.start();
System.out.println("started");
try {
objTh1.join();
objTh2.join();
} catch (InterruptedException e) {
System.out.println("error occurred");
}
System.exit(0);
}
public synchronized void run() {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
System.out.println("error occurred");
}
grabber.syncIt();
}
public synchronized void syncIt() {
try {
Thread.sleep(500);
System.out.println("Sync");
} catch (InterruptedException e) {
System.out.println("error occurred");
}
System.out.println("In the syncIt() method");
}
}
Công Cụ Đồng Thời (Concurrency Utilities)
Nền tảng Java đã thêm vào một thư viện đồng thời phong phú cho các ứng dụng lớn thực thi trên môi trường nhiều bộ xử lý. Thư viện đồng thời là một bổ sung mới trong gói java.util. Nó cũng cung cấp các cấu trúc dữ liệu đồng thời mới trong Collections Framework.
java.util.concurrent Collections
Dưới đây là một số bộ sưu tập được phân loại theo các giao diện Collection:
Chú ý: Tất cả các bộ sưu tập này giúp tránh lỗi nhất quán bộ nhớ bằng cách xác định một mối quan hệ “happens-before” giữa một hoạt động thêm một đối tượng vào bộ sưu tập với các hoạt động liên tiếp mà truy cập hoặc loại bỏ đối tượng đó.
BlockingQueue: Định nghĩa một cấu trúc dữ liệu FIFO (First In, First Out) mà chặn hoặc chờ đợi khi dữ liệu được thêm vào một hàng đợi đầy hoặc được lấy từ một hàng đợi trống.
ConcurrentMap: Là một siêu giao diện của java.util.Map, định nghĩa các hoạt động nguyên tử hữu ích. Những hoạt động này chỉ thêm một cặp khóa-giá trị nếu khóa không tồn tại. Chúng cũng có thể loại bỏ hoặc thay thế một cặp khóa-giá trị chỉ khi khóa đó tồn tại. Các hoạt động như vậy có thể được làm nguyên tử để tránh đồng bộ hóa.
Chú ý: ConcurrentHashMap là hiện thực tiêu biểu chung cho ConcurzentMap, ConcurrentHashévap là một biến thể đồng thời của HashMap.
ConcurrentNavigableMap: Là một siêu giao diện của ConcurrentMap hỗ trợ các tìm kiếm gần đúng.
Chú ý: ConcurrentSkipListMap là hiện thực tiêu biểu chung cho ConcurrentWavigableMap. ConcurrentSkipListMap là một biến thể đồng thời của TreeMap.
Biến Nguyên Tử (Atomic Variables)
Gói java.util.concurrent.atomic định nghĩa các lớp hỗ trợ các hoạt động nguyên tử trên biến đơn. Tất cả các lớp bao gồm các phương thức get và set hoạt động tương tự như đọc và ghi trên các biến volatile. Do đó, một bộ set có một mối quan hệ “happens-before” với bất kỳ lệnh get tiếp theo trên cùng một biến.
Chú ý: Phương thức so sánh và đặt (compareAndSet) trong hệ thống nguyên tử bao gồm các tính năng nhất quán bộ nhớ tương tự như các phương thức toán tử nguyên tử đơn giản được áp dụng cho biến nguyên tử kiểu số nguyên.
Ví dụ:
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicVariableApplication {
private final AtomicInteger value = new AtomicInteger(0);
public int getValue() {
return value.get();
}
public int getNextValue() {
return value.incrementAndGet();
}
public int getPreviousValue() {
return value.decrementAndGet();
}
public static void main(String[] args) {
AtomicVariableApplication obj = new AtomicVariableApplication();
System.out.println(obj.getValue());
System.out.println(obj.getNextValue());
System.out.println(obj.getPreviousValue());
}
}
Executors và Giao Diện Executor
Các đối tượng chia quản lý luồng và tạo chúng ra khỏi phần còn lại của ứng dụng được gọi là executors.
Gói java.util.concurrent định nghĩa ba giao diện executor sau đây:
Executor: Giúp khởi chạy các nhiệm vụ mới. Giao diện Executor bao gồm một phương thức duy nhất là
execute
. Nó được thiết kế để làm nhanh chóng thay thế cho một cách tạo luồng phổ biến. Nếux
là một đối tượng Runnable vàe
là một đối tượng Executor,(new Thread(r)).start();
có thể được thay thế bằnge.execute(x);
.
Các hiện thực executor trong java.util.concurrent
sử dụng các giao diện ExecutorService
và ScheduledExecutorService
tiên tiến.
Cách làm cấp thấp tạo một luồng mới và chạy nó ngay lập tức. Tùy thuộc vào cách triển khai của Executor, execute()
có thể sử dụng một luồng công nhân hiện tại để chạy nó. Nó cũng có thể đặt vào một hàng đợi để chờ một luồng công nhân trở nên khả dụng.
ExecutorService: Là một siêu giao diện của Executor và giúp quản lý sự phát triển của các nhiệm vụ của executor và các nhiệm vụ cá nhân. Giao diện cung cấp một phương thức
execute()
với một phương thứcsubmit()
có tài nguyên, chấp nhận đối tượng Runnable và Callable. Các đối tượng Callable cho phép nhiệm vụ trả về một giá trị. Phương thứcsubmit()
trả về một đối tượng Future, giúp lấy giá trị trả về từ Callable. Đối tượng cũng quản lý trạng thái của các nhiệm vụ Callable và Runnable. ExecutorService cung cấp các phương thức để gửi một bộ sưu tập lớn các đối tượng Callable. Nó cũng bao gồm các phương thức khác nhau để quản lý việc tắt nguồn của executor.
Chú ý: Các nhiệm vụ nên quản lý các sự gián đoạn tốt để hỗ trợ việc tắt nguồn ngay lập tức.
ScheduledExecutorService: Là một siêu giao diện của
Executorservice
và giúp thực hiện định kỳ các nhiệm vụ.
Chú ý: Các biến tham chiếu đến đối tượng executor được khai báo dưới dạng một trong các loại giao diện executor.
Giao diện cung cấp một lịch trình cho các phương thức của giao diện cha ExecutorService
. Lịch trình thực hiện 2 nhiệm vụ Runnable hoặc Callable sau một độ trễ cụ thể. Giao diện cũng định nghĩa scheduleAtFixedRate() và scheduleWithFixedDelay(). Tại các khoảng thời gian được xác định, chúng thực hiện các nhiệm vụ đã chỉ định lặp đi lặp lại.
ThreadPools
Thread pools có các luồng công nhân giúp tạo luồng và do đó, giảm thiểu overhead. Một số triển khai executor trong java.util.concurrent sử dụng thread pools. Thread pools thường được sử dụng để thực hiện nhiều nhiệm vụ.
Việc phân bổ và giải phóng nhiều đối tượng luồng tạo ra một lượng quản lý bộ nhớ đáng kể trong ứng dụng quy mô lớn.
Fixed thread pool là một loại thread pool phổ biến bao gồm các tính năng sau:
- Có một số luồng đang chạy được xác định.
- Khi một luồng đang sử dụng bị chấm dứt, nó sẽ tự động được thay thế bằng một luồng mới
- Ứng dụng sử dụng fixed thread pool để phục vụ các yêu cầu HTTP càng nhanh càng tốt theo khả năng của hệ thống.
Gọi phương thức factory mới newFixedThreadPool trong java.util.concurrent.Executors tạo một executor sử dụng fixed thread pool. Lớp này cung cấp các phương thức factory sau:
Phương thức newCachedThreadPool: Phương thức factory tĩnh này tạo một executor với một thread pool có thể mở rộng. Điều này phù hợp cho các ứng dụng khởi chạy nhiều nhiệm vụ ngắn hạn.
Phương thức newSingleThreadExecutor: Phương thức factory tĩnh này tạo một executor thực hiện một nhiệm vụ mỗi lần.
Chú ý: Các phương thức factory khác bao gồm các phiên bản ScheduledExecutorService khác nhau của các executors được tạo bởi các phương thức newCachedThreadPool và newSingleThreadExecutor.
Fork/Join Framework
Đây là một triển khai của giao diện Executorservice. Framework này giúp làm việc với nhiều bộ xử lý để tăng hiệu suất của ứng dụng. Nó sử dụng một thuật toán work-stealing và được sử dụng khi công việc được chia thành các phần nhỏ một cách đệ quy. Fork/Join framework phân công các nhiệm vụ cho các luồng công nhân trong một thread pool.
Trong fork/join framework, có lớp ForkJoinPool. Lớp này là một sự mở rộng của lớp AbstractExecutorService. Lớp ForkJoinPool triển khai thuật toán chính work-stealing và thực thi các tiến trình ForkJoinTask.
Dưới đây là các bước cơ bản để sử dụng fork/join framework:
- Viết mã thực hiện một phần của công việc. Mã nên giống như mã giả sau:
if (phần công việc của tôi đủ nhỏ)
thực hiện công việc trực tiếp
else
chia phần công việc của tôi thành hai phần
gọi hai phần và đợi kết quả
- Bọc mã trong một lớp ForkJointask con, thường là sử dụng Recursivetask trả về một kết quả hoặc RecursiveAction.
- Tạo đối tượng cho tất cả các nhiệm vụ cần thực hiện.
- Chuyển đối tượng đó vào phương thức invoke() của một thể hiện đối tượng ForkJoinPool.
Ví dụ:
/*
* Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license
* Click nbfs://nbhost/SystemFileSystem/Templates/Classes/Class.java to edit this template
*/
package com;
/**
*
* @author toan1
*/
import java.util.Random;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveTask;
public class ForkJoinApplication extends RecursiveTask<Integer> {
private static final int SEQUENTIAL_THRESHOLD = 5;
private final int[] data;
private final int startData;
private final int endData;
public ForkJoinApplication(int[] data, int startValue, int endValue) {
this.data = data;
this.startData = startValue;
this.endData = endValue;
}
public ForkJoinApplication(int[] data) {
this(data, 0, data.length);
}
@Override
protected Integer compute() {
final int length = endData - startData;
if (length < SEQUENTIAL_THRESHOLD) {
return computeDirectly();
}
final int midValue = length / 2;
final ForkJoinApplication leftValues = new ForkJoinApplication(data, startData, startData + midValue);
leftValues.fork();
final ForkJoinApplication rightValues = new ForkJoinApplication(data, startData + midValue, endData);
return Math.max(rightValues.compute(), leftValues.join());
}
private Integer computeDirectly() {
System.out.println(Thread.currentThread() + " computing: " + startData + " to " + endData);
int max = Integer.MIN_VALUE;
for (int i = startData; i < endData; i++) {
if (data[i] > max) {
max = data[i];
}
}
return max;
}
public static void main(String[] args) {
// Create a random object value set
final int[] values = new int[20];
final Random randObj = new Random();
for (int i = 0; i < values.length; i++) {
values[i] = randObj.nextInt(100);
}
// Submit the task to the pool
final ForkJoinPool pool = new ForkJoinPool(4);
final ForkJoinApplication maxFindObj = new ForkJoinApplication(values);
// Invoke the compute method
System.out.println("Maximum value after computing is " + pool.invoke(maxFindObj));
}
}
Output:
Thread[ForkJoinPool.commonPool-worker-3,5,main] computing: 0 to 5
Thread[ForkJoinPool.commonPool-worker-2,5,main] computing: 5 to 10
Thread[ForkJoinPool.commonPool-worker-3,5,main] computing: 10 to 15
Thread[ForkJoinPool.commonPool-worker-2,5,main] computing: 15 to 20
Thread[ForkJoinPool.commonPool-worker-3,5,main] computing: 0 to 10
Thread[ForkJoinPool.commonPool-worker-2,5,main] computing: 10 to 20
Thread[ForkJoinPool.commonPool-worker-2,5,main] computing: 5 to 15
Thread[ForkJoinPool.commonPool-worker-3,5,main] computing: 0 to 20
Maximum value after computing is 98
StackWalker API
Trước khi tìm hiểu về stack walking và duyệt qua các khung stack, điều quan trọng là hiểu rõ về khái niệm stack frame. Mỗi luồng JVM có một ngăn xếp JVM riêng được tạo ra khi một luồng được khởi tạo. Như bạn có thể biết, một stack là một cấu trúc dữ liệu tuyến tính trong bộ nhớ dựa trên nguyên tắc Last In First Out (LIFO). Khi một phương thức được gọi, một khung stack mới được tạo ra và đẩy lên đỉnh của ngăn xếp. Khi cuộc gọi phương thức hoàn thành, khung stack bị hủy bỏ (tức là ‘pop’ khỏi ngăn xếp). Một ngăn xếp cho một luồng cụ thể có thể được gọi là ngăn xếp runtime. Mọi cuộc gọi phương thức thực hiện bởi luồng đó được lưu trữ trong ngăn xếp runtime tương ứng. Mỗi khung stack trên ngăn xếp có mảng biến địa phương riêng, ngăn xếp toán hạng và các dữ liệu khác. Trong một luồng đã cho, tại bất kỳ thời điểm nào chỉ có một khung stack là hoạt động. Khung stack hoạt động được gọi là khung stack hiện tại, phương thức nào được gọi là phương thức hiện tại. Phương thức được định nghĩa trong lớp hiện tại được gọi là lớp hiện tại.
Một stack trace là một biểu diễn của một ngăn xếp cuộc gọi tại một thời điểm cụ thể, với mỗi phần tử đại diện cho một cuộc gọi phương thức. Điều này cực kỳ hữu ích trong xử lý ngoại lệ để xác định nguyên nhân gốc của sự cố. Trong các phiên bản trước, trước Java 9, việc trích xuất thông tin stack trace là khó khăn và tẻ nhạt vì quá trình thường xuyên chụp một bản chụp toàn bộ ngăn xếp, điều này không cần thiết.
Dưới đây là nhược điểm của việc sử dụng getStackTrace()
để có thông tin stack trace:
- Xảy ra vấn đề về hiệu suất khi truy cập khung stack của luồng hiện tại khỏi cuộc gọi đệ quy ở một độ sâu ngăn xếp cụ thể. Có các tình huống mà các nhà phát triển chỉ quan tâm đến một số ít phần tử của ngăn xếp. Trong những trường hợp như vậy, các phương thức này là phiện toại.
- Nhà phát triển có thể truy cập các khung không cần thiết không cần thiết cho quá trình phát triển.
- Đôi khi thông tin bị thiếu do trả về các khung ngăn xếp không đầy đủ trong triển khai VM.
- Việc truy cập thông tin Class khó khăn với thể hiện của lớp.
Java 9 giới thiệu StackWalker API cung cấp khả năng duyệt qua stack mà không cần chụp toàn bộ stack trace. Do đó, nó cải thiện việc lọc dễ dàng cũng như lọc frame của ngăn xếp. Với API này, thông tin về lớp có thể được truy cập hoàn toàn vì stack trace chứa các tham chiếu thực sự đến các thể hiện của class<?>
. Nó hỗ trợ đa luồng vì nhiều luồng được chia sẻ giữa một đối tượng StackWalker duy nhất. Lớp StackWalker
là thành phần chính của StackWalker API. Nó trả về thông tin của các đối tượng khung stack mà StackWalker xác định. Các khung stack cung cấp tên lớp và tên phương thức, nhưng không có tham chiếu lớp.
Các đặc điểm và lợi ích của API này như sau:
- Nó cung cấp cho nhà phát triển một cơ chế để lọc/bỏ qua các lớp, điều này mang lại linh hoạt để xử lý các lớp cụ thể.
- Nó cung cấp một cách để chỉ tải một số khung (ví dụ: chỉ có thể tải năm khung). Điều này có thể cải thiện hiệu suất.
- Có thể trực tiếp có được một thể hiện của lớp khai báo mà không cần sử dụng phương thức Java.lang.SecurityManager.getClassContext().
- Nó mang lại cho nhà phát triển một cách để hiểu rõ hành vi của ứng dụng một cách dễ dàng hơn.
- Nó an toàn đối với luồng và cho phép nhiều luồng chia sẻ một thể hiện StackWalker duy nhất để truy cập các ngăn xếp tương ứng của họ.
- Thời gian truy cập là giống nhau cho việc truy cập đỉnh ngăn xếp hiện tại với sâu ngăn xếp khác nhau (1, 20, 100, 1000).
- Nó lọc một số lớp, do đó giảm bớt các khung stack bằng cách truyền frame cho phương thức
filter()
vàlimit()
. getInstance()
được gọi để truy cập một số khung cụ thể trực tiếp, đảm bảo cách tiếp cận linh hoạt hơn cho các khung giới hạn.- Nó hiển thị tất cả các khung ẩn và khung phản ánh bằng cách cấu hình với các tùy chọn
SHOW_HIDDEN_FRAME
vàSHOW_REFLECT_FRAME
. - Nó giữ thể hiện của lớp gọi bằng cách sử dụng
getCallerClass()
vàgetDeclaringClass()
.
Lớp StackWalker
có một số phương thức mà bạn có thể sử dụng để chụp thông tin từ stack. Điều này bao gồm:
forEach
: Thực hiện hành động đã cho trên mỗi phần tử của luồng StackFrame của luồng hiện tại. Có thể duyệt từ đỉnh xuống dưới của ngăn xếp cuộc gọi của luồng hiện tại bằng cách áp dụng phương thức trên mỗi frame.
public void forEach(Consumer<? super StackWalker.StackFrame> action)
getInstance()
: Trả về một thể hiện StackWalker. Có ba phiên bản cho phương thức tĩnh này.walk()
: Áp dụng hàm đã cho vào luồng StackFrames cho luồng hiện tại. Phương thứcwalk()
của lớpStackWalker
dễ dàng duyệt qua ngăn xếp để cải thiện việc lọc khung. Các yếu tố ngăn xếp được sắp xếp từ nơi ngăn xếp được tạo đến dưới cùng của luồng. Luồng đóng khi phương thứcwalk()
trả về. Nó tạo và trả về luồng tuần tự của các khung stack bằng cách áp dụng một hàm vào luồng. Khi phương thức trả về, luồng sẽ đóng cửa.
public <T> T walk(Function<? super Stream<StackWalker.StackFrame>, ? extends T> function)
stackWalker.getCallerClass()
: Trả về đối tượng của lớp của người gọi. Phương thức này lọc các khung phản ánh, xử lý phương thức và khung ẩn.
public Class<?> getCallerClass()
StackWalker.StackFrame
là một giao diện lồng tĩnh của StackWalker
. Một đối tượng của giao diện này đại diện cho cuộc gọi phương thức được trả về bởi StackWalker
. Nó có các phương thức để truy cập thông tin của khung stack như getDeclaringClass()
, getLineNumber()
, và nhiều phương thức khác.
StackWalker.Option
là một định nghĩa tĩnh nằm trong StackWalker
và cấu hình thông tin khung stack được lấy khi một đối tượng StackWalker được tạo thông qua StackWalker.getInstance()
. Nó có các phương thức valueOf(String name)
và values()
.
Cuộc gọi cơ bản của StackWalker có thể như sau:
StackWalker stackWalker = StackWalker.getInstance();
Ví dụ:
import java.lang.StackWalker.StackFrame;
import java.util.*;
import java.util.stream.*;
public class StackWalkingDemo {
public static void main(String[] args) {
new StackWalkingDemo().walk();
}
private void walk() {
new Walker1().walk();
}
private class Walker1 {
public void walk() {
new Walker2().walk();
}
}
private class Walker2 {
public void walk() {
FirstMethod();
}
void FirstMethod() {
SecondMethod();
}
void SecondMethod() {
StackWalker stackWalker = StackWalker.getInstance(Set.of(StackWalker.Option.RETAIN_CLASS_REFERENCE, StackWalker.Option.SHOW_HIDDEN_FRAMES), 16);
Stream<StackFrame> stackStream = stackWalker.walk(
frames -> frames
.map(frame -> "\n" + frame.getClassName() + "/" + frame.getMethodName())
);
List<String> stacks = walkAllStackFrames();
System.out.println("Number of StackFrames: " + stacks.size());
System.out.println("*walk through all StackFrames*");
System.out.println(stacks);
System.out.println("*Skip some StackFrames*");
List<String> stacksAfterSkip = walkSomeStackFrames(2);
System.out.println("Number of StackFrames remaining: " + stacksAfterSkip.size());
System.out.println(stacksAfterSkip);
}
private List<String> walkAllStackFrames() {
return StackWalker.getInstance().walk(
frames -> frames.map(frame -> "\n" + frame.getClassName() + "/" + frame.getMethodName())
.collect(Collectors.toList())
);
}
private List<String> walkSomeStackFrames(int numberOfFrames) {
return StackWalker.getInstance().walk(
frames -> frames.map(frame -> "\n" + frame.getClassName() + "/" + frame.getMethodName())
.skip(numberOfFrames)
.collect(Collectors.toList())
);
}
}
}
Output:
Number of StackFrames: 16
*walk through all StackFrames*
[
StackWalkingDemo/SecondMethod,
StackWalkingDemo/FirstMethod,
StackWalkingDemo/Walker2.walk,
StackWalkingDemo/Walker2.walk,
StackWalkingDemo/Walker1.walk,
StackWalkingDemo/walk,
StackWalkingDemo/StackWalkingDemo.main,
java.base/java.lang.reflect.Method.invoke,
java.base/java.lang.reflect.Method.invoke,
java.base/java.lang.reflect.Method.invoke,
java.base/java.lang.reflect.Method.invoke,
java.base/java.lang.reflect.Method.invoke,
java.base/java.lang.reflect.Method.invoke,
java.base/java.lang.reflect.Method.invoke,
java.base/java.lang.reflect.Method.invoke
]
*Skip some StackFrames*
Number of StackFrames remaining: 14
[
StackWalkingDemo/StackWalkingDemo.main,
java.base/java.lang.reflect.Method.invoke,
java.base/java.lang.reflect.Method.invoke,
java.base/java.lang.reflect.Method.invoke,
java.base/java.lang.reflect.Method.invoke,
java.base/java.lang.reflect.Method.invoke,
java.base/java.lang.reflect.Method.invoke,
java.base/java.lang.reflect.Method.invoke,
java.base/java.lang.reflect.Method.invoke,
java.base/java.lang.reflect.Method.invoke,
java.base/java.lang.reflect.Method.invoke,
java.base/java.lang.reflect.Method.invoke,
java.base/java.lang.reflect.Method.invoke
]
Bài tập
Question 1:
Create a class named Member includes of the following properties:
private String memberID;
private String memberName;
private String address;
- Create 2 construct constructors
- Construct method get/set for all properties of this class.
- Override method toString() to return a string consist information of class's properties.
- This class must implements Serializable interface.
Create a class ClubManager to do the following task:
- The memberID type is: ABBCCCCC where A is only one of characters in set {T,V,A} is only one of characters in set {MB,MT,MN}, C is number.
Example: TMB12345,VMT12312 is valid
- Declare a list of members, using generic ArrayList and input information for all members of this list. The information are inputted from keyboard using java.io package. (you must not use Scanner class)
- When input information from keyboard you must use regular expression to check the memberID is valid or not. If the memberID is not valid, a message will be displayed to require input again.
- Serialize (write) all members to file "member_of_club.txt"
- Read the information from file "member_of_club.txt", store it in other list and display into the screen
Question 2:
Create 2 threads to do the following task:
- The first Thread: After a second, returns a random day of the week and display it into the screen by Vietnamese language.
Example:
"Thu hai", "Thu ba", "Thu tu", "Thu nam", "Thu sau", "Thu bay", "Chu nhat"
- The second Thread: Gets the random day that the first thread returned and display corresponding day by English language.
Example:
"Monday", "Tuesday", "Wednesday", "Friday", "Saturday", "Sunday"
- Write main() method to Demo these threads.
- You must synchronize two threads above.
Bài 2
Question 1:
Context: Book and Library Management
Class Book:
- The class
Book
should have the following private properties:private String bookID;
private String title;
private String author;
- Requirements:
-
Implement two constructors:
- One default constructor.
- One constructor with all three parameters (
bookID
,title
,author
).
-
Implement getter and setter methods for each property.
-
Override the
toString()
method to return a string containing the book’s ID, title, and author. -
The class must implement the
Serializable
interface.
-
Class LibraryManager:
-
The
bookID
should follow this format:ABCXXXX
where:- A is a letter from the set
{F,N,S}
representing Fiction, Non-fiction, or Science. - B is a letter from
{M, H}
representing Morning shift or Afternoon shift of the librarian. - C is a letter from
{P, D}
representing Paperback or Digital format. - XXXX is a 4-digit number.
- A is a letter from the set
-
Requirements:
- Declare a list of books using an
ArrayList<Book>
. - Input information for each book (bookID, title, author) from the keyboard. Ensure the
bookID
is validated by regular expression. - Serialize the list of books to a file called
"library_books.ser"
. - Read the serialized information from the file, store it in another list, and display it.
- Declare a list of books using an
New Constraint:
- If
bookID
is invalid, display an error message and request valid input again. - Add a method that counts the number of books written by a particular author.
Question 2:
Threading Scenario: Bank Transactions
- Create two threads:
-
First thread: This thread simulates depositing money into a shared bank account. Every 3 seconds, it deposits a random amount between 100 and 1000 VND. After each deposit, it displays the total balance.
-
Second thread: This thread simulates withdrawing money from the shared account. Every 4 seconds, it tries to withdraw a random amount between 100 and 500 VND. After each withdrawal, it checks if the balance is enough and displays the remaining balance or an error message if the withdrawal exceeds the balance.
-
Synchronization Requirements:
- Ensure that both threads properly access and modify the shared balance to avoid race conditions.
Example output:
- Thread 1 deposits 500 VND, total balance is 500 VND.
- Thread 2 tries to withdraw 300 VND, total balance is 200 VND.
Bài 3:
**Câu 1:**
Viết chương trình gồm 2 luồng:
- Luồng thứ nhất in các số lẻ từ 1 đến n.
- Luồng thứ hai in các số chẵn từ 0 đến n.
n là số nguyên dương được nhập từ bàn phím.
Các số trên màn hình console phải xuất hiện theo thứ tự tự nhiên.
**Ví dụ:**
Cho n = 8:
```
Luồng chẵn: 0
Luồng lẻ: 1
Luồng chẵn: 2
Luồng lẻ: 3
Luồng chẵn: 4
Luồng lẻ: 5
Luồng chẵn: 6
Luồng lẻ: 7
Luồng chẵn: 8
Kết thúc
```
**Câu 2:**
Tạo lớp `Student` để mô tả thông tin cá nhân với các thuộc tính:
- `private String name;`
- `private int age;`
- `private String phone;`
Lớp phải có 2 constructor (1: mặc định, 2: với tham số), các phương thức getter và setter. Lớp này phải có khả năng tuần tự hóa.
Cho file `input.txt` chứa thông tin các cá nhân theo định dạng:
```
Tên
Tuổi
Số điện thoại
----------------
```
Viết chương trình để đọc `input.txt` và tạo danh sách các đối tượng `Student` (sử dụng generic). Dữ liệu từ file cần được xác thực: tên là chuỗi, tuổi là số nguyên dương, số điện thoại là chuỗi. Hiển thị thông báo lỗi nếu có dữ liệu không hợp lệ, ví dụ "Not number" nếu tuổi không hợp lệ.
Sử dụng `ObjectOutputStream` để ghi từng đối tượng vào file `Persons.obj`.
---