Java: Thread

Các khóa học qua video:
Python SQL Server PHP C# Lập trình C Java HTML5-CSS3-JavaScript
Học trên YouTube <76K/tháng. Đăng ký Hội viên
Viết nhanh hơn - Học tốt hơn
Giải phóng thời gian, khai phóng năng lực

Giới thiệu

Một tiến trình là một chương trình đang được thực thi. Mỗi tiến trình có các tài nguyên run-time của chính bản thân nó, như dữ liệu, các biến và các không gian bộ nhớ. Những tài nguyên thời gian chạy này tạo thành một môi trường thực thi cho các tiến trình trong một chương trình. Vì vậy, mỗi tiến trình đều có một bộ chứa môi trường thực thi để chạy độc lập. Mỗi tiến trình thực thi một số tác vụ tại một thời điểm và mỗi tác vụ được thực hiện bởi một luồng riêng. Bản thân luồng không là gì cả nhưng nó là đơn vị cơ bản để hệ điều hành cấp phát bộ xử lý thời gian. Mỗi luồng là một thực thể của tiến trình và có thể sắp xếp được thứ tự thực hiện. Tiến trình sẽ bắt đầu với một luồng đơn, thường được gọi là nguyên thủy, mặc định hay luồng chính. Như vậy, tiến trình sẽ lần lượt chứa mộ hoặc nhiều luồng.

Mỗi tiến trình có những đặc điểm sau:

- Có một tập hoàn chỉnh các tài nguyên run-time cơ bản để chạy độc lập.

- Là đơn vị nhỏ nhất có khả năng thực thi mã lệnh trong một ứng dụng để giải quyết một công việc hay một tác vụ cụ thể.

- Một số luồng có thể được thực thi tại cùng một thời điểm, nhằm cho phép việc thực hiện các tác vụ của một ứng dụng cùng lúc.

So sánh tiến trình và luồng

Đa luồng cho phép sử dụng chung một vùng nhớ còn tiến trình thì không thể trực tiếp truy cập bộ nhớ của tiến trình khác.

Một số điểm tương tự và khác nhau giữa tiến trình và luồng:

- Tương tự nhau

+ Các luồng chia sẽ một đơn vị xử lý trung tâm (CPU) và chỉ một luồng được thực thi tại một thời điểm.

+ Các luồng nằm bên trong chuỗi thực thi các tiến trình.

+ Một luồng có thể tạo các luồng con hay các phân luồng.

+ Nếu một luồng nào đó bị khóa thì không ảnh hưởng đến các luồng khác.

- Sự khác nhau

+ Không giống như tiến trình, các luồng là không độc lập nhau.

+ Không giống như tiến trình, tất cả các luồng có thể truy cập các địa chỉ trong tác vụ.

+ Không giống như tiến trình, các luồng được thiết kế để hỗ trợ nhau.

Ứng dụng và sử dụng luồng

Trong Java, một luồng có thể xử lý song song nhiều tác vụ.

Một số ứng dụng của luồng là:

- Chơi nhạc và hiển thị ảnh cùng lúc.

- Hiển thị nhiều ảnh trên màn hình.

- Hiển thị các mẫu văn bản cuộn hoặc các ảnh trên màn hình.

Tạo luồng

Một cách dễ dàng để tạo một luồng mới là tạo một lớp dẫn xuất từ lớp java.lang.Thread. Lớp này bao gồm các phương thức và hàm tạo sẽ trợ giúp trong việc phát hành khái niệm lập trình đồng thời trong Java.

Điều này được dùng để tạo các ứng dụng có thể thực thi cùng lúc nhiều tác vụ tại một thời điểm đã cho. Giải pháp này không thực hiện được khi một lpws muống thực thi luồng được dẫn xuất từ lớp khác.

Dưới đây là các bước tạo một luồng mới từ lớp Thread.

Bước 1: Tạo một lớp con

Bằng cách khai báo một lớp con của lớp Thread được định nghĩa trong gói java.lang.

Ví dụ dưới đây minh họa điều này.

class MyThread extends Thread { //Dẫn xuất từ lớp Thread
  //định nghĩa lớp
  . . .
}

Bước 2: Ghi đè phương thức run()

Bên trong lớp con ta cần ghi đè phương thức run() đã được định nghĩa trong lớp Thread. Đoạn mã trong phương thức run() định nghĩa chức năng được yêu cầu cho luồng để thực thi. Phương thức run() trong luồng cũng tương tự như phương thức main() trong ứng dụng.

Ví dụ sau thể hiện cách thực thi phương thức run().

class MyThread extends Thread {
  public void run() //ghi đè phương thức run()
  {
    //thực thi
  }
  . . .
}

Bước 3: Khởi động luồng

Từ phương thức main() tạo một đối tượng của lớp con, sau đó gọi phương thức start() từ đối tượng này để khởi động Thread. Phương thức start() sẽ đặt đối tượng Thread trong trạng thái sẵn sàng chạy, nó sẽ gọi phương thức run() để cấp phát tài nguyên được yêu cầu để chạy luồng.

Ví dụ:

public class TestThread {
  . . .
  public static void main(String args[]) {
    MyThread t=new MyThread(); //tạo đối tượng thread có tên t
    t.start(); //khởi động thread
  }
}

Các hàm tạo và phương thức lớp Thread

Các hàm tạo của lớp Thread được liệt kê trong bảng 7.1. Lớp ThreadGroup đại diện cho một nhóm các thread và thường được sử dụng trong các hàm tạo của lớp Thread.

Hàm tạo Mô tả
Thread() Hàm tạo mặc định
Thread(Runnable objRun) Tạo một đối tượng Thread, trong đó objRun là đối tượng gọi phương thức run()
Thread(Runnable objRun, String threadName) Tạo một đối tượng Thread mới, trong đó objRun sẽ gọi phương thức run() và threadName là tên của thread sẽ được tạo
Thread(String threadName) Tạo đối tượng Thread mới với tên là giá trị của đối số threadName
Thread(ThreadGroup group, Runnable objRun) Tạo một đối tượng Thread, trong đó group là nhóm thread và objRung là đối tượng gọi phương thức run()
Thread(ThreadGroup
group, Runnable objRun, String threadName)
Tạo một đối tượng Thread trong đó obj sẽ chạy đối tượng đó, threadName là tên của Thread đó và group là nhóm thread mà Thread tham chiếu tới
Thread(ThreadGroup group, Runnable objRun, String threadName, long stackSize) Tạo một đối tượng Thread trong đó obj sẽ chạy đối tượng đó, threadName là tên của Thread đó và group là nhóm thread mà Thread tham chiếu tới, còn stackSize là kích thước stack cụ thể của Thread
Thread(ThreadGroup group, String threadName) Tạo một đối tượng Thread có tên threadName và Thread thuộc nhóm thread có tên group

Các hàm tạo phổ biến của lớp Thread

Lớp Thread cung cấp cho ta cũng cung cấp cho ta khá nhiều phương thức để làm việc với các chương trình. Bảng dưới đây liệt kê các phương thức phổ biến của Thread.

Tên phương thức Mô tả
static int activeCount() Trả về số lượng các thread active và thread hiện tại trong chương trình
static Thread currentThread() Trả về một tham chiếu cuar đối tượng thread đang thực thi
ThreadGroup getThreadGroup() Trả về nhóm thread chứa thread hiện thời
static boolean interrupted() Kiểm tra thread hiện thời xem có bị ngắt không
boolean isAlive() Kiểm trả thread hiện thời còn sống không
boolean isInterrupted() Kiểm tra thread hiện thời đã được ngắt hay chưa
void join() Chờ thread hiện thời die
void setName(String name) Thay đổi tên của thread hiện thời thành name

Các phương thức phổ biến của lớp Thread

Ví dụ sau minh họa việc tạo một luồng mới từ lớp Thread và sử dụng một số phương thức của lớp này.

package solutions;

public class NamedThread extends Thread {

  String name;

  @Override
  public void run() {
    //Lưu s lượng lung
    int count = 0;
    while (count <= 3) {
      //Hin th s lượng lung
      System.out.println(Thread.activeCount());
      //Hin th tên ca lung đang chy
      name = Thread.currentThread().getName();
      count++;
      System.out.println(name);
      if (name.equals("Thread1")) {
        System.out.println("Marimba");
      } else {
        System.out.println("Jini");
      }
    }
  }

  public static void main(String args[]) {
    NamedThread objNamedThread = new NamedThread();
    objNamedThread.setName("Thread1");
    //Hin th trng thái ca lung là còn sng hay không
    System.out.println(Thread.currentThread().isAlive());
    System.out.println(objNamedThread.isAlive());
    objNamedThread.start();
    System.out.println(Thread.currentThread().isAlive());
    System.out.println(objNamedThread.isAlive());
  }
}

Giải thích ví dụ:

Trong ví dụ trên, NamedThread được khai báo là một lớp dẫn xuất từ lớp Thread, trong phương thức main() tạo một đối tượng thread có tên là objNamedThread và ta dùng phương thức setName() để đặt tên cho nó là Thread1. Đoạn ma cũng kiểm tra xem thread hiện thời còn sống hay không bằng cách gọi phương thức isAlive() và hiển thị giá trị trả về của phương thức. Ở đây sẽ in ra kết quả là true vì thread chính đã bắt đầu thực thi và hiện tại vẫn còn sống. Đoạn mã trên cũng kiểm tra xem objNamedThread còn sống hay không, và trong trường hợp ví dụ trên thì thì đối tượng này không còn sống vì nó đã không được thực thi. Tiếp theo, phương thức start() được gọi trên đối tượng objNamedThread dẫn đến phương thức run() được kích hoạt, run() sẽ in tổng số lượng các thread đang chạy (lúc này là 2). Sau đó nó kiểm tra tên của thread đang chạy và in ra Marimba nếu thread đang chạy hiện thời có tên là Thread1. Phương thức sẽ tiến hành kiểm tra 3 lần. Output của đoạn mã ví dụ trên sẽ là như sau:

true
false
true
true
3
Thread1
Marimba
2
Thread1
Marimba
3
Thread1
Marimba
3
Thread1
Marimba

Những lưu ý về lập trình đồng thời

- Lập trình đồng thời là một tiến trình chạy cùng một lúc nhiều tác vụ.

- Trong Java thì có thể thực thì ta có thể thực hiện đồng thời một hàm được gọi và các câu lệnh sau lời gọi hàm mà không cần chờ hàm được gọi kết thúc.

- Hàm được gọi sẽ chạy độc lập và đồng thời với lời gọi chương trình, và có thể chia sẻ các biến, dữ liệu, ... với nó.

Giao diện Runnable

Interface Runnable được thiết kế để cung cấp một tập phổ biến các quy tắc cho các đối tượng muốn thực thi một đoạn mã nào đó trong khi một thread đang active. Hay nói một cách khác của việc tạo thread mới là bằng cách thực thi giao diện  interface is designed to provide a common set of rules for objects that wish to execute a code while a thread is active. Another way of creating a new thread is by implementing the Runnable interface. This approach can be used because Java does not allow multiple class inheritance. Therefore, depending upon the need and requirement, either of the approaches can be used to create Java threads.

Các bước để tạo và chạy một Thread mới bằng giao diện Runnable như sau:

- Bước 1: Thực thi giao diện Runnable

Khai báo một lớp thực thi giao diện. Ví dụ:

class MyRunnable implements Runnable {
. . .
}

- Bước 2: Thực thi phương thức run().

class MyRunnable implements Runnable {

  public void run() {// ghi đè phương thức run()
    . . . // code thực thi

  }

}

- Bước 3: Kích hoạt Thread

Kích hoạt bằng cách dùng phương thức start().

class ThreadTest {

  public static void main(String args[]) {

    MyRunnable r=new Runnable();

    Thread thObj=new Thread(r);

    thObj.start(); //kích thoạt thread

  }

}

Ví dụ sau minh họa cách tạo và sử dụng thread bằng cách sử dụng giao diện Runnable.

package solutions;

public class NamedThread implements Runnable {
  /* Biến này lưu tên ca lung */
  String name;

  public void run() {
    int count = 0; //biến lưu s lượng lung
    while (count < 3) {
      name = Thread.currentThread().getName();
      System.out.println(name);
      count++;
    }
  }
}

class Main {
  public static void main(String args[]) {
    NamedThread objNewThread = new NamedThread();
    Thread objThread = new Thread(objNewThread);
    objThread.start();
  }
}

Trong ví dụ trên, lớp NamedThread thực thi interface Runnable, do đó thể hiện của nó có thể được truyền như là đối số tới hàm tạo của lớp Thread. Output của ví dụ trên như sau:

Thread-0
Thread-0
Thread-0

Các trạng thái của luồng

Mỗi luồng đều có một số trạng thái như new, alive, runnable, blocked, waiting  terminated, và luồng có thể ở những trạng thái khác nhau trong chương trình. Mặc định khi tạo mới luồng thì luồng đó ở trạng thái không sống alive.

Khi ở trạng thái không sống thì nó là luồng empty và không được cấp phát tài nguyên hệ thống. Do vậy, trạng thái của luồng được coi là new cho tới khi phương thức start() được goi. Một khi luồng ở trạng thái new thì ta chỉ có thể start hoặc stop nó, cho nên lời gọi tới bất kỳ phương thức nào trước start() đều phát sinh ngoại lệ ThreadStateException.

Trạng thái Runnable

Luồng mới có thể có trạng thái runnable khi nó gọi phương thức start(), khi đó luồng trong trạng thái đang sống.

Các luồng có thể được thiết lập thứ tự ưu tiên do trong một hệ thống đơn nhiệm thì không thể thực thi các luồng runnable cùng lúc mà chỉ có 1 luồng runnable được chạy. Một khi ở trạng thái runnable thì luồng đủ điều kiện để chạy, nhưng nó có thể không chạy được dó nó phụ thuộc vào thứ tự ưu tiên luồng (vì vậy trạng thái này mới có tên là runnable). Khi luồng chạy thì nó sẽ thực thi các câu lệnh trong phương thức run().

http://v1study.com/public/images/article/java-thread-runnable-state.png
Trạng thái Runnable

Ví dụ:

. . .

MyThreadClass myThread = new MyThreadClass();
myThread.start();
. . .

Tạo một luồng và gọi phương thức start() của luồng đó, luồng sẽ ở trạng thái runnable.

Trạng thái khóa (Blocked State)

Đây là một trong các trạng thái của thread:

- Nó còn sống nhưng hiện tại không đủ điều kiện để chạy

- Mặc dù không có khả năng chạy nhưng nó có thể quay trở về trạng thái chạy (runnable) sau khi nhận màn hình hoặc khóa.

Thread ở trạng thái này sẽ chờ hoạt động trên tài nguyên hoặc đối tượng ở lần kế tới đang được xử lý bởi một thread khác. Thread đang chạy sẽ chuyển trạng thái thành khóa khi một trong các phương thức sleep(), wait() hoặc suspend() được gọi.

Trạng thái Waiting

Luồng có sẽ ở trạng thái này khi nó đang chờ luồng khác giải phóng tài nguyên cho nó. Khi hai hoặc nhiều luồng chạy đồng thời và chỉ một luồng chiếm giữ tài nguyên thì các luồng khác sẽ phải cho cho đến khi luồng này giải phóng tài nguyên. Trong trạng thái chờ này thì luồng sẽ sống nhưng không chạy.

Luồng sẽ ở trạng thái waiting khi nó gọi phương thức wait(). Lời gọi tới phương thức notify() hay notifyAll() sẽ chuyển luồng từ trạng thái waiting sang runnable.

Trạng thái Terminated

Sau khi luồng thực thi phương thức run() sx sẽ die và chuyển sang trạng thái ngắt (terminated). Đây là cách dừng (stop) thông thường của một luồng. Khi luồng bị ngắt thì nó không thể quay về trạng thái runnable. Các phương thức chuyển sạng trạng thái terminated của luồng là stop() và destroy().

Lưu ý - Nếu luồng đang ở trạng thái terminated mà nó lại gọi phương thức start() thì nó sẽ ném ra một ngoại lệ run-time.

Các phương thức của lớp Thread

Lớp Thread có chứa một số phương thức có thể được dùng để thao tác với luồng.

Phương thức getName()

Phương thức này lấy tên của luồng hiện thời.

Ví dụ:

public void run() {

for(int i = 0;i < 5;i++) {

Thread t = Thread.currentThread();

System.out.println(“Name = “ + t.getName());
...

The code snippet demonstrates the use of getName() method to obtain the name of the thread object.

Lưu ý - Phương thức setName(String name) của luồng có tác dụng thiết lập lại tên cho luồng. Tên được truyền qua đối số của phương thức.

Phương thức start()

Phương thức này được gọi sẽ chuyển trạng thái của luồng thành alive, và nó sẽ gọi phương thức run() của luồng.

Tại thời điểm gọi phương thức start() thì:

- Bắt đầu thực thi luồng mới

- Chuyển luồng sang trạng thái runnable

Ví dụ:

. . .
NewThread thObj = new NewThread():
thObj.start();
. . .

Lưu ý - Chỉ gọi phương thức start() của luồng một lần, vì nó không thể được khởi động lại sau mỗi lần thực thi.

Phương thức run()

Phương thức này sẽ kích hoạt thread, thread sẽ chính thức hoạt động. Phương thức này có một số đặc điểm sau:

- Có tầm vực là public

- Không cần thiết phải có đối số

- Không trả về giá trị

- Không ném ngoại lệ

Phương thức này sẽ hoạt động mỗi khi phương thức start() được gọi.

Ví dụ:

class myRunnable implements Runnable {
. . .
public void run() {
System.out.println(“Inside the run method.”);
}
. . .
}

Phương thức sleep()

Phương thức này có những đặc điểm sau:

- Nó treo luồng hiện tại không có thực thi

- Tạo bộ xử lý thời gian cho phép các luồng khác nhau của một hoặc nhiều ứng dụng có thể chạy trên hệ thống máy tính

- Nó sẽ dừng thực thi nếu luồng active trong một khoảng thời gian cụ thể theo đơn vị mili giây hoặc nano giây

- Nó kích hoạt ngoại lệ InterruptedException khi nó bị ngắt bởi phương thức interrupt()

Ví dụ:

try {
  myThread.sleep (10000);
}
catch (InterruptedException e)
{}

Trong đoạn mã trên, myThread sẽ chuyển sang trạng thái sleep khoảng thời gian 10 giây.

Phương thức interrupt()

Phương thức này dùng để ngắt luồng. Phương thức này có những đặc điểm sau:

- Luồng đã bị ngắt có thể die, nó sẽ chờ một tác vụ khác hoặc chuyển tới bước tiếp theo tùy thuộc vào yêu cầu của ứng dụng.

- Phương thức này không ngắt hoặc dùng luồng đang chạy, thay vào đó nó sẽ ném ngoại lệ InterruptedException nếu luồng bị khóa để thoát khỏi trạng thái khóa.

- Nếu luồng bị khóa do phương thức wait(), join() hay sleep() thì nó sẽ nhận một ngoại lệ InterruptedException để chấm dứt phương thức đó.

Lưu ý - Luồng có thể tự ngắt. Khi một luồn không tự ngắt được thì phương thức checkAccess() sẽ được gọi, phương thức này sẽ kiểm tra xem luồng hiện tại có quyền thay đổi hay không. Trong trường hợp không có quyền thì trình quản lý bảo mật sẽ ném một ngoại lệ SecurityException thông báo vi phạm bảo mật.

Quản lý luồng

Vì các luồng không thể thực hiện cùng một thời điểm, cho nên ta cần phải sắp xếp thứ tự ưu tiên sao cho luồng nào quan trọng hơn thì được thực hiện trước, và luôn cần đảm bảo rằng không xảy ra hiện tượng xung đột luồng, tức là các luồng cần phải hoạt động tuần tự và trôi chảy.

Tầm quan trọng của mức ưu tiên luồng

Khi tạo các ứng dụng đa luồng thì có thể xảy ra trường hợp luồng này đang chạy thì ta muốn chạy một luồng khác có tầm quan trọng hơn, muốn làm được điều này thì ta cần thông qua mức ưu tiên luồng.

Các loại mức ưu tiên luồng

Có 3 loại mức yêu tiên luồng như sau:

- Thread.MAX_PRIORITY

Mức này tương đương với 10, mức cao nhất.

- Thread.NORM_PRIORITY

Mức này tương đương 5, mức mặc định.

- Thread.MIN_PRIORITY

Mức này tương được với 1, mức thấp nhất.

Phương thức setPriority(newPriority)

Phương thức này thiết lập mức ưu tiên cho luồng. Đối số newPriority của nó có thể nhận một số nguyên từ 1 đến 10.

Ví dụ sau thể hiện cách sử dụng phương thức này:

. . .
Thread threadA = new Thread("Meeting deadlines");
threadA.setPriority(8);
. . .

Phương thức getPriority()

Phương thức này dùng để truy xuất đến giá trị ưu tiên hiện tại của bất kỳ thread nào.

Cú pháp:

public final int getPriority()

Luồng Daemon

Luồng daemon có đặc điểm là nó chạy liên tục để thực hiện một dịch vụ mà không cần kết nối nào tới toàn bộ tình trạng của chương trình. Những luồng chạy mã lệnh hệ thống là ví dụ về luồng daemon.

Các đặc điểm của luồng daemon:

- Làm việc trong nền tảng cung cấp của các luồng khác.

- Phụ thuộc hoàn toàn vào các luồng của người dùng.

- JVM đã dừng luồng nào thì luồng đó die nhưng luồng daemon sẽ vẫn còn sống.

Các phương thức liên quan:

- setDaemon(boolean value)

Phương thức này dùng để chuyển một luồng người dùng thành luồng daemon, đối số value của nó chứa giá trị true thì có nghĩa nó sẽ thiết lập thành luồng daemon. Mặc định thì tất cả các luồng đều là luồng người dùng.

- isDaemon()

Phương thức này để kiểm tra xem luồng có phải là daemon hay không, trả về true là đúng, false là sai.

Lưu ý: Luồng garbage collector và luồng xử lý sự kiện chuột là những luồng daemon.

Tầm quan trọng của luồng Daemon

Luồng Daemon có thể làm được:

- Các luồng Daemon là những nhà cung cấp dịch vụ cho các luồng khác chạy trong cùng một tiến trình.

- Các luồng Daemon được thiết kế ở dạng các luồng nền mức thấp để thực hiện một số tác vụ như sự kiện chuột.

» Tiếp: Về JFC và Swing
« Trước: Câu hỏi và bài tập phần Concurrency - Thread
Các khóa học qua video:
Python SQL Server PHP C# Lập trình C Java HTML5-CSS3-JavaScript
Học trên YouTube <76K/tháng. Đăng ký Hội viên
Viết nhanh hơn - Học tốt hơn
Giải phóng thời gian, khai phóng năng lực
Copied !!!