Giới thiệu

Process

  • Là một chương trình đã được nạp vào bộ nhớ.
  • Có không gian địa chỉ riêng: các biến và các cấu trúc dữ liệu
  • Mỗi process có thể chạy một chương trình khác
  • Các process liên lạc với nhau thông qua hệ điều hành, file, kết nối mạng...
  • Mỗi process có thể chứa nhiều thread
  • Thread - luồng

  • Thread là một chuỗi lệnh chạy tuần tự của một chương trình
  • Các thread của cùng một process dùng chung không gian bộ nhớ của process đó
  • Mỗi thread có ngữ cảnh thực thi riêng (con trỏ lệnh, bộ nhớ stack, biến địa phương...)
  • Các thread của cùng một process liên lạc với nhau qua dữ liệu dùng chung của process
  • Các thread của cùng một process chạy cùng một chương trình
  • Thread còn được gọi là lightweight process
  • Ví dụ sử dụng

  • Web server dùng nhiều thread để xử lý song song các request từ các web browser
  • Web browser dùng nhiều thread để xử lý song song các tác vụ: nhận dữ liệu từ server, hiển thị các nội dung khác nhau trên màn hình, xử lý tương tác người dùng...
  • Lập trình với thread

  • Cách tạo thread
  • Trạng thái của thread
  • Lập lịch
  • Tạo thread

    Đầu tiên, bạn cần định nghĩa công việc bạn muốn chạy trong một thread. Cách làm thông dụng là cài interface Runnable
        public interface Runnable {
            public void run();
        }
    
    Viết một class cài Runnable với hàm run() mô tả công việc cần chạy. Ví dụ:
    public class Worker implements Runnable {
        @Override
        public void run() {
            long sum = 0;
            for (int i = 0; i < 10000000; i++) {
                sum = sum + i; // do some time-consuming work
            }
        }
    }
    
    Để quan sát khi chạy thử lần đầu, bạn có thể thêm vài lệnh để viết gì đó ra màn hình.
    public class Worker implements Runnable {
    
        @Override
        public void run() {
            long sum = 0;
            for (int i = 0; i < 10000000; i++) {
                sum = sum + i; // do some work
                // every n iterations, print an update
                if (i % 1000000 == 0) {
                    System.out.println(Thread.currentThread().getName() + " " + i);
                }
            }
        }
    }
    
    Thread là một class trong thư viện Java đại diện cho một thread. Một chương trình Java khi chạy luôn có ít nhất 01 thread. Lệnh Thread.currentThread().getName() trong đoạn code trên lấy tên của thread hiện đang chạy (hàm Thread.currentThread() trả về thread hiện đang chạy, hàm thread.getName() trả về tên của thread). Sửa đổi tại đoạn code trên sẽ cho bạn biết worker hiện đang chạy tại thread nào.

    Hàm main() của chương trình chạy trên một thread được tạo sẵn tên là main. Bạn có thể chạy chương trình sau để xem Worker chạy trên thread main.

    public static void main(String[] args) {
        Worker worker = new Worker();
        worker.run();
    }
    
    Để cho worker chạy trên một thread khác, không phải thread main mặc định. Ta cần tạo một đối tượng thread mới cho đối tượng Worker và cho thread đó chạy Để tạo thread cho một công việc loại Runnable, ta dùng constructor Thread(Runnable) của lớp Thread.
    Thread a = new Thread(new Worker());
    Để cho thread a chạy, ta dùng lệnh
    a.start();
    Chẳng hạn, dưới đây là một đoạn chương trình ví dụ tạo 02 thread để chạy song song 02 worker:
            Thread a = new Thread(new Worker());
            Thread b = new Thread(new Worker());
    
            System.out.println(Thread.currentThread().getName() + " starting workers...");
            a.start();
            b.start();
            // The current running thread (executing main()) blocks
            // until both workers have finished
            try {
                a.join();
                b.join();
            }
            catch (Exception ignored) {}
    
            System.out.println(Thread.currentThread().getName() + " All done");
    
    Đoạn try-catch với các lệnh join() là để đợi các thread a và b chạy xong rồi mới chạy tiếp, nếu không có đoạn hàm main do ít việc sẽ chạy xong trước khi các thread a và b hoàn thành công việc của mình. Bạn thử bỏ đoạn code trên và chạy lại để xem hiệu ứng. Lớp Thread trong thư viện Java có API như sau:
        public class Thread {
            public Thread(Runnable R);  // Thread ⇒ R.run()
            public Thread(Runnable R, String name);
            public void start(); // bắt đầu chạy thread (sẽ chạy hàm run của Runnable)
            ...
            public String getName();    //lấy tên của thread
            public void interrupt();
            public boolean isAlive();
            public void join();         //đợi đến khi thread chạy xong
            public void setDaemon(boolean on);
            public void setName(String name);
            public void setPriority(int level);
            public static Thread currentThread();       // trả về thread hiện hành
            public static void sleep(long milliseconds);    // cho thread tạm ngừng
            public static void yield();
        }
    
    Bên cạnh cách tạo Runnable và dùng nó để khởi tạo thread, còn có một cách khác là viết lớp con của Thread và override lại hàm run của lớp Thread. Tuy nhiên đó không phải cách được khuyên dùng do code của worker và code của thread sẽ bị rối vào nhau, gây khó khăn khi cần chuyển sang các cách tiếp cận hiệu quả hơn chẳng hạn như Thread Pool (sẽ học sau).

    Các trạng thái của Java thread

    Mỗi thời điểm, mỗi thread có thể nằm trong một trong 04 trạng thái sau:
  • new - thread mới được cấp phát và đang đợi chạy start()
  • runnable - thread có thể chạy
  • blocked - thread đang đợi các sự kiện khác, chẳng hạn đang đợi ghi/đọc dữ liệu
  • terminated - thread đã chạy xong
  • Thread thay đổi trạng thái do một trong các nguyên nhân sau:
  • các hàm start(), yield(), và sleep() được gọi
  • các sự kiện bên ngoài, chẳng hạn như hệ điều hành phát lệnh chạy hoặc tạm dừng, ghi đọc dữ liệu, hàm run() chạy xong....
  • Lập lịch

    Các thread trong một process được chạy lúc nào, ngừng lúc nào là do bộ phận lập lịch của hệ điều hành hoặc máy ảo Java. Do đó mỗi lần chạy sẽ ra thứ tự khác nhau của các kết quả output của các worker. Bạn cũng có thể không nhìn thấy khác biệt vì nếu như công việc của các thread worker quá ngắn,mỗi thread có thể chạy xong trước khi thread tiếp theo được bắt đầu.

    Bài tập

    Sử dụng thư viện đồ họa StdDraw.java, và các lớp Shape, Rectangle, Circle, và chương trình khởi đầu DrawingApp. Chương trình đó chạy vô tận, và cứ nửa giây nó vẽ lại màn hình với một số đối tượng Shape một lần. Hiện giờ không có hiệu ứng gì đặc biệt vì các đối tượng shape không thay đổi hình dạng màu sắc. Hãy viết tiếp để trong lúc chương trình đang chạy có một số worker chạy trên các thread song song, mỗi worker có nhiệm vụ đổi màu một đối tượng shape, khoảng 10 milli giây lại đổi màu một lần.

    Gợi ý: hàm changeColor() của các đối tượng Shape đổi màu của chúng. Các bạn có thể sửa nội dung nếu muốn đổi màu theo cách khác. Hãy tạo thêm các loại worker khác để làm các hiệu ứng như hình di chuyển hoặc hình phóng to, thu nhỏ.

    Tương tranh dữ liệu

    Nhớ là các thread của cùng một process dùng chung một không gian bộ nhớ với các biến và cấu trúc dữ liệu. Do đó, nếu worker chạy tại các thread khác nhau cùng ghi đọc một biến chung thì sẽ dẫn đến tình trạng tương tranh và chạy đua. Nghĩa là tùy theo thứ tự các thread truy nhập một biến mà biến đó sẽ có kết quả cuối cùng khác nhau. Ví dụ, đoạn chương trình sau cho 03 worker chạy song song và dùng chung dữ liệu là một đối tượng Data
        Data data = new Data();
    
        Thread a = new Thread(new DataRace(data));
        Thread b = new Thread(new DataRace(data));
        Thread c = new Thread(new DataRace(data));
    
        a.start();
        b.start();
        c.start();
    
    Trong đó, data là một đối tượng chứa một biến int có giá trị ban đầu bằng 0
        public class Data {
            int x = 0;
        }
    
    còn công việc của worker rất đơn giản là tăng x rồi lại giảm x:
        public void run() {
            for (int i = 0; i < 10000; i++) {
                data.x++;
                data.x--;
            }
        }
    
    Nếu như không có tương tranh và chạy đua giữa các thread thì sau khi cả ba thread kết thúc, giá trị của data.x phải không đổi. Tuy nhiên, thực tế là có chạy đua, do đó mỗi lần chạy ta sẽ có một kết quả khác của data.x. Bạn hãy làm và chạy thử. Nguyên nhân là do trình tự truy nhập.