Java Lập trình tương tranh

Tram Ho

Nhóm các công cụ lập trình ứng dụng vận hành đa nhiệm được Java cung cấp kèm theo hai khái niệm là ProcessThread. Trong đó thì một Process mô tả một môi trường vận hành code hoàn chỉnh, và Thread là một dạng Process với thiết kế đơn giản hơn và tồn tại bên trong Process. Mỗi ứng dụng JVM mặc định là được khởi tạo với một Process, và mỗi Process được mặc định là được khởi tạo với một Thread.

Ở đây chúng ta sẽ không đề cập đến vấn đề quản lý ở cấp độ của các Process mà chỉ tìm hiểu về cách tạo ra các Thread.

Runnable

interface: java.lang.Runnable

Trước khi nghĩ tới việc thực hiện song song các tác vụ thì chúng ta cần một giao diện chung để định nghĩa các tác vụ. Thay vì object A mô tả một tác vụ có phương thức khởi chạy là .start()object B mô tả một tác vụ khác có phương thức khởi chạy là .execute(), thì ở đây chúng ta có Runnable để tạo giao diện lập trình chung cho các object mô tả các tác vụ đơn với phương thức duy nhất .run() để khởi chạy.

Lúc này các object mô tả các tác vụ như $task được tạo ra đã có giao diện lập trình chung, nhưng vẫn chưa được thực thi trên các tiến trình riêng biệt. Bước tiếp theo là chúng ta cần tạo ra các tiến trình thực thi code độc lập so với tiến trình vận hành chính của phương thức main.

Thread

class: java.util.Thread

Chúng ta có thể sử dụng class Thread để tạo ra các object mô tả các tiến trình thực thi code độc lập theo hai cách: Bởi vì class Thread có triển khai Runnable vì vậy nên chúng ta có thể tạo ra các object thuộc class nặc danh và override phương thức .run(); Hoặc, chúng ta cũng có thể truyền một object Runnable vào trình khởi tạo của Thread.

Ở đây chúng ta có câu lệnh Thread.sleep(1000); sẽ tạm dừng tiến trình của Thread đang thực thi $task. Tuy nhiên, do chúng ta đã đặt $task vào một tiến trình mới nên tiến trình của main không bị ảnh hưởng; Và kết quả là câu lệnh in ra dòng chữ "Main thread" sẽ được thực hiện trước so với câu lệnh in trong $task.

Trong trường hợp muốn gộp tiến trình thực thi của một Thread với một tiến trình bất kỳ thì chúng ta cần giữ địa chỉ tham chiếu của Thread đó và gọi phương thức .join() trong tiến trình muốn gộp. Ở đây chúng ta sẽ thử gộp trở lại tiến trình chính main bằng cách gọi phương thức .join() ngay sau khi .start().

Kết quả vận hành:

Synchronized

Ở bài viết trước, khi giới thiệu tổng quan về Java Collections Framework, chúng ta đã thấy Java có cung cấp thêm các phiên bản cấu trúc dữ liệu thread-safe. Cụm từ thread-safe có nghĩa là các cấu trúc này được thiết kế để đảm bảo rằng: trong trường hợp cùng lúc được truy xuất và chỉnh sửa bởi nhiều thread khác nhau thì kết quả hoạt động sẽ luôn ổn định giống như khi làm việc với một thread duy nhất.

Để hiểu rõ hơn ở điểm này, chúng ta sẽ xuất phát với một cấu trúc dữ liệu tự định nghĩa và giả định rằng cấu trúc này sẽ lưu trữ toàn bộ dữ liệu mô tả bối cảnh hoạt động của một ứng dụng.

Lúc này chúng ta có một object Context sử dụng chung bởi tất cả các thành phần trong ứng dụng. Giả sử, chúng ta có thread A thực hiện tăng giá trị và truy xuất, và đồng thời thread B thực hiện giảm giá trị và truy xuất. Lúc này, kết quả vận hành có thể sẽ diễn ra như sau:

  • thread A truy xuất giá trị của data=0 và thực hiện tăng giá trị thành 1.
  • thread B truy xuất giá trị của data=0 và thực hiện giảm giá trị thành -1.
  • thread A ghi giá trị trở lại bộ nhớ data=1.
  • thread B ghi giá trị trở lại bộ nhớ data=-1.
  • thread A truy xuất giá trị và in ra data=-1.
  • thread B truy xuất giá trị và in ra data=-1.

Như vậy là kết quả mà chúng ta dự kiến cho logic hoạt động của thread A đã sai lệch. Thay vì in ra giá trị là 1 thì câu lệnh in của thread A lại in ra -1. Và cấu trúc dữ liệu Context mà chúng ta định nghĩa lúc này được gọi là non-threadsafe – có nghĩa là không đảm bảo an toàn khi sử dụng trong môi trường đa nhiệm. Và để đơn giản hóa giải pháp ở đây thì Java có cung cấp một từ khóa synchronized để có thể sử dụng cho các phương thức và các khối lệnh.

Và đây chính là cách mà Java tạo ra các phiên bản thread-safe của các cấu trúc dữ liệu. Khi có nhiều thread đồng thời gọi một phương thức synchronized, giả sử đầu tiên .increase() được thread A gọi sẽ được xử lý trước như trên. Thì tất cả các lời gọi phương thức khác của SynchronizedContext đều sẽ tạm dừng để chờ .increase() được thực thi xong trên thread A.

Sau khi .increase() được thực thi xong, JVM sẽ tiếp tục kiểm tra thread-id của thread A trong số các lời gọi phương thức còn lại và tìm thấy .getData() để in ra và sẽ tiếp tục ưu tiên thread A xử lý tương tác này. Và kết quả hoạt động mà chúng ta có ở đây sẽ là:

  • thread A truy xuất giá trị của data=0 và thực hiện tăng giá trị thành 1.
  • thread A ghi giá trị trở lại bộ nhớ data=1.
  • thread A truy xuất giá trị và in ra data=1.
  • thread B truy xuất giá trị của data=1 và thực hiện giảm giá trị thành 0.
  • thread B ghi giá trị trở lại bộ nhớ data=0.
  • thread B truy xuất giá trị và in ra data=0.

Trong trường hợp cần chuyển đổi một cấu trúc dữ liệu non-threadsafe thành kiểu synchronized, chúng ta có thể sử dụng các phương thức synchronized của java.util.Collections

Đây có lẽ đã là điểm dừng phù hợp cho chủ đề Concurrency Programming trong Sub-Series mang tính chất giới thiệu ngôn ngữ. Tiếp theo, chúng ta đã có thể suy nghĩ tới việc thực hiện mini project để thực hành áp dụng tư duy OOP vào kiến trúc code của phần mềm muốn xây dựng.

Tới đây thì mình đã phải thay đổi quyết định khá nhiều lần giữa lựa chọn xây dựng một bàn cờ vua chế độ 2 người chơi và một ứng dụng quản lý dữ liệu. Tuy nhiên, cho dù là lựa chọn nào thì cũng sẽ cần phải tạo ra một giao diện đồ họa để tương tác với người dùng thay cho giao diện cửa sổ dòng lệnh. Các lựa chọn còn lại hiển nhiên là:

  • Vẽ giao diện trong cửa sổ trình duyệt web với HTML, CSS, và JavaScript.
  • Hoặc, vẽ giao diện ứng dụng độc lập với một framework nào đó có hỗ trợ Java.

Lựa chọn đầu tiên sẽ kéo theo yêu cầu tìm hiểu về module java.net để tạo máy chủ web làm back-end xử lý logic hoạt động cốt lõi của ứng dụng bằng Java. Nghe thì có vẻ sẽ đỡ tốn công sức hơn bởi chúng ta đã biết về bộ ngôn ngữ front-end của lập trình web. Tuy nhiên, nếu như để tạo ứng dụng vận hành offline thì Java có cung cấp module.desktop hỗ trợ vẽ giao diện người dùng. Nếu bạn không hẳn ưa việc sử dụng các ngôn ngữ nền web ở trên thì có thể đồng hành cùng mình trong những bài viết tiếp theo, tìm hiểu về các package vẽ giao diện đồ họa java.awtjavax.swing.

Chia sẻ bài viết ngay

Nguồn bài viết : Viblo