Bài học về notify() và notifyAll()

Hôm nay tôi có cho lớp JC3 tại ActiveStudy làm một bài tập như sau:

Implements mô hình Producer – Consumer sử dụng synchonized, wait và notify

Và đây là một bài làm điển hình:


import java.util.LinkedList;
import java.util.Queue;
/**
*
* @author 404NotFound
*/
public class MyQueue<E> {
private final Object sync = new Object();
private final int capacity;
private Queue<E> queue = new LinkedList<>();
public MyQueue() {
this.capacity = 1024;
}
public MyQueue(int capacity) {
this.capacity = capacity;
}
public void put(E e) {
synchronized (sync) {
try {
while (queue.size() == capacity) {
sync.wait();
}
} catch (InterruptedException ignore) {
}
queue.offer(e);
sync.notify();
}
}
public E take() {
synchronized(sync){
try {
while (queue.size() == 0) {
sync.wait();
}
} catch (InterruptedException ignore) {
}
E e = queue.poll();
sync.notify();
return e;
}
}
public int size(){
return queue.size();
}
}

view raw

MyQueue.java

hosted with ❤ by GitHub

Class MyQueue trên có thể chạy tốt trong rất nhiều trường hợp. Tuy nhiên, đôi khi chương trình lại treo một cách bất thường…

 Để dễ hiểu, tôi tạo một class Main trong đó khởi tạo MyQueue<String> có capacity bằng 1, chương trình chạy với 1 producer P và 2 consumer C1, C2. Lúc đầu tôi nghĩ rằng chương trình treo do deadlock, nhưng khi dump thread thì mọi chuyện lại khác.

“P” #11 prio=5 os_prio=0 tid=0x000000001741f800 nid=0x2cac in Object.wait() [0x0000000017d1f000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
– waiting on <0x00000000e102e270> (a java.lang.Object)
at java.lang.Object.wait(Object.java:502)
at Processor.produce(Processor.java:19)
– locked <0x00000000e102e270> (a java.lang.Object)
at App$1.run(App.java:14)
at java.lang.Thread.run(Thread.java:745)

Locked ownable synchronizers:
– None

“C2” #13 prio=5 os_prio=0 tid=0x000000001741f000 nid=0x2a2c in Object.wait() [0x0000000017c1f000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
– waiting on <0x00000000e102e270> (a java.lang.Object)
at java.lang.Object.wait(Object.java:502)
at Processor.consume(Processor.java:37)
– locked <0x00000000e102e270> (a java.lang.Object)
at App$3.run(App.java:50)
at java.lang.Thread.run(Thread.java:745)

Locked ownable synchronizers:
– None

“C1” #12 prio=5 os_prio=0 tid=0x0000000017429000 nid=0x1728 in Object.wait() [0x0000000017b1f000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
– waiting on <0x00000000e102e270> (a java.lang.Object)
at java.lang.Object.wait(Object.java:502)
at Processor.consume(Processor.java:37)
– locked <0x00000000e102e270> (a java.lang.Object)
at App$2.run(App.java:36)
at java.lang.Thread.run(Thread.java:745)

Locked ownable synchronizers:
– None

Như vậy, cả 3 thread cùng wait. sau khi debug để xem thời gian ở mức nanosecond thì chuyện là thế này (1 trong rất nhiều case):

  • P1: RUNNABLE,  C1: RUNNABLE, C2: RUNNABLE
  • C1 và C2 lần lượt lấy lock và WAITING
  • P1 put String vào queue, notify rồi ngay lập tức quay về BLOCKED
  • Giả sử P1 notify cho C1. Chú ý rằng khi một thread được notify thì nó sẽ chuyển về trạng thái BLOCKED và vẫn phải tranh chấp, cùng với đó nó phải được thread scheduler lập lịch để lấy lock trước khi chuyển về trạng thái RUNNABLE.  Do đó lúc này cơ hội để P1 và C1 lấy lock là như nhau.
  • P1 lấy được lock, chuyển về RUNNABLE. Do queue full, P1 nhả lock và wait.
  • C1 giữ lock, lấy object và in ra màn hình. Sau đó notify cho C2 (Chú ý rằng method notify() sẽ gọi một thread bất kỳ trong số những thread đang wait.).
  • C2 check điều kiện. Hiển nhiên queue trống. C2 tiếp tục wait.
  • C1 đang trong trạng thái BLOCKED, lấy được lock từ C2, check queue –> queue trống –> C1 wait.

–> 3 Thread cùng wait, không có thread nào notify! Mặc dù C2 đã nhả lock, nhưng cả 3 thread đều đang trong trạng thái WAITING, mà lại không có thread nào notify cả.

Đây chắc chắn không phải deadlock, cũng không phải livelock hay starvation. Đây là hiện tượng missing signal, khi một thread bị miss tín hiệu notify. Cuối cùng, producer đã không được nhận signal notify mà nó đáng ra nên được nhận.

Để giải quyết vấn đề này, tôi dùng notifyAll(). Tương tự như trên, nếu dùng notifyAll(), mỗi thread đang wait sẽ được phép bỏ qua synchonized block và execute tiếp sau lệnh wait() một lần (theo thứ tự nhất định chứ không phải đồng thời). Như vậy, sau khi C1 giữ lock và in ra màn hình, cả P và C2 đều có cơ hội được giữ lock. Do khi giữ lock C2 wait() ngay nên P tiếp tục được produce, do đó chương trình chạy ngon lành cành đào.

Chốt: Chỉ dùng notify khi các thread đang wait có cùng công việc, bản chất như nhau, hay nói cách khác thằng nào wakeup không quan trọng. Còn lại thì gọi tất notifyAll.

Chuyện hỏi đáp trên các forum/website/group

Trong những năm gần đây, với sự phổ biến của Internet tại Việt Nam, việc trở thành một coder chưa bao giờ dễ dàng hơn nữa. Hầu hết mọi vấn đề liên quan đến lập trình đều có thể được tìm thấy trong vài click. Khi chương trình xảy ra lỗi, chỉ cần ném thông báo lỗi lên google là đã có thể tìm thấy hàng trăm kết quả giải thích lỗi và cách sửa. Thường kết quả sẽ dẫn đến stackoverflow, hoặc các forum hay website đã có sẵn câu trả lời.

Nếu bạn muốn một câu trả lời dành cho riêng mình, rất đơn giản, bạn chỉ cần hỏi. Tại Việt nam, chúng ta có các Facebook groups, có daynhauhoc.com. Riêng đối với ngôn ngữ Java chúng ta có Cộng đồng Java Việt Nam. Bạn nào biết chút tiếng Anh có thể lên stackoverflow hỏi. Mọi người rất sẵn lòng giúp bạn. Họ đọc câu hỏi rồi bỏ công ra trả lời mà không cần biết bạn là ai.

Họ cũng phải suy nghĩ để trả lời câu hỏi của bạn. Họ nghĩ cách trả lời sao cho bạn có thể hiểu vấn đề dễ dàng nhất. Và họ không lấy của bạn một xu. Có thể họ thích giải quyết vấn đề mà bạn đưa ra, có thể họ thích giúp đỡ mọi người, hoặc họ thích nổi tiếng. Nhưng dù sao, bạn cũng chả tốn một đồng nào để được giúp đỡ.

Tôi thường thấy các coder trẻ vứt câu hỏi lên groups/website rồi ra đi không lời từ biệt. Thật sự sẽ lịch sự hơn nếu bạn báo lại cho người trả lời rằng bạn đã thử cách của họ và thành công, hoặc bạn báo lại cho họ rằng bạn đã tìm ra cách của riêng mình, và giải thích cách làm. Ở những trang như stackoverflow hay daynhauhoc chúng ta có nút “Solution”, báo rằng một câu trả lời nào đó được chấp nhận đã giải quyết được vấn đề trong câu hỏi.

Bạn ra đi không lời từ biệt không chỉ thể hiện thái độ phụ bạc khi bạn đọc câu trả lời của người khác rồi bỏ đi, mà còn thể hiện sự thiếu trách nhiệm với những người sau đó đọc câu hỏi của bạn. Họ có thể nghĩ rằng bạn chưa được giải đáp và lại mất công trả lời bạn. Hoặc những người đang tìm kiếm câu trả lời cũng rất thất vọng khi thấy một câu hỏi không có lời giải đáp. Xin hãy nhớ rằng, chúng ta là một cộng đồng, chúng ta học hỏi từ người khác, cùng với đó chúng ta giúp cộng đồng này trở nên tốt hơn. Bạn nhận được thứ gì đó từ cộng đồng, vậy bạn nên cống hiến lại một giá trị nhất định, kể cả khi giá trị đó rất khiêm tốn.

Không có ai rảnh để chửi mắng bạn khi bạn không trả lời lại những người đã giúp bạn (miễn phí). Họ dành thời gian để tiếp tục giúp đỡ những người khác, trả lời những câu hỏi khác, hoặc câu hỏi tiếp theo của bạn. Họ biết bạn sẽ lại đọc câu trả lời của họ rồi lặn mất, nhưng họ không quan tâm.

Nhưng bạn nên quan tâm.

Hãy trở thành một người lịch sự, ngay cả trên Internet.

Translated from this post.

XML Config API

 

Ngày trước tạo file config dùng class Properties và phải dùng file properties –> gõ nhiều mỏi tay quá nên viết cái thư viện này xài cho tiện 😀 Chuyển luôn qua XML nhìn cho clear.

Jar file: https://github.com/dangxunb/XmlConfig/releases

Source: https://github.com/dangxunb/XmlConfig

Ví dụ file RMQConfig.xml đặt trong thư mục ./config. Custom XML, chỉ theo quy định XML chứ không theo quy định nào khác.


<?xml version="1.0" encoding="UTF-8"?>
<rmq-config>
<connection>
<host>localhost</host>
<port>5672</port>
<username>guest</username>
<password>guest</password>
<retry-policy>
<max-attempt>1</max-attempt>
<interval>1</interval>
</retry-policy>
</connection>
<consumer>
<exchange-declare type="topic" durable="true" auto-delete="false">topic_exchange</exchange-declare>
<queue-declare durable="true" exclusive="false" auto-delete="false">Test Queue</queue-declare>
<queue-bind>
<queue-name>Test Queue</queue-name>
<exchange-name>topic_exchange</exchange-name>
<routing-key>java.share.com</routing-key>
</queue-bind>
<reply-to>
<exchange-name>topic_exchange</exchange-name>
<routing-key>aa.*.*.qee.tttt</routing-key>
</reply-to>
<pool-size>32</pool-size>
</consumer>
</rmq-config>

view raw

rmq-config.xml

hosted with ❤ by GitHub

Vào code tạo interface:


import org.dangnh.xmlconfig.annotation.*;
/**
*
* @author DangNH
*/
@Source("file:./config/RMQConfig.xml") /*URI đến file config*/
public interface RmqExchangeDeclareConfig{
@Key("rmq-config.consumer.exchange-declare")
@DefaultValue("tms-topic") /*giá trị mặc định*/
String name();
@Key("rmq-config.consumer.exchange-declare.type")
@DefaultValue("topic")
String excType();
@Key("rmq-config.consumer.exchange-declare.durable")
@DefaultValue("true")
boolean isDurable();
@Key("rmq-config.consumer.exchange-declare.auto-delete")
@DefaultValue("false")
boolean isAutoDelete();
}

Rồi xài:


import com.elcom.ifccore.config.RmqExchangeDeclareConfig;
import org.dangnh.xmlconfig.ConfigFactory;
/**
*
* @author DangNH
*/
public class TestConfig {
public static void main(String[] args) {
RmqExchangeDeclareConfig config = ConfigFactory.create(RmqExchangeDeclareConfig.class);
//auto convert type
System.out.println(config.excType());
System.out.println(config.isDurable());
System.out.println(config.name());
}
}

view raw

TestConfig.java

hosted with ❤ by GitHub

API tự convert từ String thành kiểu dữ liệu được khai báo. Hiện tại chỉ hỗ trợ các kiểu primitives và String.  Nếu muốn tự convert thành kiểu khác thì implement interface Converter:


import java.lang.reflect.Method;
import org.dangnh.xmlconfig.Converter.Converter;
/**
*
* @author DangNH
*/
public class CustomCvt implements Converter{
@Override
public Object convert(Method method, Class<?> type, String string) {
//custom code here
}
}

view raw

CustomCvt.java

hosted with ❤ by GitHub

Rồi xài:


import org.dangnh.xmlconfig.annotation.*;
/**
*
* @author DangNH
*/
@Source("file:./config/RMQConfig.xml")
public interface RmqConsumerConfig {
@CustomConverter(CustomCvt.class)
@Key("rmq-config.consumer.pool-size")
@DefaultValue("10")
int getConsumerPoolSize();
}

Sau này sẽ thêm mấy thứ linh tinh (auto reload, hỗ trợ thêm các định dạng khác ngoài XML…) vào nhưng vẫn keep simple thế này.

Kỳ lạ chuyện lost message khi dùng RabbitMQ auto ack

Hiện tại tôi đang dùng rabbitmq-java-client-3.6.1. Bài viết này có thể không còn đúng với các phiên bản mới hơn.

Edit: Đã có câu trả lời tại đây.

Tôi có một Publisher đơn giản gửi 1000 persistent message vào exchange topic_test với routing key foo.bar như sau:


import com.rabbitmq.client.*;
public class Publisher {
private static final String EXCHANGE_NAME = "topic_test";
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare(EXCHANGE_NAME, "topic");
String routingKey = "foo.bar";
String message = "TestMsg";
for (int i = 0; i < 1_000; i++) {
/*Persistent message*/
channel.basicPublish(EXCHANGE_NAME, routingKey, MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes());
System.out.println(" [x] Sent " + i);
}
connection.close();
}
}

view raw

Publisher.java

hosted with ❤ by GitHub

Tương ứng là một Subscriber đứng đợi tại durable queue Foo Queue


import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
public class Subscriber {
private static final String EXCHANGE_NAME = "topic_test";
private static final String queueName = "Foo Queue";
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare(EXCHANGE_NAME, "topic");
//create a durable, non-autodelete, non-exclusive queue
channel.queueDeclare(queueName, true, false, false, null);
channel.queueBind(queueName, EXCHANGE_NAME, "foo.bar");
System.out.println(" [*] Waiting for messages");
Consumer consumer = new DefaultConsumer(channel) {
private int i = 0;
@Override
public void handleDelivery(String consumerTag, Envelope envelope,
AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println(" [x] Received " + (++i));
//gracefully exit (not kill -9) if received 10 message
if (i%10 == 0) {
System.exit(0);
}
try {
TimeUnit.MILLISECONDS.sleep(500);
} catch (InterruptedException ignore) {
}
}
};
channel.basicConsume(queueName, true /*auto-ack*/, consumer);
}
}

view raw

Subscriber.java

hosted with ❤ by GitHub

Mọi việc sẽ chẳng có gì nếu tôi để subscriber chạy bình thường. Nhưng tôi muốn test trường hợp subscriber mất kết nối sau khi nhận được 10 message.

Đầu tiên cho Subscriber lắng nghe trên queue: Ok.

Cho publisher chạy: ok.

Capture.PNG

Ngay lập tức Subscriber consume message:

 

Capture.PNG
Consume 10 message rồi ra đi không lời từ biệt

 

Và đây là điều kỳ diệu:

 

Capture.PNG
990 message còn lại cũng ra đi không lời từ biệt

 

Tôi thử chạy lại quá trình trên và xem chuyện gì đã xảy ra?

 

Capture.PNG
Message lên 1k cực nhanh và xuống cũng nhanh không kém

 

Vấn đề này cực kì khó hiểu vì chỉ cần tắt auto ack rồi thay bằng explicit ack là có thể giải quyết được (tốn thêm 1 dòng code)??


import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
public class Subcriber {
private static final String EXCHANGE_NAME = "topic_test";
private static final String queueName = "Foo Queue";
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
final Connection connection = factory.newConnection();
final Channel channel = connection.createChannel();
channel.exchangeDeclare(EXCHANGE_NAME, "topic");
//create a durable, non-autodelete, non-exclusive queue
channel.queueDeclare(queueName, true, false, false, null);
channel.queueBind(queueName, EXCHANGE_NAME, "foo.bar");
System.out.println(" [*] Waiting for messages");
Consumer consumer = new DefaultConsumer(channel) {
private int i = 0;
@Override
public void handleDelivery(String consumerTag, Envelope envelope,
AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println(" [x] Received " + (++i));
//explicit ack
channel.basicAck(envelope.getDeliveryTag(), false);
//gracefully exit (not kill -9) if received 10 message
if (i%10 == 0) {
System.exit(0);
}
try {
TimeUnit.MILLISECONDS.sleep(500);
} catch (InterruptedException ignore) {
}
}
};
channel.basicConsume(queueName, false /*turn-off auto-ack*/, consumer);
}
}

view raw

Subcriber.java

hosted with ❤ by GitHub

Exception Handling in Java – The Bad Parts

Trong bài lần trước, Exception Handling in Java – The Good Parts, tôi đã nêu ra các lợi ích của việc sử dụng Exception Handling trong Java. Tuy vậy, nếu sử dụng sai chỗ, các Exception có thể khiến performance của chương trình bị ảnh hưởng ít nhiều.

Chắc hẳn trong chúng ta ai cũng có lần phải check xem chuỗi (String) nhập vào có phải số nguyên hay không, dù là làm bài tập, hay làm project. Tôi đã từng rất vui khi tìm ra một mẹo có thể xử lý vấn đề này một cách nhanh gọn như sau:


public boolean checkInt(String input){
try {
Integer.parseInt(input);
return true;
} catch (NumberFormatException e) {
return false;
}
}

Cho đến khi tôi thấy method này:


public static boolean isInteger(String str) {
 if (str == null) {
 return false;
 }
 int length = str.length();
 if (length == 0) {
 return false;
 }
 for (int i = 0; i < length; i++) {
 char c = str.charAt(i);
 if (c <= '/' || c >= ':') {
 return false;
 }
 }
 return true;
 }

Vâng, tôi đã thử check xem cái method dài dài kia thì có gì hay hơn cái mẹo mà tôi vẫn hay dùng dù cùng công dụng là check xem có phải số nguyên hay không. Tôi tạo một vòng for và check 10 triệu lần:

Trường hợp chuỗi nhập vào là số nguyên:


public static void main(String[] args) {
 long startTime = System.currentTimeMillis();
 for (int i = 0; i < 10_000_000; i++) {
 checkInt("50");
 }
 long endTime = System.currentTimeMillis();
 System.out.println(endTime - startTime);
 
 startTime = System.currentTimeMillis();
 for (int i = 0; i < 10_000_000; i++) {
 isInteger("50");
 }
 endTime = System.currentTimeMillis();
 System.out.println(endTime - startTime);
 }

Kết quả là 16 và 4. Cái method dài kia nhanh hơn method của tôi chẳng đáng là bao, mà code thì lằng nhằng. Hihi.

Trường hợp chuỗi nhập vào không phải số nguyên:


public static void main(String[] args) {
 long startTime = System.currentTimeMillis();
 for (int i = 0; i < 10_000_000; i++) {
 checkInt("a");
 }
 long endTime = System.currentTimeMillis();
 System.out.println(endTime - startTime);
 
 startTime = System.currentTimeMillis();
 for (int i = 0; i < 10_000_000; i++) {
 isInteger("a");
 }
 endTime = System.currentTimeMillis();
 System.out.println(endTime - startTime);
 }

Tôi giật mình vì kết quả là 7166 và 3. Khoảng cách quá xa và chương trình chạy khá lâu. CPU i5 của tôi cũng sử dụng khoảng 40% trong quá trình chương trình chạy. Tại sao lại như vậy nhỉ?

1. Khái niệm Context-Switch

Khái niệm context-switch có thể rất lạ với một sinh viên chỉ học lập trình phần mềm mà không học gì về khoa học máy tính (như tôi). Bạn có thể hiểu như sau: Context-Switch là quá trình lưu và khôi phục luồng xử lý hiện tại của một process hoặc một thread. Như vậy luồng xử lý đó có thể chạy tiếp vào một thời điểm nào đó sau khi được lưu lại.

Context-switch có thể được sử dụng ở cả phần mềm và phần cứng. Nó là khái niệm cốt lõi của một hệ điều hành đa nhiệm. Trong các chương trình phần mềm, quá trình này sẽ xảy ra gián tiếp khi có bất kì process nào làm gián đoạn luồng xử lý của chương trình và chuyển qua một luồng xử lý khác.

2. Context-Switch trong Java và cái giả phải trả cho Exception-Handling

Dễ thấy, quá trình xử lý Exception của Java sẽ làm xảy ra gián tiếp quá trình Context-Switch, do flow của chương trình bị gián đoạn khi Exception được ném ra. Trong các chương trình Java, quá trình context-switch luôn luôn gây ra sự tốn kém: Non-Reentran Data, Heap + Stack paged out và được lưu lại để phục vụ cho việc quay trở lại ngẫu nhiên, và một môi trường mới sẽ được paged-in. (Bạn đọc hãy vào link wikipedia đọc để hiểu rõ nhất các khái niệm tôi đưa ra, vì khả năng dịch tiếng anh chuyên ngành của tôi có hạn. Rất xin lỗi các bạn).

Chốt lại: Rất tốn tài nguyên –> performance của chương trình bị ảnh hưởng.

3. Khi nào dùng Exception-Handling?

Đây là một câu hỏi không hề có câu trả lời cụ thể. Càng vào code nhiều, bạn sẽ càng hiểu được những trường hợp nào nên dùng, những trường hợp nào thì không. Tôi sẽ nêu ra một vài trường hợp bạn có thể nhận biết được sự nguy hiểm như sau:

  1. Có nhất thiết phải dùng try/catch block không?
  2. Có nhất thiết phải throws exception không?
  3. Đặt try catch trong vòng lặp for – Rất nguy hiểm, cần cân nhắc kĩ. Bạn hoàn toàn có thể đặt for trong try/catch block, nhưng điều ngược lại thì cần phải xem có thực sự cần thiết không.
  4. Nếu bạn buộc phải catch một exception và không làm gì với nó cả thì hãy dùng throws
  5. Xin phép được trích từ stackoverflow:

Capture

Mình còn gà, nên từ quan điểm của mình thì:

  1. Nếu lỗi là một lỗi phổ biến (check số nguyên, chia cho 0, người dùng nhập sai pass…blah…blah…) thì không catch exception
  2. Nếu không thể làm gì để xử lý exception thì không catch nó.

Hi vọng các tiền bối bổ sung thêm.

Exception Handling in Java – The Good Parts

Có một câu châm ngôn đã có từ lâu trong ngành phát triển phần mềm:

80% nỗ lực code của chúng ta được sử dụng trong 20% thời gian.

80% đó là nỗ lực để check và xử lý các lỗi của chương trình. Trong rất nhiều ngôn ngữ lập trình, việc check lỗi, bug rất buồn chán. Nó khiến cho chương trình trở nên hỗn loạn. Tôi đã từng thấy cảnh một nhóm lập trình viên PHP phải túm tụm lại với nhau và debug trên giấy! Phát hiện và xử lý lỗi là một phần vô cùng quan trọng trong bất kì chương trình nào. Java đưa ra một phương pháp hiệu quả và rất lịch sự để làm việc với các lỗi: Exception Handling.

*Exception = exceptional conditions

Xử lý ngoại lệ (Exception Hanling) cho phép các developer có thể phát hiện lỗi mà không phải viết các dòng code test đặc biệt để test các giá trị trả về. Nó còn có một công dụng tốt hơn: Những dòng code để xử lý lỗi sẽ được tách biệt với những dòng code gây ra lỗi đó làm cho code sáng sủa hơn. Như vậy chương trình của chúng ta có thể đưa ra những thông báo lịch sự hoặc xử lý trong im lặng và tiếp tục chạy bình thường. Sau đây tôi sẽ trình bày 3 lợi ích của việc xử dụng Exception Handling trong Java.

The Good Parts – Part 1: Tách bạch code lỗi và code gây lỗi

Giả sử tôi có đoạn code sẽ ghi file vào RAM như sau:

readFile {
    open the file;
    determine its size;
    allocate that much memory;
    read the file into memory;
    close the file;
}

Thoạt nhìn chương trình này có vẻ ổn, nhưng nó đã bỏ qua vài phần quan trọng:

  • Điều gì xảy ra nếu không mở được file?
  • Điều gì xảy ra nếu không tính được dung dượng?
  • Điều gì xảy ra nếu không có đủ RAM?
  • Điều gì xảy ra nếu không đọc được file?
  • Điều gì xảy ra nếu không đóng được file?

Để xử lý những điều trên, hàm này cần có nhiều code hơn:

errorCodeType readFile {
    initialize errorCode = 0;
    
    open the file;
    if (theFileIsOpen) {
        determine the length of the file;
        if (gotTheFileLength) {
            allocate that much memory;
            if (gotEnoughMemory) {
                read the file into memory;
                if (readFailed) {
                    errorCode = -1;
                }
            } else {
                errorCode = -2;
            }
        } else {
            errorCode = -3;
        }
        close the file;
        if (theFileDidntClose && errorCode == 0) {
            errorCode = -4;
        } else {
            errorCode = errorCode and -4;
        }
    } else {
        errorCode = -5;
    }
    return errorCode;
}

Chúng ta dễ dàng nhận thấy có quá nhiều if, else, trả về, ở đoạn code trên. Nói chung là rất khó đọc. Chúng ta có thể sử dụng Exception Handling để giúp đoạn code trên bớt “nguy hiểm” như sau:

readFile {
    try {
        open the file;
        determine its size;
        allocate that much memory;
        read the file into memory;
        close the file;
    } catch (fileOpenFailed) {
       doSomething;
    } catch (sizeDeterminationFailed) {
        doSomething;
    } catch (memoryAllocationFailed) {
        doSomething;
    } catch (readFailed) {
        doSomething;
    } catch (fileCloseFailed) {
        doSomething;
    }
}

Dễ thấy là dùng exception handling thì cũng chẳng giúp chúng ta đỡ được mấy việc. Chúng ta vẫn phải check, phát hiện và xử lý lỗi. Nhưng nó giúp code trông “sáng sủa” hơn rất nhiều.

The Good Parts – Part 2: Truyền lỗi ngược lại call stack

Giả sử tôi có 3 hàm mà hàm 1 gọi hàm 2, hàm 2 gọi hàm 3 như sau:

method1 {
    call method2;
}

method2 {
    call method3;
}

method3 {
    call readFile;
}

Giả sử trong chương trình tôi chỉ gọi đến hàm 1. Đó là hàm duy nhất tôi cần quan tâm. Nhưng hàm 3 lại có thể gây ra lỗi. Tôi có thể xử lý như sau:

method1 {
    errorCodeType error;
    error = call method2;
    if (error)
        doErrorProcessing;
    else
        proceed;
}

errorCodeType method2 {
    errorCodeType error;
    error = call method3;
    if (error)
        return error;
    else
        proceed;
}

errorCodeType method3 {
    errorCodeType error;
    error = call readFile;
    if (error)
        return error;
    else
        proceed;
}

Vâng và tôi chỉ cần quan tâm đến hàm 1, nhưng tôi lại phải đi code cho hàm 2 (hàm không liên quan) và hàm cuối. Lưu ý rằng JVM sẽ tìm ngược trở lại call stack và xem hàm nào sẽ xử lý cái exception mà một hàm gọi nó đã ném ra. Như vậy tôi có thể tôi có thể xử lý bằng cách khác:

method1 {
    try {
        call method2;
    } catch (exception e) {
        doErrorProcessing;
    }
}

method2 throws exception {
    call method3;
}

method3 throws exception {
    call readFile;
}

Như vậy, chỉ có hàm 1 nhận trách nhiệm bắt và xử lý lỗi. Đơn giản hơn rất nhiều.

The Good Parts – Part 3: Nhóm và phân biệt các lỗi khác nhau

Do tất cả các Exception trong java đều là Object, việc nhóm các lỗi lại là điều dễ hiểu sau khi phân chia các class của chúng. Ví dụ FileNotFoundException thông báo chương trình không tìm thấy file trên đĩa cứng. Một method có thể bắt lấy nó:

catch (FileNotFoundException e) {
    ...
}

Do IOException là class cha của FileNotFoundException, chúng ta có thể làm như sau để bắt lấy tất cả các lỗi Input/Output:

catch (IOException e) {
...
}

Trong phần lớn các trường hợp, các lập trình viên sẽ muốn catch một lỗi cụ thể hơn là chỉ ra một lỗi chung chung. Nhưng dù sao, chúng ta có thể catch một lỗi chung, tùy từng tình huống. Đó là một điều tốt.

Bài viết sử dụng tư liệu từ docs.oracle.com và cuốn sách Introduction to Java Programming 10th Edition. Mọi ý kiến đóng góp xin để lại tại phần comment.

Java Stream API: Phân biệt Terminal và Intermediate call

Tôi có một class Person đơn giản chỉ gồm 2 field age và name như sau:


/**
*
* @author NguyenHaiDang@ActiveStudy
*/
public class Person {
private int age;
private String name;
public Person() {
}
public Person(int age, String name) {
this.age = age;
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}

view raw

Person.java

hosted with ❤ by GitHub

Build một stream và thực hiện map/filter:


import java.util.ArrayList;
import java.util.List;
/**
*
* @author NguyenHaiDang@ActiveStudy
*/
public class StreamTest {
public static void main(String[] args) {
List<Person> people = new ArrayList<>();
people.add(new Person(18, "Joe"));
people.add(new Person(20, "Jack"));
people.stream()
.map(p -> p.getAge()) //map tuổi của person
.peek(System.out::println) //in ra màn hình bằng lệnh peek(), mục đích để debug
.filter(age -> age < 20) //filter các person có tuổi nhỏ hơn 20
.forEach(System.out::println); //in ra màn hình tuổi sau khi đã filter
}
}

view raw

StreamTest.java

hosted with ❤ by GitHub

Chương trình chạy tốt như tôi mong đợi, tuy nhiên tôi thử thay forEach() bằng một lệnh peek() khác:


import java.util.ArrayList;
import java.util.List;
/**
*
* @author NguyenHaiDang@ActiveStudy
*/
public class StreamTest {
public static void main(String[] args) {
List<Person> people = new ArrayList<>();
people.add(new Person(18, "Joe"));
people.add(new Person(20, "Jack"));
people.stream()
.map(p -> p.getAge())
.peek(System.out::println) //(1)
.filter(age -> age < 20)
.peek(System.out::println); //thay forEach bằng peek (2)
}
}

view raw

StreamTest.java

hosted with ❤ by GitHub

Lần này cả peek() 1 và peek() 2 đều không hiển thị gì ra màn hình…Vì sao lại như vậy?

Terminal vs Intermediate call

Stream operation được chia ra làm 2 loại:

  • Intermediate operation (hoạt động trung gian) luôn trả về một stream mới. Loại này luôn luôn là lazy operation, nghĩa là chúng sẽ không làm gì cả cho đến khi được gọi. Hàm filter() trên thực tế không filter cái gì cả, mà nó sẽ tạo ra một stream mới, stream mới này khi được duyệt thì sẽ chứa các phần tử của stream đầu tiên thỏa mãn Predicate truyền vào. Nó sẽ không filter cho đến khi được một terminal call gọi.
  • Terminal operation (hoạt động cuối cùng), ví dụ forEach() hay sum(), sẽ duyệt qua các phần tử của stream và trả về kết quả hay in ra màn hình hay làm gì đó…Khi terminal operation được gọi, stream được xem xét là đã bị tiêu thụ, và sẽ không tái sử dụng được nữa.

Như vậy chúng ta có thể hiểu theo cách đơn giản là ở trong ví dụ trên, đầu tiên forEach() sẽ được gọi, sau đó nó mới bắt đầu map, peek và filter rồi trả về cho forEach. Trong ví dụ 2, không có một terminal operation nào thúc đẩy việc duyệt qua stream, peek() là intermediate operation nên không có tác dụng gì, do đó toàn bộ stream vẫn đứng im tại chỗ.

Thông báo khai giảng khóa Java Core for Beginner

Xin chào các bạn, Active Study thông báo khai giảng khóa học Java Core for Beginner đầu tiên trong năm mới.

Rất mong được gặp các bạn. Xin cảm ơn 😀

Tại sao dùng static final cho các hằng số?

Khi khai báo hằng số trong Java, các lập trình viên thường viết như sau:


public static final int MY_CONSTANT = 100;

view raw

Constant.java

hosted with ❤ by GitHub

Câu hỏi đặt ra là: Nếu muốn khai báo một hằng số chỉ cần final là đủ, tại sao lại dùng static?

Đó là bởi vì nếu bạn chỉ khai báo hằng bằng final, mỗi một object tạo ra sẽ có một hằng số riêng của mình. Trong khi đó, do nó là một hằng số, nó không thể thay đổi, tại sao lại phải phí bộ nhớ vì nó?

Vì thế chúng ta đặt thêm keyword static, chỉ ra rằng hằng số này sẽ được dùng chung bởi tất cả các object tạo ra từ class chứa nó. Như vậy sẽ chỉ có một biến hằng như thế được tạo ra để phục vụ cho các object, mà các object này cũng không thể thay đổi được nó.

Java Share Club

Chia sẻ lại là học, đừng ngại chia sẻ

Trung Tuyến Nguyễn - Median Nguyen

TDD craftsman rong chơi giữa đời!