Các vấn đề đa luồng giải quyết trong Java
Về cơ bản, đa luồng Java được phát minh để giải quyết hai vấn đề chính:-
Thực hiện nhiều hành động cùng một lúc.
Trong ví dụ trên, các chủ đề khác nhau (tức là các thành viên trong gia đình) thực hiện song song một số hành động: rửa bát, đi đến cửa hàng, gấp đồ.
Có thể đưa ra một ví dụ khác về “lập trình viên”. Hãy tưởng tượng rằng bạn có một chương trình có giao diện người dùng. Khi nhấp vào nút Tiếp tục, một số phép tính sẽ diễn ra trong chương trình và người dùng sẽ thấy màn hình giao diện sau. Nếu những hành động này được thực hiện tuần tự, sau khi nhấp vào nút “Tiếp tục”, chương trình sẽ chỉ bị treo. Người dùng sẽ thấy cùng một màn hình với nút “Tiếp tục” cho đến khi tất cả các phép tính bên trong được hoàn thành và chương trình đến phần mà giao diện sẽ bắt đầu được vẽ.
Vâng, chúng ta hãy đợi một vài phút!
Chúng tôi cũng có thể làm lại chương trình của mình, hoặc, như các lập trình viên nói, “song song hóa”. Hãy để các phép tính cần thiết được thực hiện trong một luồng và hiển thị giao diện trong một luồng khác. Hầu hết các máy tính đều có đủ tài nguyên cho việc này. Trong trường hợp này, chương trình sẽ không còn “ngu ngốc” và người dùng sẽ bình tĩnh di chuyển giữa các màn hình giao diện mà không cần lo lắng về những gì đang diễn ra bên trong. Nó không can thiệp :)
-
Tăng tốc độ tính toán.
Mọi thứ ở đây đơn giản hơn nhiều. Nếu bộ xử lý của chúng tôi có một số lõi và hầu hết các bộ xử lý hiện nay đều là đa lõi, thì danh sách các tác vụ của chúng tôi có thể được giải quyết song song bằng nhiều lõi. Rõ ràng, nếu chúng ta cần giải quyết 1000 vấn đề và mỗi vấn đề được giải quyết trong một giây, thì một lõi sẽ xử lý danh sách trong 1000 giây, hai lõi trong 500 giây, ba lõi chỉ trong hơn 333 giây, v.v.
Thread
. Nghĩa là, để tạo và chạy 10 luồng, bạn sẽ cần 10 đối tượng của lớp này. Hãy viết ví dụ đơn giản nhất:
public class MyFirstThread extends Thread {
@Override
public void run() {
System.out.println("I'm Thread! My name is " + getName());
}
}
Để tạo và khởi chạy các luồng, chúng ta cần tạo một lớp và kế thừa nó từ tệp java.lang
. Thread
và ghi đè phương thức trong đó run()
. Điều cuối cùng là rất quan trọng. Chính trong phương thức mà run()
chúng ta quy định logic mà luồng của chúng ta phải thực thi. Bây giờ, nếu chúng ta tạo một cá thể MyFirstThread
và chạy nó, phương thức run()
sẽ in một dòng có tên của nó ra bàn điều khiển: phương thức này getName()
in tên “hệ thống” của luồng, được gán tự động. Mặc dù, thực ra, tại sao lại là “nếu”? Hãy tạo và thử nghiệm!
public class Main {
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
MyFirstThread thread = new MyFirstThread();
thread.start();
}
}
}
Đầu ra của bảng điều khiển: Tôi là Thread! Tên tôi là Thread-2 Tôi là Thread! Tên tôi là Thread-1 Tôi là Thread! Tên tôi là Thread-0 Tôi là Thread! Tên tôi là Thread-3 Tôi là Thread! Tên tôi là Thread-6 Tôi là Thread! Tên tôi là Thread-7 Tôi là Thread! Tên tôi là Thread-4 Tôi là Thread! Tên tôi là Thread-5 Tôi là Thread! Tên tôi là Thread-9 Tôi là Thread! Tên tôi là Thread-8 Chúng tôi tạo 10 luồng (đối tượng) MyFirstThread
kế thừa từ đó Thread
và khởi chạy chúng bằng cách gọi phương thức của đối tượng start()
. Sau khi gọi một phương thức , start()
phương thức của nó bắt đầu hoạt động run()
và logic được viết trong đó sẽ được thực thi. Xin lưu ý: tên chủ đề không theo thứ tự. Khá kỳ lạ, tại sao họ không thực hiện lần lượt: Thread-0
, Thread-1
, Thread-2
v.v.? Đây chính xác là một ví dụ khi lối suy nghĩ tiêu chuẩn, “tuần tự” sẽ không hiệu quả. Thực tế là trong trường hợp này, chúng tôi chỉ đưa ra lệnh tạo và khởi chạy 10 luồng. Thứ tự khởi chạy chúng được quyết định bởi bộ lập lịch luồng: một cơ chế đặc biệt bên trong hệ điều hành. Nó được cấu trúc chính xác như thế nào và nó đưa ra quyết định dựa trên nguyên tắc nào là một chủ đề rất phức tạp và chúng ta sẽ không đi sâu vào vấn đề đó ngay bây giờ. Điều chính cần nhớ là lập trình viên không thể kiểm soát trình tự thực hiện luồng. Để nhận ra mức độ nghiêm trọng của tình huống, hãy thử chạy phương pháp main()
từ ví dụ trên một vài lần nữa. Đầu ra bảng điều khiển thứ hai: Tôi là Thread! Tên tôi là Thread-0 Tôi là Thread! Tên tôi là Thread-4 Tôi là Thread! Tên tôi là Thread-3 Tôi là Thread! Tên tôi là Thread-2 Tôi là Thread! Tên tôi là Thread-1 Tôi là Thread! Tên tôi là Thread-5 Tôi là Thread! Tên tôi là Thread-6 Tôi là Thread! Tên tôi là Thread-8 Tôi là Thread! Tên tôi là Thread-9 Tôi là Thread! Tên tôi là Thread-7 Đầu ra của bảng điều khiển thứ ba: Tôi là Thread! Tên tôi là Thread-0 Tôi là Thread! Tên tôi là Thread-3 Tôi là Thread! Tên tôi là Thread-1 Tôi là Thread! Tên tôi là Thread-2 Tôi là Thread! Tên tôi là Thread-6 Tôi là Thread! Tên tôi là Thread-4 Tôi là Thread! Tên tôi là Thread-9 Tôi là Thread! Tên tôi là Thread-5 Tôi là Thread! Tên tôi là Thread-7 Tôi là Thread! Tên tôi là Thread-8
Các vấn đề do đa luồng tạo ra
Trong ví dụ về sách, bạn đã thấy rằng đa luồng giải quyết được các vấn đề khá quan trọng và việc sử dụng nó sẽ tăng tốc hoạt động của các chương trình của chúng ta. Trong nhiều trường hợp - nhiều lần. Nhưng không phải vô cớ mà đa luồng được coi là một chủ đề phức tạp. Suy cho cùng, nếu sử dụng không đúng cách, nó sẽ tạo ra vấn đề thay vì giải quyết chúng. Khi tôi nói “tạo ra vấn đề”, tôi không có ý nói điều gì đó trừu tượng. Có hai vấn đề cụ thể mà đa luồng có thể gây ra: bế tắc và tình trạng dồn đuổi. Bế tắc là tình huống trong đó nhiều luồng đang chờ tài nguyên bị chiếm giữ bởi nhau và không ai trong số chúng có thể tiếp tục thực thi. Chúng ta sẽ nói nhiều hơn về nó trong các bài giảng sau, nhưng bây giờ ví dụ này là đủ: Hãy tưởng tượng rằng thread-1 đang làm việc với một số Object-1 và thread-2 đang làm việc với Object-2. Chương trình được viết như thế này:- Thread-1 sẽ ngừng hoạt động với Object-1 và chuyển sang Object-2 ngay khi Thread-2 ngừng hoạt động với Object 2 và chuyển sang Object-1.
- Thread-2 sẽ ngừng hoạt động với Object-2 và chuyển sang Object-1 ngay khi Thread-1 ngừng hoạt động với Object 1 và chuyển sang Object-2.
public class MyFirstThread extends Thread {
@Override
public void run() {
System.out.println("Выполнен поток " + getName());
}
}
public class Main {
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
MyFirstThread thread = new MyFirstThread();
thread.start();
}
}
}
Bây giờ hãy tưởng tượng rằng chương trình chịu trách nhiệm vận hành một robot chuẩn bị thức ăn! Thread-0 lấy trứng ra khỏi tủ lạnh. Luồng 1 bật bếp. Stream-2 lấy chảo rán ra và đặt lên bếp. Suối 3 thắp lửa trên bếp. Dòng 4 đổ dầu vào chảo. Dòng 5 đập trứng và đổ vào chảo rán. Stream 6 ném vỏ vào thùng rác. Stream-7 loại bỏ trứng bác đã hoàn thành khỏi nhiệt. Potok-8 bày trứng bác ra đĩa. Stream-9 rửa bát. Hãy xem kết quả của chương trình của chúng ta: Đã thực thi Thread-0 Đã thực thi Thread-2 Đã thực thi Thread-1 đã thực hiện Thread-4 đã thực thi Thread-9 đã thực thi Thread-5 đã thực thi Thread-8 đã thực hiện Thread-7 đã thực hiện Thread-7 thread đã được thực thi -3 Thread của Thread-6 đã được thực thi. Kịch bản này có thú vị không? :) Và tất cả là do hoạt động của chương trình của chúng tôi phụ thuộc vào thứ tự thực thi các luồng. Chỉ cần vi phạm trình tự một chút, nhà bếp của chúng tôi sẽ biến thành địa ngục và một con robot phát điên sẽ phá hủy mọi thứ xung quanh nó. Đây cũng là một vấn đề phổ biến trong lập trình đa luồng mà bạn sẽ nghe đến nhiều lần. Cuối bài giảng, tôi muốn giới thiệu cho bạn một cuốn sách về đa luồng.
GO TO FULL VERSION