JavaRush /Blog Java /Random-VI /Đa luồng trong Java: bản chất, ưu điểm và những cạm bẫy t...

Đa luồng trong Java: bản chất, ưu điểm và những cạm bẫy thường gặp

Xuất bản trong nhóm
Xin chào! Trước hết, xin chúc mừng: bạn đã đến được chủ đề Đa luồng trong Java! Đây là một thành tựu nghiêm túc, còn một chặng đường dài phía trước. Nhưng hãy sẵn sàng: đây là một trong những chủ đề khó nhất trong khóa học. Và vấn đề không phải là các lớp phức tạp hay nhiều phương thức được sử dụng ở đây: ngược lại, thậm chí không có hai tá. Quan trọng hơn là bạn cần thay đổi suy nghĩ của mình một chút. Trước đây, các chương trình của bạn được thực hiện tuần tự. Một số dòng mã theo sau những dòng khác, một số phương thức theo sau những dòng khác và nhìn chung mọi thứ đều rõ ràng. Đầu tiên, tính toán một cái gì đó, sau đó hiển thị kết quả trên bảng điều khiển, sau đó kết thúc chương trình. Để hiểu đa luồng, tốt nhất bạn nên nghĩ về sự tương tranh. Hãy bắt đầu với điều gì đó rất đơn giản :) Đa luồng trong Java: bản chất, ưu điểm và cạm bẫy thường gặp - 1Hãy tưởng tượng rằng gia đình bạn đang chuyển từ nhà này sang nhà khác. Một phần quan trọng của việc di chuyển là đóng gói sách của bạn. Bạn đã tích lũy được rất nhiều sách và bạn cần phải cất chúng vào hộp. Bây giờ chỉ có bạn được tự do. Mẹ đang chuẩn bị đồ ăn, anh trai đang thu dọn quần áo, còn chị gái thì đi chợ. Ít nhất, bạn có thể tự mình quản lý và sớm hay muộn bạn thậm chí sẽ tự mình hoàn thành nhiệm vụ, nhưng sẽ mất rất nhiều thời gian. Tuy nhiên, trong 20 phút nữa, chị gái bạn sẽ từ cửa hàng trở về và chị ấy không có việc gì khác để làm. Để cô ấy có thể tham gia cùng bạn. Nhiệm vụ vẫn như cũ: xếp sách vào hộp. Nó chỉ chạy nhanh gấp đôi. Tại sao? Vì công việc được thực hiện song song. Hai “chủ đề” khác nhau (bạn và chị gái của bạn) đang đồng thời thực hiện cùng một nhiệm vụ và nếu không có gì thay đổi thì sự chênh lệch về thời gian sẽ rất lớn so với tình huống bạn làm mọi việc một mình. Nếu anh trai của bạn sớm hoàn thành nhiệm vụ của mình, anh ấy có thể giúp bạn và mọi việc sẽ còn diễn ra nhanh hơn nữa.

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:
  1. 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!

    Đa luồng trong Java: Bản chất, ưu điểm và cạm bẫy thường gặp - 3

    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 :)

  2. 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.

Tuy nhiên, như bạn đã đọc trong bài giảng, các hệ thống hiện đại rất thông minh và ngay cả trên một lõi điện toán, chúng vẫn có thể thực hiện song song hoặc giả song song khi các nhiệm vụ được thực hiện luân phiên. Chúng ta hãy chuyển từ những điều chung chung đến những điều cụ thể và làm quen với lớp chính trong thư viện Java liên quan đến đa luồng - java.lang.Thread. Nói đúng ra, các luồng trong Java được biểu diễn bằng các thể hiện của lớp 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. Threadvà 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ể MyFirstThreadvà 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) MyFirstThreadkế thừa từ đó Threadvà 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-2v.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à đủ: Đa luồng trong Java: Bản chất, ưu điểm và cạm bẫy thường gặp - 4 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:
  1. 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.
  2. 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.
Ngay cả khi không có kiến ​​​​thức sâu về đa luồng, bạn vẫn có thể dễ dàng hiểu rằng sẽ không có kết quả gì. Những sợi dây sẽ không bao giờ đổi chỗ và sẽ đợi nhau mãi mãi. Lỗi có vẻ hiển nhiên nhưng thực tế không phải vậy. Bạn có thể dễ dàng cho phép nó vào chương trình. Chúng ta sẽ xem xét các ví dụ về mã gây ra bế tắc trong các bài giảng sau. Nhân tiện, Quora có một ví dụ thực tế tuyệt vời giải thích thế nào là bế tắc . “Ở một số bang ở Ấn Độ, họ sẽ không bán đất nông nghiệp cho bạn trừ khi bạn đăng ký là nông dân. Tuy nhiên, bạn sẽ không được đăng ký là nông dân nếu bạn không sở hữu đất nông nghiệp.” Tuyệt vời, tôi có thể nói gì đây! :) Bây giờ về điều kiện cuộc đua - trạng thái của cuộc đua. Đa luồng trong Java: bản chất, ưu điểm và cạm bẫy thường gặp - 5Điều kiện tương tranh là một lỗi thiết kế trong một hệ thống hoặc ứng dụng đa luồng trong đó hoạt động của hệ thống hoặc ứng dụng phụ thuộc vào thứ tự các phần của mã được thực thi. Hãy nhớ ví dụ với các luồng đang chạy:
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.
Đa luồng trong Java: bản chất, ưu điểm và cạm bẫy thường gặp - 6
“Thực hành đồng thời Java” được viết vào năm 2006 nhưng vẫn không mất đi tính liên quan. Nó bao gồm lập trình đa luồng trong Java, bắt đầu từ những điều cơ bản và kết thúc bằng danh sách các lỗi và phản mẫu phổ biến nhất. Nếu bạn quyết định trở thành một chuyên gia lập trình đa luồng thì bạn phải đọc cuốn sách này. Hẹn gặp lại các bạn ở những bài giảng tiếp theo! :)
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION