[Java Memory 2] Cách Garbage Collector Java giải phóng bộ nhớ (Stop The World, Reference Counting, Sweep, ..)

This entry is part 2 of 2 in the series Java Memory

Trở thành “mồi” của GC (Garbage Collector)

  • Ta đã biết, khi một đối tượng không còn tác dụng nữa, thì chúng sẽ bị GC dọn đi để tiết kiệm bộ nhớ, nhưng thế nào là “hết tác dụng” ?
  • Có thể tóm gọn bằng một câu cơ bản “Object tại heap sẽ không còn hữu dụng nếu chúng mất kết nối tới stack”
  • Object mất kết nối tới stack khi không còn một con trỏ nào chỉ tới chúng nữa cả, hãy xem thử đoạn code sau:
Object o = new Object();
System.out.println(o);
o = null; 
  • Ở dòng đầu tiên, ta tạo một đối tượng o, o lúc này thực chất đang chỉ tới giá trị thực sự của đối tượng ta vừa tạo ra trong heap
  • Khi in thử đối tượng này ra, ta có output theo mẫu sau
java.lang.Object@4617c264
  • Tiếp theo, ta cập nhật o thành null. Lúc này, đối tượng nằm ở heap không còn cái gì trỏ đến nó nữa, và không gì có thể truy cập lại nó nữa, và lúc này nó trở thành mồi của GC

Vấn đề không đơn giản

  • Ví dụ trên khá đơn giản do chỉ có 1 object, vấn đề sẽ phức tạp hơn khi ta tiếp cận với nhiều object liên kết với nhau 1 lúc
image 31 - quochung.cyou PTIT
  • Giả sử với 6 dòng lệnh sau, ta sẽ đi qua từng dòng một
image 32 - quochung.cyou PTIT
  • Sau 4 dòng đầu tiên, stack và heap của chúng ta có dạng như sau. Tại stack là các biến giữ con trỏ tới các giá trị thực trong heap.
image 33 - quochung.cyou PTIT

  • Sau dòng thứ 5, ta có hình như sau. Ta thấy, dù sau khi đã cập nhật p1 thành null với mong muốn GC sẽ dọn object này, nhưng thực tế, ta thấy ta vẫn có thể truy cập vào p1 bằng person.get(1) qua list persons, tức là ta vẫn có cách reach đến điểm này
image 34 - quochung.cyou PTIT

  • Chỉ sau khi set cả list thành null, ta mới mất hoàn toàn liên hệ với p1
  • => Không khó để đánh giá các phần tử nào sẽ được Garbage Collector dọn khi bạn đã hiểu về quan hệ bên trên. Dễ dàng thấy, sau 6 bước, p1 mất hoàn toàn liên hệ với stack và sẽ được dọn
  • Tuy nhiên, đó là ta nhìn thủ công, còn để garbage collector biết cái nào còn kết nối với stack sẽ tốn một lượng thời gian, và nó sẽ làm chậm hệ thống lại (chi tiết ở bên dưới). Có nhiều cách để làm điều này, và ta sẽ thảo luận ở bên dưới

Đánh dấu đệ quy

  • Ta sẽ thử đánh dấu các object còn live và object nào có thể bị dọn bởi GC. Dễ dàng, ta thêm 1 bit để dánh dấu xem chúng có còn kết nối với stack hay không. Khi tạo, ta sẽ để bit là 0, và khi ở giai đoạn đánh dấu, object là 0 sẽ bị xoá đi, còn nếu vẫn còn sử dụng, ta sẽ cập nhật nó thành 1
  • Tuy nhiên, heap và stack thay đổi liên tục. Cách đánh dấu được triển khai tuỳ thuộc vào phiên bản Java và GC bạn sử dụng, tuy nhiên, ta sẽ thử xem cách hệ thống đánh từ stack, hãy thử xem ví dụ sau
image 35 - quochung.cyou PTIT
  • Đầu tiên toàn bộ phần tử sẽ được để là 0
image 36 - quochung.cyou PTIT
  • Sau đó, toàn bộ các phần tử có kết nối trực tiếp tới stack được đánh thành 1
image 37 - quochung.cyou PTIT
  • Tuy nhiên, do p1 vẫn có thể truy cập từ danh sách persons, và hơn nữa, ta không thể cứ dọn những gì vẫn còn kết nối. Vì vậy ta cần một cách duyệt đơn giản, với điểm khởi đầu từ các điểm đang = 1, sau đó đi đến mọi quan hệ của nó và đánh chúng thành 1, rồi tiếp tục đệ quy.
  • Các thuật toán đánh dấu đóng vai trò quan trọng trong giai đoạn đánh dấu, đầu tiên thử đi qua cách stop-the-world

Kĩ thuật Stop-The-World – Dừng thế giới

image 38 - quochung.cyou PTIT
  • Với cách đánh dấu trên, ta nhận thấy. Nếu một phần tử được tạo trong quãng đánh dấu, nó sẽ không còn đúng nữa.
  • Vì vây, có một solution đơn giản là ta sẽ dừng mọi luồng khác và chỉ chạy luồng đánh dấu của cả chương trình, điều này sẽ rất ảnh hưởng đến hiệu năng. Ta sẽ thử xem các thuật toán tiếp theo.

Kĩ thuật Reference counting – Đếm liên hệ

image 39 - quochung.cyou PTIT

  • Một cách triển khai khác là đếm số lần một object được trỏ đến. Mỗi object sẽ chứa số lần object được trỏ như một thông số mà nó nắm giữ. Như vậy, GC chỉ việc quét qua và xoá mọi object có 0 lần bị nắm giữ. Cách này sẽ không cần stop-the-world như cách đánh số đệ quy nữa, vì khi object được tạo ra giữa chừng lúc đánh số lần trỏ thì nó vẫn đều là 1 rồi.
  • Tuy nhiên, nó có một điểm yếu là sẽ tạo ra island of isolation, hay các vùng, một tập các object tự trỏ nhau nhưng mà thực tế không có kết nối tới stack
image 40 - quochung.cyou PTIT

Giải phóng bộ nhớ

  • Cách thức làm sao để đánh dấu các object sẽ bị xoá sẽ được quyết định khác nhau bởi phiên bản Java và các kiểu GC khác nhau.
  • Giả sử ta đã đánh dấu được hết các object sẽ có thể bị xoá, tuy nhiên việc xoá chúng đi cũng không phải một quá trình đơn giản
  • Việc xoá object được gọi là sweeping by garbage collector, trong bài viết sẽ mention 3 cách thức sweeping khác nhau
    • Normal sweeping
    • Sweeping with compacting
    • Sweeping with copying

Normal sweeping

image 41 - quochung.cyou PTIT
  • Hình ảnh trên thể hiện các khối bộ nhớ trong ram, các vùng có dấu X là các object đã được đánh dấu và chuẩn bị bị xoá
image 42 - quochung.cyou PTIT
  • Sau khi xoá đi, vùng nhớ của ta có dạng như sau, dễ thấy, điều này dẫn tới một triệu chứng có tên Fragmentation

Fragmentation trong Java Garbage Collector

image 43 - quochung.cyou PTIT
  • Lúc này, vì các vùng trống nằm ở giữa các vùng bị chiếm dụng, nên ta chỉ có thể thêm các bộ nhớ nhỏ hơn hoặc bằng vùng vào các vùng trống. Điều này sẽ dẫn tới 1 vài vấn đề khi ta muốn cấp phát một bộ nhớ lớn hơn
image 44 - quochung.cyou PTIT
  • Giả dụ với trường hợp cấp phát một vùng nhớ lớn hơn các khe trống như ảnh trên, khi cấp phát vào, ta chỉ có thể xếp như sau:
image 45 - quochung.cyou PTIT
  • Dù tổng thể bộ nhớ còn trống ta vẫn đủ để xếp vùng nhớ, nhưng thực tế thì ta không có một vùng nhớ liên tiếp nào chứa đủ vùng nhớ mới này. Và việc này sẽ throw ra 1 runtime exception là OutOfMemoryError.

Ưu nhược điểm của Normal sweeping

  • Normal Sweeping là một kĩ thuật giải phóng vùng nhớ ngây thơ, khá tiện dụng và đơn giản. Tuy nhiên sẽ dẫn tới các vùng nhớ bị phân mảnh. Quá trình này phù hợp khi ta có nhiều bộ nhớ, và ta chỉ cần nhanh chóng dọn bộ nhớ đi. Khi mà lượng vùng nhớ còn trống nhỏ hơn, ta sẽ prefer các kĩ thuật khác

Sweeping with compacting

  • Sweeping with compacting là một quá trình 2 bước. Đầu tiên, chúng vẫn giải phóng bộ nhớ, nhưng sau đó ta sẽ thực hiện thêm 1 bước gọi là compacting (thu gọn), ta sẽ dời toàn bộ vùng nhớ về phía đầu để đảm bảo không có bất kì khoảng trống nào ở giữa
image 46 - quochung.cyou PTIT

Ưu nhược điểm Sweeping with compacting

  • Cách làm này giúp bộ nhớ không còn bị phân mảnh như thông thường, tuy nhiên việc di chuyển các vùng nhớ về đầu là một quá trình tốn kém, vì gần như với lượng vùng nhớ nhỏ và trải dài nhiều, ta sẽ phải copy và di chuyển khá nhiều trên vùng nhớ.

Sweeping with copying

image 50 - quochung.cyou PTIT
  • Ở cách làm này, ta sẽ cần 2 vùng nhớ khác nhau. Ta sẽ không trực tiếp xoá các vùng nhớ bị đánh dấu là xoá đi, mà ta sẽ copy các vùng nhớ k bị xoá vào vùng nhớ mới
image 51 - quochung.cyou PTIT
  • Sau đó mới thực hiện xoá toàn bộ vùng nhớ ở vùng nhớ cũ
image 52 - quochung.cyou PTIT

Ưu nhược điểm của Sweeping with copying

  • Dữ liệu không bị phân mảnh
  • Về hiệu năng thì nhanh hơn compacting, do không phải thực hiện nhiều công đoạn tính toán khi di chuyển, mà chỉ copy nhanh chóng sang vùng mới đang trống hoàn toàn (ở compacting ta ví dụ di chuyển vùng 30-50 sang vùng 0-10, đầu tiên ta phải di 30-40, sau đó lại di vùng 10-20 đi, …. và khó khăn hơn nhiều)
  • Tuy nhiên cần nhiều bộ nhớ hơn vào cùng 1 thời điểm, cần lượng bộ nhớ trữ còn lại đủ nhiều để di chuyển.

[Java Core] B3: Cách Java quản lý dữ liệu

This entry is part 3 of 8 in the series Java Core

Mục lục:

  • Cách Java lưu trữ dữ liệu (Kiểu dữ liệu nguyên thuỷ, Object, Wrapper class, Auto-boxing, Auto-unboxing)
  • Constructor, Super, This
  • Pass by value
  • Garbage Collector

I. Cách Java lưu trữ dữ liệu (Kiểu dữ liệu nguyên thuỷ, Object, Wrapper class, Auto-boxing, Auto-unboxing)

1. Kiểu dữ liệu nguyên thuỷ (Primitive data type)

  • Trong Java, có 8 kiểu dữ liệu nguyên thuỷ (Primitive data type): byteshortintlongfloatdoublebooleanchar.
  • Kiểu dữ liệu nguyên thuỷ là các kiểu dữ liệu mà không phải là đối tượng, không có các phương thức, thuộc tính, … như các đối tượng.
  • Tức là, bạn không thấy các phương thức toString()equals()hashCode(), … trong các kiểu dữ liệu nguyên thuỷ.
inta=1;

// a không có hàm hay biến như các object// a.toString() không tồn tại// a.equals() không tồn tại// a.(gì đó) không tồn tại
Primitive data type
  • Ví dụ, khi khai báo 1 biến int a = 1;, thì Java sẽ cấp phát 1 vùng nhớ có kích thước 4 byte để lưu giá trị 1 vào biến a. Thực chất trong bộ nhớ nó được lưu theo binary là 00000000 00000000 00000000 00000001. (1 byte = 8 bit)
  • Do có các 32 bit, nên int có thể lưu giá trị từ -2^31 đến 2^31 – 1. (1 bit để lưu dấu)
  • Tương tự, các kiểu dữ liệu khác cũng được lưu theo binary, và có kích thước khác nhau.

2. Object

2.1 Object là gì?

image 6 - quochung.cyou PTIT
  • Trong Java, mọi thứ đều là đối tượng (Object). Vậy nên các kiểu dữ liệu nguyên thuỷ cũng là đối tượng.
  • Hiện tại các em chưa học về kế thừa, nhưng cứ tạm hiểu, khi ta nói về 1 tập đối tượng, ví dụ: Xe Ô Tô, hay class Car, có các thuộc tính, phương thức gì đó. Xe máy, hay class Motorbike, cũng có các thuộc tính, phương thức gì đó. Vậy thì Xe Ô Tô và Xe Máy đều là đối tượng, và chúng có các thuộc tính, phương thức giống nhau.
  • Vậy ta gọi tập đối tượng có cả xe máy và xe ô tô, không chứa các phương thức đặc thù như chạydừng, … là gì? Đó là tập đối tượng Xe, hay class Vehicle. Vậy nên, Xe Ô Tô và Xe Máy đều là Xe, hay Vehicle. Tập hợp Vehicle có thể nằm trong tập hợp to hơn là Phương Tiện Giao Thông, hay class Transportation.
  • Trong Java cũng như vậy, mọi class các em tạo ra mặc định đều thuộc 1 tập hợp chung là Object. Và class này luôn cung cấp cho mọi object tạo ra từ các class khác các phương thức, thuộc tính như toString()equals()hashCode(), … như đã nói ở trên.

2.2 Object được lưu thế nào trong Java

image 7 - quochung.cyou PTIT
  • Hiểu đơn giản, giá trị cách lưu object thực sự rất phức tạp, điều đó đều do Java quản lý, và ta không cần quan tâm. Ta chỉ cần biết, khi tạo ra 1 object, ta chỉ lưu 1 địa chỉ tới object đó. Và từ địa chỉ này, ta có thể ra lệnh từ xa để lấy các thuộc tính, phương thức của object đó.
  • Vậy nên, khi ta gán 1 object cho 1 biến, thực chất ta đang gán 1 địa chỉ tới object đó cho biến đó.
Studentobj=newStudent();

// obj là 1 địa chỉ tới object Student
  • Vậy nên ta không thể so sánh 2 object với nhau bằng toán tử ==, mà phải dùng phương thức equals().
Studentobj1=newStudent();

Studentobj2=newStudent();

// obj1 != obj2// obj1.equals(obj2) == false

2.3 Wrapper class

  • Wrapper class là các class được tạo ra để bao bọc các kiểu dữ liệu nguyên thuỷ. Ví dụ: IntegerFloatDoubleBooleanCharacter, …
  • Wrapper class cung cấp các phương thức để thao tác với các kiểu dữ liệu nguyên thuỷ. Ví dụ: Integer.parseInt()Integer.toString()Integer.valueOf(), …
Primitive data typeWrapper class
byteByte
shortShort
intInteger
longLong
floatFloat
doubleDouble
booleanBoolean
charCharacter
  • Các kiểu dữ liệu lưu dạng object này tuân theo cách Java lưu trữ object, tức là không lưu trực tiếp object, mà chỉ lưu địa chỉ tới object đó.
Ưu điểmNhược điểm
Có thể sử dụng các phương thức của objectTốn bộ nhớ hơn
  • Ví dụ, ở Java ta có thể chuyển String thành số, từ các số hay từ 1 object bất kì sang String rất nhanh bằng toString(), ngoài ra khi nó là Object, ta có thể sử dụng các kĩ thuật OOP như kế thừa, đa hình, … (sẽ nói ở các buổi sau).
inta=1;

// a không có hàm hay biến như các object// a.toString() không tồn tạiIntegerb=1;

// b có hàm toString()// b.toString() tồn tại

2.4 Auto-boxing và Auto-unboxing

  • Auto-boxing là quá trình chuyển đổi từ kiểu dữ liệu nguyên thuỷ sang kiểu dữ liệu lưu dạng object.
  • Auto-unboxing là quá trình chuyển đổi từ kiểu dữ liệu lưu dạng object sang kiểu dữ liệu nguyên thuỷ.
  • Ví dụ:
inta=1;

Integerb=1;

// Auto-boxingIntegerc= a;

// Auto-unboxingintd= b;

  • Auto-boxing và Auto-unboxing là 2 quá trình tự động, do Java tự động thực hiện. Vậy nên các em không cần phải quan tâm đến nó.
  • Tuy nhiên, khi sử dụng Auto-boxing và Auto-unboxing, các em cần phải cẩn thận, vì nó có thể gây ra lỗi.
Integera=null;

intb= a; // NullPointerException
  • Vậy nên, khi sử dụng Auto-boxing và Auto-unboxing, các em cần phải kiểm tra giá trị null trước khi sử dụng.
Integera=null;

intb= a == null ? 0 : a;

  • Auto-boxing và Auto-unboxing có thể gây ra lỗi khi sử dụng toán tử ==.
Integera=1;

Integerb=1;

Integerc=128;

Integerd=128;

System.out.println(a == b); // true

System.out.println(c == d); // false
  • Autoboxing và Auto-unboxing giúp cho việc sử dụng các kiểu dữ liệu nguyên thuỷ và object trở nên dễ dàng hơn. Ví dụ, khi ta muốn sử dụng HashMap, thì ta không thể sử dụng kiểu dữ liệu nguyên thuỷ là int, mà phải sử dụng Integer.

HashMap<Integer, String> map = newHashMap<>();

map.put(1, "Hello");

map.put(2, "World");

System.out.println(map.get(1)); // Hello

System.out.println(map.get(2)); // World
  • Nếu không có Auto-boxing và Auto-unboxing, thì ta phải làm như sau:

HashMap<Integer, String> map = newHashMap<>();

map.put(Integer.valueOf(1), "Hello");

map.put(Integer.valueOf(2), "World");

System.out.println(map.get(Integer.valueOf(1))); // Hello

System.out.println(map.get(Integer.valueOf(2))); // World
  • Tuy nhiên, trong nhiều trường hợp nên hạn chế dùng các kiểu Object do chúng tốn bộ nhớ
for (Integeri=0; i < 1000000; i++) {

  // ...

}

// Tốn bộ nhớ hơn// Vì i là object, nên phải tạo ra 1 object mới mỗi lần tăng i// Còn nếu dùng int, thì chỉ cần tạo ra 1 biến int i, và tăng i mỗi lần

3. Các phương thức khởi tạo (Constructor) trong Java

  • Constructor là một phương thức đặc biệt, nó được gọi khi ta khởi tạo một đối tượng. Constructor có tên giống với tên class, và không có kiểu trả về.
  • Constructor được sử dụng để khởi tạo các giá trị ban đầu cho đối tượng. Ví dụ như ta có một class Student, trong đó có các thuộc tính nameageaddressmathliteratureenglish. Ta có thể tạo một constructor như sau:
classStudent {
  String name;
  int age;
  String address;
  float math;
  float literature;
  float english;

  Student(String name, int age, String address, float math, float literature, float english) {
    this.name = name;
    this.age = age;
    this.address = address;
    this.math = math;
    this.literature = literature;
    this.english = english;
  }
}

  • Constructor có thể có hoặc không có tham số. Nếu không có tham số, thì constructor đó được gọi là constructor mặc định. Nếu có tham số, thì constructor đó được gọi là constructor có tham số.
  • Constructor có thể có nhiều tham số, và các tham số đó có thể là bất kỳ kiểu dữ liệu nào.
  • Constructor có thể có nhiều hơn một, và các constructor đó có thể có số lượng tham số khác nhau.
  • Constructor có thể gọi constructor khác của cùng một class bằng từ khóa this(). Ví dụ:
classStudent {
  String name;
  int age;
  String address;
  float math;
  float literature;
  float english;

  Student(String name, int age, String address, float math, float literature, float english) {
    this.name = name;
    this.age = age;
    this.address = address;
    this.math = math;
    this.literature = literature;
    this.english = english;
  }

  Student(String name, int age, String address) {
    this(name, age, address, 0, 0, 0);
  }
}

4. Các khái niệm cơ bản về Garbage Collector (GC)

image 8 - quochung.cyou PTIT
image 9 - quochung.cyou PTIT
  • Trong quá trình thực thi chương trình, Java sẽ liên tục tìm các Object không được tham chiếu tới Heap Memory (mang giá trị null hoặc một vài trường hợp khác). Sau đó Garbage Collector sẽ tự động giải phóng bộ nhớ cho các ô nhớ chứa tham chiếu đó.
  • Điều này kiến lập trình viên không cần phải giải phóng bộ nhớ thủ công như C/C++. Thay vào đó, để giải phóng bộ nhớ, lập trình viên chỉ cần loại bỏ tham chiếu của Object.

5. Cách Java truyền tham số | Pass-by-value

Cách Java truyền tham số

5.1. Pass-by-value

  • Trong Java, khi ta truyền tham số vào một hàm, thì tham số đó sẽ được copy ra một vùng nhớ khác, và hàm sẽ thao tác với tham số ở vùng nhớ mới này.
  • Vậy nên, khi ta thay đổi giá trị của tham số trong hàm, thì giá trị của tham số bên ngoài hàm không bị thay đổi.
publicclassMain {
  publicstaticvoidmain(String[] args) {
    inta=1;
    System.out.println(a); // 1
    change(a);
    System.out.println(a); // 1
  }

  publicstaticvoidchange(int a) {
    a = 2;
  }
}

  • Tuy nhiên, nó sẽ lại thay đổi được các thuộc tính của object.
publicclassMain {
  publicstaticvoidmain(String[] args) {
    Studentstudent=newStudent();
    student.name = "A";
    System.out.println(student.name); // A
    change(student);
    System.out.println(student.name); // B
  }

  publicstaticvoidchange(Student student) {
    student.name = "B";
  }
}

classStudent {
  String name;
}

5.2. Tại sao pass-by-value mà String lại thay đổi được?

image 13 - quochung.cyou PTIT
  • Ta hãy quay trở lại khái niệm vừa nhắc tới Khi ta truyền một biến nguyên thuỷ, hay trong trường hợp trên là age, ta đang như tạo một bản sao của một tờ giấy bình thường, tờ giấy đó ghi số là 20, ta đưa cho người khác. Họ sửa chúng thành 90, 100 hay gì đi chăng nữa, thì tờ giấy ban đầu của ta vẫn là 20
  • Tuy nhiên, khi ta truyền một biến reference (tham chiếu) của Object person, ta đang truyền bản sao của “tham chiếu” của nó. Hay đơn giản là, ta đang tạo ra bản sao của một cái điều khiển, vậy thì cái điều khiển bản sao này khi đưa cho người khác, vẫn sẽ bật tắt được tivi ban đầu của mình.

3 cách thực hiện Dependency Injection (DI) và vấn đề với @Autowired trong Spring

Lời mở đầu

Khi code các ứng dụng backend bằng Java với Spring Framework, khả năng cao là bạn đã gặp qua một đoạn code sử dụng @Autowired. Tuy nhiên, nếu bạn sử dụng một số phần mềm lint để kiểm tra code, bạn có thể thấy các phần mềm sẽ cảnh báo @Autowired is deprecated (Autowired không được khuyến khích sử dụng nữa, cách làm không tốt)

image - quochung.cyou PTIT
  • Nhìn sơ qua thì đây có vẻ là một cách đơn giản và nhanh gọn để thực hiện DI qua Spring. Chỉ cần khai báo các dependency rồi thêm một annotation (@Autowired), các phần liên kết sẽ được Spring xử lí
  • Tuy nhiên, cách làm phổ biến này lại có thể dẫn đến nhiều vấn đề về sau này. Hãy thử xem qua các thủ pháp DI được sử dụng qua Spring Framework và các ưu nhược điểm của chúng

3 cách thực hiện Dependency Injection trong Spring Framework

Field Injection

  • Field Injection chính là tên gọi của cách inject dependency khi ta khai báo bằng @Autowired
  • Về cơ bản khi khai báo annotation này trên 1 field, method, constructor. Spring sẽ tự tìm dependency phù hợp và kết nối chúng với nhau.
  • @Autowired về mặc định không sai, tuy nhiên nếu sử dụng không hợp lí chúng có thể gây ra một số vấn đề về sau.
  • Ví dụ với một code như sau:
image 1 - quochung.cyou PTIT
  • Khai báo một Multiplier trong Calculator nhanh chóng bằng @Autowired sẽ không báo lỗi gì khi compile, về cách hoạt động, khi chạy chương trình, Spring sẽ tự tìm kiếm dependency và kết nối chúng với nhau
  • Tuy nhiên, vì chúng tự động, nên ta thiếu kiểm soát hơn phần này. Tức là lúc này ta không có cách nào để thêm thủ công dependency nữa cho các hoàn cảnh khác ngoài lúc chạy ứng dụng (VD khi chạy test)
image 2 - quochung.cyou PTIT
  • Ví dụ như code trên sẽ không báo lỗi, tuy nhiên khi chạy test thì chúng có thể throw NullPointer
  • Lí do là ở file test trên thì chúng ta đang không có chỗ nào liên kết với Spring cả, vì vậy Spring sẽ không quản lý các bean mà ta đã tạo (Khai báo @Component ở các class kia) nên chúng sẽ không nối vào nhau được -> Ta sẽ bị null pointer vì Multiplier không được khởi tạo lên
  • Để xử lý vấn đề này, thì nếu sử dụng 2 thủ pháp DI bên dưới (Constructor Injection và Setter Injection) , ta có thể có nhiều khả năng kiểm soát hơn và thêm dependency một cách thủ công mà không qua Spring, tạo nhiều khả năng hơn cho việc test
  • Hoặc, ta có thể khai báo @SpringBootTest để Spring quản lý file test này và tự động thực hiện việc gắn các bean mà không cần gắn chay
image 20 - quochung.cyou PTIT

Setter Injection

image 3 - quochung.cyou PTIT
  • Cách làm trên như tên của nó, ta sẽ set một dependency của class lớn hơn ở thời điểm runtime
  • Cách làm này cho phép ta dễ dàng thay đổi dependency của một class trong runtime, ví dụ như đổi 1 repo khác 1 service nào đó, …
  • Tuy nhiên, ngoài ưu điểm là sự flexible khi set ở runtime, thì nó cũng đi cùng vài nhược điểm khác
  • Do được set trong runtime, ta đôi khi có thể gặp phải NullPointer khi dependency chưa được khởi tạo, là null, … (vì lúc này chúng là optional, không bắt buộc với class nữa)
  • Do nó lỏng lẻo hơn nên lúc này với các method trong class gọi đến dependency, ta không được đảm bảo là dependency đã được set, hoặc ta lại phải thêm code để kiểm tra là có dependency chưa, 1 là thêm code, 2 là dẫn tới nullpointer
image 21 - quochung.cyou PTIT
  • Với cách làm như thế này, ta có thể không cần Spring quản lý các bean mà có thể test chay hơn, có nhiều quyền kiểm soát hơn vào việc quản lý các dependency

Constructor Injection

image 4 - quochung.cyou PTIT
  • Như tên của thủ pháp, ta sẽ thêm dependency cho 1 class ngay tại thời điểm khởi tạo class cha
  • Ở file test trên, mình đang sử dụng mock để dựng class multiplier lên theo kèm để gắn vào Calculator qua constructor
  • Việc này đảm bảo 1 class luôn có đủ dependency để khởi tạo (tuỳ theo cách custom constructor)
  • Hoặc mình hoàn toàn có thể làm như thế này
image 22 - quochung.cyou PTIT
  • (Việc sử dụng mock cho phép nhiều khả năng khác trong test hơn, ví dụ mình có thể kiểm tra xem một hàm trong class đó được gọi bao nhiêu lần, … , chúng được quản lý theo dạng Proxy Design Pattern, mình sẽ nói ở bài viết khác)

Bạn có thể đọc thêm bài viết này về một số khái niệm trong Spring như IoC, Bean, …

Tham khảo:

[Web-Buổi 1] Tìm hiểu về cách hoạt động của Web, HTML cơ bản

Mục lục:

  • Cách thức hoạt động của một trang web
  • Cấu trúc của HTML
  • Block, inline elements. Qua phân biệt div, span.
  • Các thẻ liên quan đến table trong HTML
  • Các thẻ: image, video, links, audio
  • Section elements: div, span, header, footer, nav, main, section, article
  • HTML text fundamentals: h1 -> h6, p, ul, li
  • Các thẻ liên quan đến form: form, input, button, label, textarea
  • Có thể tìm hiểu các thẻ khác ngoài các thẻ trên các thẻ trên

I. Cách thức hoạt động của một trang web

Khi máy tính nhà của mình kết nối đến một trang web trên mạng, ta có thể gọi chúng là các clients (máy khách) và servers (máy chủ).

Ảnh minh hoạ: ảnh minh hoạ 1

Client máy khách sẽ gửi các Requests (yêu cầu) về máy chủ, và máy chủ sẽ trả về Response (Phản hồi) tương ứng với yêu cầu của người dùng.

VD:

  • Người dùng gửi yêu cầu tìm trang bạn bè, server trả về trang banbe.html
  • Người dùng gửi yêu cầu nhắn tin, server trả về dữ liệu tin nhắn

Ở thời kì đầu của web, đa phần các web đều là web tĩnh. Tức là các trang web chỉ có thể hiển thị các nội dung cố định, không thể thay đổi được. Ví dụ như các trang web tin tức, các trang web thông tin, các trang web giới thiệu sản phẩm, …

Tuy nhiên, với sự phát triển của công nghệ, các trang web ngày nay có thể thay đổi được nội dung, có thể tương tác với người dùng. Ví dụ như các trang web mạng xã hội, các trang web thương mại điện tử, các trang web chơi game, … (Được gọi là web động). Các trang web động sẽ có dữ liệu hiển thị tuỳ theo người dùng khác nhau, và có thể thay đổi được dữ liệu hiển thị tuỳ theo người dùng. Ví dụ như trang web mạng xã hội, khi bạn đăng nhập vào trang web, bạn sẽ thấy các bài viết của bạn bè, và bạn có thể đăng bài viết của mình lên trang web.

ảnh minh hoạ 2

Vậy nhìn chung thì Web là một dữ liệu sẽ được server trả về khi người dùng yêu cầu. Tuy nhiên nếu đào sâu ra thì đây là một quá trình rất dài, tưởng tượng máy bạn là nhà bạn, và web bạn truy cập là một cửa hàng trên phố, lúc này để truy cập web, bạn cần đi qua một tuyến đường để đến cửa hàng đó.

Quá trình xảy ra khi bạn kết nối một trang web:

  • Đầu tiên tưởng tượng, để đến một cửa hàng (web), ta cần địa chỉ của nó. Các máy chủ hay máy người dùng khi kết nối đến hệ thống internet toàn cầu đều được cấp một địa chỉ, thường là Ipv4 (ví dụ 98.139.180.149)
  • Tuy nhiên, số lượng ipv4 là có hạn. Vì vậy thông thường các máy tính sẽ nằm trong một mạng cục bộ, và chỉ có một ipv4 bên ngoài quyết định chúng. VD: Toàn bộ máy tính của 1 toà nhà sẽ đều kết nối vào một modem mạng có Ipv4 là (98.139.180.149). Đây sẽ là địa chỉ để bên ngoài có thể kết nối tới. Tuy nhiên khi có yêu cầu gửi tới, ta có thể cài đặt nó sẽ được đi vào máy tính nào trong toà nhà đó, các máy tính lúc này sẽ có một ipv4 cục bộ kiểu: 192.168.0.1, 192.168.0.2, ….. Tưởng tượng: Các cửa hàng nằm trong phố Nguyễn Trãi, ta đi đến Nguyễn Trãi và nó sẽ là số 100 Nguyễn Trãi, số 101 Nguyễn Trãi, …
ảnh minh hoạ 3
  • Như vậy ta đã biết là bằng cách trên. Một server có thể truy cập bằng một địa chỉ Ipv4. Kiểu ta có thể truy cập bằng http://98.139.180.149. Tuy nhiên người dùng không thể nhớ hết số IP này, vì vậy ta cần một hệ thống định dang (tên miền – domain) để biến ip số này thành các tên miền dễ nhớ. VD: http://google.com, http://facebook.com, http://youtube.com, …
  • Để có thể biết được địa chỉ ip của một tên miền, ta cần một hệ thống DNS (Domain Name System). Hệ thống này sẽ lưu trữ các tên miền và địa chỉ ip tương ứng của nó. VD: google.com -> 20.002.003.149
  • Các hệ thống DNS như một cuốn từ điển và cho biến tên miền nào chỉ đến ip nào. Thông thường khi kết nối, đầu tiên máy tính sẽ tìm lại vào cache của chính máy mình để xem tên miền đã được lưu chưa, nếu chưa có thì máy tính sẽ tìm đến các dns server của ISP (nhà mạng, ví dụ Viettel, VNPT). Nếu vẫn chưa tìm thấy, máy tính sẽ tìm đến các DNS Server khác như của Google (8.8.8.8), Cloudflare (1.1.1.1), …
  • Khi người dùng gõ tên miền vào trình duyệt, trình duyệt sẽ gửi yêu cầu tới DNS để lấy địa chỉ ip của tên miền đó. Sau đó trình duyệt sẽ gửi yêu cầu tới địa chỉ ip đó, và server sẽ trả về dữ liệu tương ứng với yêu cầu của người dùng.
ảnh minh hoạ 4

II. Cấu trúc của HTML

ảnh minh hoạ 5
ảnh minh hoạ 6
  • HTML là một ngôn ngữ đánh dấu siêu văn bản (HyperText Markup Language). Nó được sử dụng để xây dựng các trang web. HTML được sử dụng để đánh dấu các phần tử trong một trang web. Các phần tử này được đánh dấu bằng các thẻ HTML. Mỗi thẻ HTML bắt đầu bằng một ký tự đặc biệt và kết thúc bằng một ký tự đặc biệt. Nội dung giữa các thẻ HTML là nội dung của phần tử đó. Ví dụ: <p> This is a paragraph </p>
  • Các thẻ HTML có thể được phân thành hai loại: thẻ đóng và thẻ không đóng. Thẻ đóng có một thẻ đóng tương ứng, trong khi thẻ không đóng không có thẻ đóng tương ứng. Ví dụ: <p> This is a paragraph </p> là một thẻ đóng, trong khi <br> là một thẻ không đóng.

III. Thẻ khối và thẻ nội tuyến

Các thẻ HTML có thể được nhóm thành các thẻ khối và các thẻ nội tuyến. Các thẻ khối bắt đầu trên một dòng mới và chiếm toàn bộ chiều rộng của trình duyệt, trong khi các thẻ nội tuyến không bắt đầu trên một dòng mới và chỉ chiếm một phần của chiều rộng của trình duyệt. Ví dụ: <p> là một thẻ khối, trong khi <b> là một thẻ nội tuyến.

Có 2 kiểu giá trị hiển thị chính : block và inline.

Block-level elements (thẻ khối) :

Một block-elements luôn bắt đầu trên một dòng mới và Browser tự động thêm khoảng trắng (margin) và trước và sau phần tử đó. Nó luôn chiếm toàn bộ chiều rộng có sẵn. 2 block-elements thường dùng là : <p> và <div>.

Inline elements (thẻ nội tuyến) :

Một Inline elements không bắt đầu trên một dòng mới. Nó chỉ chiếm chiều rộng cần thiết. Một phần tử inline không thể chưa phần tử block.

  • <div> là một thẻ khối, nó được sử dụng để nhóm các phần tử HTML lại với nhau. Nó không có bất kỳ ý nghĩa đặc biệt nào. Nó thường được sử dụng để tạo các nhóm phần tử CSS để áp dụng các kiểu CSS cho các phần tử đó. Ví dụ: <div> This is a div </div>
  • <span> là một thẻ nội tuyến, nó được sử dụng để nhóm các phần tử HTML lại với nhau. Nó không có bất kỳ ý nghĩa đặc biệt nào. Nó thường được sử dụng để tạo các nhóm phần tử CSS để áp dụng các kiểu CSS cho các phần tử đó. Ví dụ: <span> This is a span </span>

IV. Các thẻ liên quan đến table trong HTML

  • <table> là một thẻ khối, nó được sử dụng để tạo một bảng. Nó có thể chứa một hoặc nhiều thẻ <tr>. Ví dụ: <table> This is a table </table>
  • <tr> là một thẻ khối, nó được sử dụng để tạo một hàng trong bảng. Nó có thể chứa một hoặc nhiều thẻ <td>. Ví dụ: <tr> This is a table row </tr>
  • <td> là một thẻ nội tuyến, nó được sử dụng để tạo một ô trong bảng. Nó có thể chứa một hoặc nhiều thẻ <p>. Ví dụ: <td> This is a table cell </td>
  • <th> là một thẻ nội tuyến, nó được sử dụng để tạo một ô trong bảng. Nó có thể chứa một hoặc nhiều thẻ <p>. Nó khác với thẻ <td> ở chỗ nó được sử dụng để tạo một ô tiêu đề trong bảng. Ví dụ: <th> This is a table header </th>
ảnh minh hoạ 7

  • <img> là một thẻ nội tuyến, nó được sử dụng để chèn một hình ảnh vào trang web. Nó không có thẻ đóng tương ứng. Ví dụ: <img src="image.jpg" alt="Image">
<img src="url" alt="alternatetext">
  • <video> là một thẻ khối, nó được sử dụng để chèn một video vào trang web. Nó có thể chứa một hoặc nhiều thẻ <source>. Ví dụ: <video> This is a video </video>
  • <source> là một thẻ nội tuyến, nó được sử dụng để chỉ định một nguồn video. Nó không có thẻ đóng tương ứng. Ví dụ: <source src="video.mp4" type="video/mp4">
  • <audio> là một thẻ khối, nó được sử dụng để chèn một âm thanh vào trang web. Nó có thể chứa một hoặc nhiều thẻ <source>. Ví dụ: <audio> This is an audio </audio>
  • <source> là một thẻ nội tuyến, nó được sử dụng để chỉ định một nguồn âm thanh. Nó không có thẻ đóng tương ứng. Ví dụ: <source src="audio.mp3" type="audio/mp3">
  • <a> là một thẻ nội tuyến, nó được sử dụng để tạo một liên kết đến một trang web khác. Nó có thể chứa một hoặc nhiều thẻ <img>. Ví dụ: <a href="https://www.google.com"> This is a link </a>
<a href="url">link text</a>
  • attributes quan trọng nhất của thẻ là “href”, nó cho biết đích đến của link.
  • Target attribute :
    • _self : mở tài liệu cùng cửa sổ khi nhấn link.
    • _blank : mở tài liệu ở cửa sổ mới.
    • _parent : mở tài liệu trong khung chính.
    • _top : mở tài liệu trong toàn bộ cửa sổ.

VI. Section elements: div, span, header, footer, nav, main, section, article

https://www.w3schools.com/html/img_sem_elements.gif
  • <div> là một thẻ khối, nó được sử dụng để nhóm các phần tử HTML lại với nhau. Nó không có bất kỳ ý nghĩa đặc biệt nào. Nó thường được sử dụng để tạo các nhóm phần tử CSS để áp dụng các kiểu CSS cho các phần tử đó. Ví dụ: <div> This is a div </div>
  • <span> là một thẻ nội tuyến, nó được sử dụng để nhóm các phần tử HTML lại với nhau. Nó không có bất kỳ ý nghĩa đặc biệt nào. Nó thường được sử dụng để tạo các nhóm phần tử CSS để áp dụng các kiểu CSS cho các phần tử đó. Ví dụ: <span> This is a span </span>
  • <header> là một thẻ khối, nó được sử dụng để tạo một tiêu đề cho trang web hoặc một phần của trang web. Ví dụ: <header> This is a header </header>
  • <footer> là một thẻ khối, nó được sử dụng để tạo một chân trang cho trang web hoặc một phần của trang web. Ví dụ: <footer> This is a footer </footer>
  • <nav> là một thẻ khối, nó được sử dụng để tạo một thanh điều hướng cho trang web hoặc một phần của trang web. Ví dụ: <nav> This is a nav </nav>
  • <main> là một thẻ khối, nó được sử dụng để tạo một phần chính của trang web. Ví dụ: <main> This is a main </main>
  • <section> là một thẻ khối, nó được sử dụng để tạo một phần của trang web. Ví dụ: <section> This is a section </section>
  • <article> là một thẻ khối, nó được sử dụng để tạo một bài viết. Ví dụ: <article> This is an article </article>

VII. HTML text fundamentals: h1 -> h6, p, ul, li

ảnh minh hoạ 8
  • <h1> là một thẻ khối, nó được sử dụng để tạo một tiêu đề lớn nhất cho trang web hoặc một phần của trang web. Ví dụ: <h1> This is a heading </h1>
  • <h2> là một thẻ khối, nó được sử dụng để tạo một tiêu đề lớn hơn <h1> cho trang web hoặc một phần của trang web. Ví dụ: <h2> This is a heading </h2>
  • <p> là một thẻ khối, nó được sử dụng để tạo một đoạn văn bản. Ví dụ: <p> This is a paragraph </p>
  • <ul> là một thẻ khối, nó được sử dụng để tạo một danh sách không có thứ tự. Nó có thể chứa một hoặc nhiều thẻ <li>. Ví dụ: <ul> This is an unordered list </ul>
  • <li> là một thẻ khối, nó được sử dụng để tạo một mục trong danh sách. Nó có thể chứa một hoặc nhiều thẻ <p>. Ví dụ: <li> This is a list item </li>
  • <ol> là một thẻ khối, nó được sử dụng để tạo một danh sách có thứ tự. Nó có thể chứa một hoặc nhiều thẻ <li>. Ví dụ: <ol> This is an ordered list </ol>
ảnh minh hoạ 9

VIII. Các thẻ liên quan đến form: form, input, button, label, textarea

  • <form> là một thẻ khối, nó được sử dụng để tạo một biểu mẫu. Nó có thể chứa một hoặc nhiều thẻ <input><button><label><textarea>. Ví dụ: <form> This is a form </form>
  • <input> là một thẻ nội tuyến, nó được sử dụng để tạo một trường nhập liệu. Nó không có thẻ đóng tương ứng. Ví dụ: <input type="text">
  • <button> là một thẻ nội tuyến, nó được sử dụng để tạo một nút. Nó có thể chứa một hoặc nhiều thẻ <img>. Ví dụ: <button> This is a button </button>
  • <label> là một thẻ nội tuyến, nó được sử dụng để tạo một nhãn cho một trường nhập liệu. Nó có thể chứa một hoặc nhiều thẻ <input>. Ví dụ: <label> This is a label </label>
  • <textarea> là một thẻ khối, nó được sử dụng để tạo một trường nhập liệu dài. Nó không có thẻ đóng tương ứng. Ví dụ: <textarea> This is a textarea </textarea>
  • <select> là một thẻ khối, nó được sử dụng để tạo một trường nhập liệu dạng danh sách thả xuống. Nó có thể chứa một hoặc nhiều thẻ <option>. Ví dụ: <select> This is a select </select>
  • <option> là một thẻ khối, nó được sử dụng để tạo một mục trong danh sách thả xuống. Nó không có thẻ đóng tương ứng. Ví dụ: <option> This is an option </option>
ảnh minh hoạ 11

IX. Có thể tìm hiểu các thẻ khác ngoài các thẻ trên các thẻ trên

  • <br> : Xuống dòng
  • <hr> : Thẻ ngang
  • <i> : In nghiêng
  • <b> : In đậm

Cách up ảnh lên Facebook giữ được chất lượng tốt nhất

Facebook là một mạng xã hội phổ biến bậc nhất tại Việt Nam. Mọi người thường xuyên chia sẻ ảnh lên Facebook như một thói quen hàng ngày. Tuy nhiên có một vấn đề mà ai cũng thấy khó chịu là ảnh tải lên Facebook thường bị giảm chất lượng thấy rõ.

Bài viết này sẽ hướng dẫn cách tối ưu nhất có thể việc bị giảm chất lượng ảnh khi tải lên Facebook.

Tại sao Facebook lại giảm chất lượng ảnh tải lên?

Vì lượng ảnh tải lên Facebook từng phút từng giây có dung lượng rất lớn. Với ảnh có chất lượng giảm đi và dung lượng nhẹ hơn, Facebook sẽ cho người dùng tốc độ khi load các bài viết trên ứng dụng/web nhanh hơn, đồng thời giảm bớt chi phí lưu trữ lượng lớn ảnh như vậy. Việc nén ảnh này được thực hiện tự động khi bạn up ảnh lên.

Ảnh tải lên Facebook được hỗ trợ có chiều ngang cạnh phổ thông là 720px, 960px hoặc 2048px. Các ảnh có kích thước khác đều bị co lại theo kích thước tiêu chuẩn, điều này làm chất lượng ảnh giảm rõ rệt.

Giảm Facebook nén ảnh với file ảnh JPEG/JPG

Mỗi khi bạn tải ảnh lên, Facebook mặc định sẽ nén ảnh của bạn, nên không có cách nào chống được điều đó cả. Tuy nhiên có vài cách để giảm bớt việc nén ảnh đó

Đảm bảo ảnh bạn tải lên có cạnh dài lớn nhất là 720, 960 hoặc 2048px. Facebook sẽ không co kích thước ảnh với các kích cỡ ảnh này, việc này giúp ảnh của bạn sắc nét hơn nhiều khi tải lên.

Ví dụ sử dụng phần mềm Adobe Photoshop 2022

  1. Sử dụng chức năng File/ Export/ Save For Web (Legacy)
  2. Chỉnh các thông số như sau: (Cạnh nhiều nhất: 720/960/2048, PNG-24, Embed Color Profile, Convert to sRGB
  3. Ấn Save và kiểm tra thành quả!
image - quochung.cyou PTIT

So sánh kết quả

image 1 - quochung.cyou PTIT

Tham khảo: https://www.naturettl.com/upload-photos-facebook-best-quality-possible/

Cách cài đặt C++, MingW cho Visual Studio Code (C++ 20)

Video hướng dẫn

Video hướng dẫn

Hướng dẫn

Các bước thực hiện

  1. Cài Visual Studio Code.
  2. Tải extension C/C++ extension for VS Code. hỗ trợ C/C++. Hoặc bạn có thể tự vào mục Extension bằng Ctrl+Shift+X rồi tìm C++ C/C++ extension
  3. Tải Mingw để có thể compile code https://github.com/brechtsanders/winlibs_mingw/releases/, thông thường các máy sẽ dùng file có dạng “winlibs-x86_64-posix-seh-gcc-12.1.0-mingw-w64msvcrt-10.0.0-r3.zip” (winlibsx86_64…… ) (đuôi .zip)
  4. Bạn giải nén thư mục được 1 folder mingw, để folder này vào đâu đó trong ổ C. Trong folder mingw sẽ có một thư mục con tên “bin” , hãy nhớ thư mục này
  5. Dùng thanh tìm kiếm của Window, tìm chữ “path”, chọn lựa chọn Edit environment variables for your account.
    1. Chọn Path trong User variables và click Edit.
    2. Chọn New, bây giờ hãy paste đường dẫn vào folder mingw/bin bên trên (dẫn đến thư mục con bin) VD: C:\msys64\mingw64\bin
    3. Chọn OK và tắt tất cả cửa sổ cài đặt

Kiểm tra cài đặt thành công chưa

Mở CMD/Powershell của Window lên, gõ lệnh

g++ --version

Nếu trả về là phiên bản của GCC có dạng GCC 4.7.2 …. thì bạn đã cài thành công