Tuesday, May 10, 2022

 

Multithreading trong ngôn ngữ java


 

Đa luồng trong ngôn ngữ lập trình Java

Đa luồng hay còn được gọi là Multithreading. Một chương trình đa luồng luôn có 2 tiến trình trở lên chạy song song nhau, mỗi tiến trình đó người ta gọi là một luồng (thread). Luồng là đơn vị nhỏ nhất trong java có thể thực hiện được 1 công việc riêng biệt và các luồng được quản lý bởi máy ảo java (Java Virtual Machine - JVM). Một ứng dụng java ngoài luồng chính có thể có các luồng khác thực thi đồng thời. Đa luồng giúp cho các tác vụ được xử lý độc lập giúp công việc được hoàn thành nhanh chóng. Vậy đa luồng có thể hiểu đơn giản là quá trình xử lý nhiều thread song song nhau và thực hiện các nhiệm vụ khác nhau cùng một lúc.

Lưu ý: chúng ta rất dễ bị nhầm giữa đa nhiệm (Multiprocessing) và đa luồng (Multithreading) . Một ví dụ để chúng ta dễ phân biệt hơn: Ta có 2 tiến trình được gọi là A và A', khi chạy thì sẽ chạy tiến trình A một khoảng thời gian nhất định rồi sẽ tạm dừng và chuyển sang chạy tiến trình A' và với A' cũng chạy một khoảng thời gian và chuyển về A. Quá trình này diễn ra liên tục đến khi nào A hay A' được thục thi xong. Quá trình chuyển đổi giữa A sang A' hoặc từ A' sang A là rất rất nhanh, thời gian cố định để thực thi tiến trình A và A' cũng rất nhanh vì thế người dùng có cảm giác A và A' là hai chương trình chạy song song nhau.

Trình chơi nhạc, trình duyệt web là những ví dụ điển hiền cho đa luồng:

  • Khi vào một website thì sẽ có rất nhiểu resource (css, javascript, image...) được tải về đồng thời bởi các luồng khác nhau
  • Khi play một file nhạc hoặc file video thì các button play, pause, next, stop, change speed... vẫn làm điều khiển được vì luông phát nhạc là luồng riêng biệt với luông tiếp nhận tương tác của người dùng.
  • Trong game chúng ta thường thấy có cảnh nhân vật cưỡi ngựa đi trong trời mưa, nhân vật mà ta tương tác và mưa gió hoạt cảnh là hai luồng khác nhau chạy song song, nhân vật cứ theo điều khiển của người chơi, mưa cứ mua không ảnh hưởng gì đến nhân vật...
  • etc...

Tạo và quản lý luồng trong Java

Ngôn ngữ java cung cấp cho ta đối tượng Thread nhằm thể hiện cơ chế đa luồng. Có hai cách chính để tạo luồng đó là tạo 1 đối tượng của lớp được thừa kế từ lớp Thread hoặc implements từ giao diện Runnable.

So sánh hai cách tạo ra thread hay dùng ở trên:

  • Giống nhau: cùng xử lý để có thể tạo ra một luồng
  • Khác nhau: nếu bạn tạo thread bằng cách implements từ interface Runnable thì bạn có thể kể thừa một class khác ngoài Class Thread, còn nếu bạn tạo thread bằng cách kế thừa Class Thread thì bạn sẽ không thể extend class nào khác vì Java không hỗ trợ đa kế thừa.

Tạo mới một luồng (thread) bằng cách kế thừa Class Thread

Để sử dụng cách tạo luồng này thì điều kiện đầu tiên chúng ta phải tuân thủ đó là class của chúng ta bắt buộc phải kế thực Class Thread:

package FrThread;
/**
 *
 * @author DUYLIEMPRO
 */
public class ThreadFramgia extends Thread{
    //...Nội dung...
}

Ghi đè phương thức (override method) run() ở class mới mà chúng ta vừa tạo ra, những gì trong phương thức run sẽ được thực thi khi luồng bắt đầu chạy.

package FrThread;
/**
 *
 * @author DUYLIEMPRO
 */
public class ThreadFramgia extends Thread{
    @Override
    public void run() {
        System.out.println();
        for (int x = 1; x <= 3; x++) {
            System.out.println(x + " Thread name: " + Thread.currentThread().getName());
        }
    }
}

Lưu ý: Sau khi chạy xong tất cả các câu lệnh trong phương thức run() thì luồng sẽ tự hủy và giải phóng bộ nhớ

Chúng ta sẽ tạo một class Main để test class ThreadFramgia vừa tạo ra ở trên:

package FrThread;
/**
 *
 * @author DUYLIEMPRO
 */
public class Main {
    public static void main(String[] args) {
        //Tạo ra luồng t1 từ class ThreadFramgia
        ThreadFramgia t1 = new ThreadFramgia();
        t1.start();

        //Tạo ra luồng t2 từ class ThreadFramgia
        ThreadFramgia t2 = new ThreadFramgia();
        t2.start();

        //Tạo ra luồng t3 từ class ThreadFramgia
        ThreadFramgia t3 = new ThreadFramgia();
        t3.start();
    }
}

Kết quả sau khi thực thi:

Capture.PNG

Lưu ý: Mọi câu lệnh, nhiệm vụ muốn luồng thực thi chúng ta khái báo trong phương thức run() nhưng khi thực thi luồng ta phải gọi phương thức start(). Vì đây là phường thức đặc biệt mà java xây dựng sẵn trong lớp Thread, phương thức này sẽ yêu cầu máy ảo JVM cấp phát tài nguyên cho luồng mới rồi chạy phương thức run() ở luồng này. Vì vậy, nếu ta gọi phương thức run() mà không gọi start() thì cũng như ta gọi 1 phương thức của 1 đối tượng bình thường và phương thức vẫn chạy trên luồng mà gọi phương thức chứ không chạy ở luồng mới tạo ra nên vẫn chỉ có 1 luồng chính làm việc chứ ứng dụng vẫn không phải là đa luồng.

Tạo mới một luồng (thread) bằng cách implements Interface Runnable

Để sử dụng cách tạo luồng này thì chúng ta phải tạo ra một class và cài đặt Interface Runnable cho nó:

package FrThread;
/**
 *
 * @author DUYLIEMPRO
 */
public class ThreadRunnableFramgia implements Runnable{
    //Class này có implements interface Runnable
}

Viết lệnh cần xử lý ở method run() trong class này, những gì trong phương thức run() sẽ được thực thi khi luồng bắt đầu chạy.

package FrThread;

/**
 *
 * @author DUYLIEMPRO
 */
public class ThreadRunnableFramgia implements Runnable {
    @Override
    public void run() {
        System.out.println();
        for (int x = 1; x <= 3; x++) {
            System.out.println(x + " Thread name: " + Thread.currentThread().getName());
        }
    }

}

Tạo mới một class có tên là TestThreadRunnable trong cùng package FrThread có nội dung như sau:

package FrThread;

/**
 *
 * @author DUYLIEMPRO
 */
public class TestThreadRunnable {
    /**
     * @param args the command line arguments
     */
    public static void main(String[] args) {
        //Tạo ra luồng t1 từ class ThreadRunnableFramgia
        ThreadRunnableFramgia run1 = new ThreadRunnableFramgia();
        Thread t1 = new Thread(run1);
        t1.start();

        //Tạo ra luồng t2 từ class ThreadRunnableFramgia
        ThreadRunnableFramgia run2 = new ThreadRunnableFramgia();
        Thread t2 = new Thread(run2);
        t2.start();

        //Tạo ra luồng t3 từ class ThreadRunnableFramgia
        ThreadRunnableFramgia run3 = new ThreadRunnableFramgia();
        Thread t3 = new Thread(run3);
        t3.start();
    }
}

Và sau khi chạy TestThreadRunnable chúng ta sẽ được kết quả như sau:

Capture2.PNG

Thông tin tham khao khảo liên quan đến luồng (thread)

  • ThreadID: ThreadID là id của luồng, nó dùng để phân biệt với các luồng khác cùng tiến trình hoặc cùng tập luồng. Đây là thông số mà máy ảo java tự tạo ra khi ta tạo luồng nên ta không thể sửa đổi cũng như áp đặt thông số này khi tạo luồng. Nhưng ta có thể lấy được nó thông qua phương thức getId() của lớp Thread Ex:
    @Override
    public void run() {
        System.out.println();
        for (int x = 1; x <= 3; x++) {
            //Ghi ra log thread id cua luong dang chay
            System.out.println(x + " Thread id: " + Thread.currentThread().getId());
        }
    }
  • Priority: Mỗi luồng có 1 độ ưu tiên nhất định, nó là thông số quyết định mức ưu tiên khi cấp phát CPU cho các luồng. Trong Java, đế đặt độ ưu tiên cho 1 luồng ta dùng phương thức:
void setPriority(int newPriority)

newPriority (int) : Là giá trị từ 1 đến 10. Java có định nghĩa sẵn 3 mức ưu tiên chuẩn như sau:

  • Thread.MAX_PRIORITY : giá trí ưu tiên là 10
  • Thread.NORM_PRIORITY : giá trí ưu tiên là 05
  • Thread.MIN_PRIORITY : giá trí ưu tiên là 01

Ex:

package FrThread;
/**
 *
 * @author DUYLIEMPRO
 */
public class Main {

    /**
     * @param args the command line arguments
     */
    public static void main(String[] args) {
        //Tạo ra luồng t1 từ class ThreadFramgia
        ThreadFramgia t1 = new ThreadFramgia();
        //Xét thứ tự ưu tiên cao nhất cho luồng này
        t1.setPriority(Thread.MAX_PRIORITY);
        t1.start();

        //Tạo ra luồng t2 từ class ThreadFramgia
        ThreadFramgia t2 = new ThreadFramgia();
        //Xét thứ tự ưu tiên trung bình cho luồng này
        t2.setPriority(Thread.NORM_PRIORITY);
        t2.start();

        //Tạo ra luồng t3 từ class ThreadFramgia
        ThreadFramgia t3 = new ThreadFramgia();
        //Xét thứ tự ưu tiên thấp nhất cho luồng này
        t3.setPriority(Thread.MIN_PRIORITY);
        t3.start();
    }
}
  • StackSize: là độ lớn của ngăn xếp (tính bằng byte) mà luồng có thể dùng trong quá trình thực thi. Nếu ta không quy định giá trị này hoặc đặt giá trị này bằng 0 thì nó sẽ phụ thuộc vào giá trị mặc định mà JMV quy định và JMV sẽ tự điều chỉnh cho hợp lý. Còn nếu ta có quy định, thread sẽ chỉ cho phép dùng Stack tới mức tối đa mà giá trị này quy định. Ngoài ra bạn có thể sử dụng phần mềm ArcGIS Java Configuration Tool để thiết lập.

Trạng thái của luồng và một số method hay dùng (tham khảo)

Trạng Thái

Trạng thái của luồng được Java định nghĩa ở những trường hợp cụ thể:

Java định nghĩa các trạng thái của luồng trong các thuộc tính static trong Thread.State:

  • Thread.State.NEW : Đây là trạng thái khi luồng vừa được khởi tạo bằng phương thức khởi tạo của lớp Thread nhưng chưa được start(). Ở trạng thái này, luồng được tạo ra nhưng chưa được cấp phát tài nguyên và cũng chưa chạy. Nếu luồng đang ở trạng thái này mà ta gọi các phương thức ép buộc stop,resume,suspend … sẽ là nguyên nhân sảy ra ngoại lệ IllegalThreadStateException .
  • Thread.State.RUNNABLE : Sau khi gọi phương thức start() thì luồng test đã được cấp phát tài nguyên và các lịch điều phối CPU cho luồng test cũng bắt đầu có hiệu lực. Ở đây, chúng ta dùng trạng thái là Runnable chứ không phải Running, vì như đã nói ở phần đầu (Các mô hình đa luồng) thì luồng không thực sự luôn chạy mà tùy vào hệ thống mà có sự điều phối CPU khác nhau.
  • Thread.State.BLOCKED : Đây là 1 dạng của trạng thái “Not Runnable”. Thread chờ 1 đối tượng bị lock bởi JVM Monitor
  • Thread.State.WAITING : Đây là 1 dạng của trạng thái “Not Runnable”. Thread đang chờ 1 notify() từ 1 thread khác. Thread rơi vào trạng thái này do phương thức wait() hoặc join()
  • Thread.State.TIMED_WAITING : Đây là 1 dạng của trạng thái “Not Runnable”. Thread đang chờ 1 notify() từ 1 thread khác trong 1 thời gian nhất định, Thread rơi vào trạng thái này do phương thức wait(long timeout) hoặc join(long timeout)
  • Thread.State.TERMINATED : Thread đã hoàn thành công việc trong run() hoặc bị stop()

Phương thức hay dùng

  • suspend() : Đây là phương thức làm tạm dừng hoạt động của 1 luồng nào đó bằng các ngưng cung cấp CPU cho luồng này. Để cung cấp lại CPU cho luồng ta sử dụng phương thức resume(). Cần lưu ý 1 điều là ta không thể dừng ngay hoạt động của luồng bằng phương thức này. Phương thức suspend() không dừng ngay tức thì hoạt động của luồng mà sau khi luồng này trả CPU về cho hệ điều hành thì không cấp CPU cho luồng nữa.
  • resume() : Đây là phương thức làm cho luồng chạy lại khi luồng bị dừng do phương thức suspend() bên trên. Phương thức này sẽ đưa luồng vào lại lịch điều phối CPU để luồng được cấp CPU chạy lại bình thường.
  • stop() : Luồng này sẽ kết thúc phương thức run() bằng cách ném ra 1 ngoại lệ ThreadDeath, điều này cũng sẽ làm luồng kết thúc 1 cách ép buộc. Nếu giả sử, trước khi gọi stop() mà luồng đang nắm giữa 1 đối tượng nào đó hoặc 1 tài nguyên nào đó mà luồng khác đang chờ thì có thể dẫn tới việc sảy ra deadlock.
  • isAlive() : Phương thức này kiểm tra xem luồng còn active hay không. Phương thức sẽ trả về true nếu luồng đã được start() và chưa rơi vào trạng thái dead. Nếu phương thức trả về false thì luồng đang ở trạng thái “New Thread” hoặc là đang ở trạng thái “Dead”
  • yeild() : Hệ điều hành đa nhiệm sẽ phân phối CPU cho các tiến trình, các luồng theo vòng xoay. Mỗi luồng sẽ được cấp CPU trong 1 khoảng thời gian nhất định, sau đó trả lại CPU cho HĐH, HĐH sẽ cấp CPU cho luồng khác. Các luồng sẽ nằm chờ trong hàng đợi Ready để nhận CPU theo thứ tự. Java có cung cấp cho chúng ta 1 phương thức khá đặc biệt là yeild(), khi gọi phương thức này luồng sẽ bị ngừng cấp CPU và nhường cho luồng tiếp theo trong hàng chờ Ready. Luồng không phải ngưng cấp CPU như suspend mà chỉ ngưng cấp trong lần nhận CPU đó mà thôi.

No comments:

Post a Comment

So sánh các GitFlow model và áp dụng với CICD

https://medium.com/oho-software/so-s%C3%A1nh-c%C3%A1c-gitflow-model-v%C3%A0-%C3%A1p-d%E1%BB%A5ng-v%E1%BB%9Bi-cicd-b6581cfc893a