Bộ câu hỏi phỏng vấn Java phần 10

Tại sao ArrayList lại được ưa chuộng để sử dụng trong nhiều trường hợp hơn LinkedList?


LinkedList hầu như luôn luôn có một lỗi (về hiệu suất):

  • Nó sử dụng rất nhiều đối tượng bộ nhớ nhỏ và do đó ảnh hưởng đến hiệu suất trong toàn bộ process.
  • Rất nhiều đối tượng nhỏ có hại cho bộ nhớ cache.
  • Bất kỳ hoạt động được lập chỉ mục nào đều yêu cầu chuyển tải, tức là có hiệu suất O (n). Điều này không rõ ràng trong mã nguồn, dẫn đến các thuật toán O (n) chậm hơn so với khi - sử dụng ArrayList.
  • Để đạt được hiệu suất tốt là điều khó khăn.
  • Ngay cả khi hiệu suất của big-O giống như ArrayList, nó có thể sẽ chậm hơn đáng kể.
  • Thật khó chịu khi xem LinkedList trong nguồn vì nó có thể là sự lựa chọn sai lầm.

DGC là gì? Và nó hoạt động như thế nào?


DGC là viết tắt của Distributed Garbage Collection. Remote Method Invocation (RMI) sử dụng DGC để thu gom rác tự động.

Vì RMI liên quan đến các tham chiếu đối tượng từ xa trên JVM, nên việc thu thập rác có thể khá khó khăn. DGC sử dụng thuật toán đếm tham chiếu để cung cấp khả năng quản lý bộ nhớ tự động cho các đối tượng từ xa.

So sánh biến volatile và biến static trong Java?


Khai báo một biến static trong Java, có nghĩa là sẽ chỉ có một bản sao, bất kể có bao nhiêu đối tượng của lớp được tạo. Biến sẽ có thể truy cập được ngay cả khi không có đối tượng nào được tạo ra. Tuy nhiên, các thread có thể có các giá trị được lưu trong bộ nhớ cache cục bộ của nó.

Khi một biến là volatile và non-static, sẽ có một biến cho mỗi đối tượng. Vì vậy, bề ngoài có vẻ như không có sự khác biệt so với một biến bình thường nhưng hoàn toàn khác với static. Tuy nhiên, ngay cả với các thuộc tính của đối tượng, một thread có thể lưu vào bộ nhớ cache một giá trị biến cục bộ.

Điều này có nghĩa là nếu hai thread cập nhật đồng thời một biến của cùng một đối tượng và biến đó không được khai báo là biến volatile, có thể xảy ra trường hợp một trong các thread có giá trị cũ trong bộ nhớ cache.

Ngay cả khi bạn truy cập một giá trị static thông qua nhiều thread, mỗi thread có thể có bản sao được lưu trong bộ nhớ cache cục bộ của nó! Để tránh điều này, bạn có thể khai báo biến là static volatile và điều này sẽ buộc thread đọc giá trị toàn cục mỗi lần.

Sự khác biệt giữa Soft reference và Weak reference trong Java là gì?


  • Strong reference là tham chiếu bình thường bảo vệ đối tượng được tham chiếu khỏi bị GC thu thập, tức là không bao giờ bị thu gom rác.
  • Soft reference đủ điều kiện để thu thập bởi bộ thu gom rác, nhưng có thể sẽ không được thu thập cho đến khi bộ nhớ của nó cần, tức là rác được thu thập trước OutOfMemoryError.
  • Weak reference là tham chiếu không bảo vệ đối tượng được tham chiếu khỏi GC thu thập, tức là rác được thu thập khi không có Strong reference hoặc Soft reference.
  • Phantom reference là một tham chiếu đến một đối tượng được tham chiếu ảo sau khi nó đã được hoàn thành, nhưng trước khi bộ nhớ được cấp phát của nó được lấy lại.

Việc thu gom rác có diễn ra trong PermGen space trong JVM không?


Thu gom rác thực hiện trong PermGen space và nếu PermGen space đầy hoặc vượt qua một ngưỡng, nó có thể kích hoạt một bộ thu gom rác đầy đủ. Nếu bạn xem xét kỹ đầu ra của trình thu gom rác (garbage collector), bạn sẽ thấy rằng PermGen space cũng được thu gom rác. Đây là lý do tại sao việc đo chính xác kích thước của PermGen space là quan trọng để tránh việc thường xuyên thu gom rác đầy đủ.

Các layer của kiến trúc Remote Method Invocation (RMI) là gì?


Kiến trúc RMI bao gồm các layer sau:

  • Stub and Skeleton layer: layer này nằm ngay bên dưới chế độ view của nhà phát triển. Layer này chịu trách nhiệm chặn các cuộc gọi phương thức do client thực hiện đến giao diện và chuyển hướng các cuộc gọi này đến một Remote RMI Service.
  • Remote Reference layer: layer thứ hai của kiến trúc RMI xử lý việc diễn giải các tham chiếu được tạo từ client đến các đối tượng từ xa (remote object) của server. Layer này thông dịch và quản lý các tham chiếu được tạo từ client đến các đối tượng dịch vụ từ xa (remote service object). Kết nối này là liên kết một-một (unicast).
  • Transport layer: layer này có nhiệm vụ kết nối hai JVM tham gia vào dịch vụ. Lớp này dựa trên các kết nối TCP / IP giữa các máy trong mạng. Nó cung cấp kết nối cơ bản, cũng như một số chiến lược thâm nhập tường lửa (firewall).

Làm thế nào để việc đồng bộ hóa thread xảy ra bên trong một monitor?


JVM sử dụng khóa kết hợp với monitor. Một monitor về cơ bản là người giám sát theo dõi một chuỗi mã được đồng bộ hóa và đảm bảo rằng mỗi lần chỉ có một thread thực thi một đoạn mã được đồng bộ hóa. Mỗi monitor được liên kết với một tham chiếu đối tượng. Thread không được phép thực thi mã cho đến khi nó nhận được khóa.

Synchronized có nghĩa là gì?


Từ khóa synchronized là tất cả về việc đọc và ghi các thread khác nhau vào các biến, đối tượng và tài nguyên giống nhau. Từ khóa synchronized là một trong những công cụ giúp chuỗi mã của bạn an toàn.

Các phương thức synchronized thiết lập một chiến lược đơn giản để ngăn chặn sự can thiệp của thread và lỗi đồng nhất bộ nhớ: nếu một đối tượng hiển thị cho nhiều hơn một thread, thì tất cả việc đọc hoặc ghi vào các biến của đối tượng đó được thực hiện thông qua các phương thức synchronized. Các phương thức synchronized không thể được gọi cùng một lúc từ nhiều thread.

Vì vậy, nói một cách đơn giản khi bạn có hai thread đang đọc và ghi vào cùng một 'tài nguyên', chẳng hạn như một biến có tên foo, bạn cần đảm bảo rằng các thread này truy cập biến theo cách nguyên tử (atomic way). Nếu không có từ khóa synchronized, thread 1 của bạn có thể không nhìn thấy thay đổi mà thread 2 thực hiện lên foo, hoặc tệ hơn, nó có thể chỉ được thay đổi một nửa. Đây sẽ không phải là những gì mà bạn mong đợi.

Có gì sai với việc khởi tạo Double Brace trong Java?


Sử dụng khởi tạo Double Brace không phải là lý tưởng vì:

  1. Bạn đang tạo quá nhiều lớp ẩn danh. Ví dụ:
Map source = new HashMap(){{
   put("firstName", "John");
   put("lastName", "Smith");
   put("organizations", new HashMap(){{
      put("0", new HashMap(){{
         put("id", "1234");
      }});
      put("abc", new HashMap(){{
         put("id", "5678");
      }});
   }});
}};

... sẽ tạo ra các lớp sau:

Test$1$1$1.class
Test$1$1$2.class
Test$1$1.class
Test$1.class
Test.class

Khá nhiều chi phí cho classloader (trình tải lớp) của bạn.

  1. Bạn có khả năng tạo ra một memory leak (rò rỉ bộ nhớ). Nếu bạn lấy đoạn mã trên và trả về map đó từ một phương thức, các caller của phương thức đó có thể giữ các tài nguyên rất nặng mà không thể thu thập được.

Sự khác biệt giữa HashSet và TreeSet là gì?


  • HashSet được triển khai bằng cách sử dụng một bảng hash và do đó, các phần tử của nó không được sắp xếp theo thứ tự. Các phương thức add, remove và contains của một HashSet có độ phức tạp thời gian không đổi O(1).
  • Mặt khác, một TreeSet được thực hiện bằng cách sử dụng cấu trúc cây. Các phần tử trong TreeSet được sắp xếp và do đó, các phương thức add, remove và contains có độ phức tạp về thời gian là O(logn).

Hãy nêu một cách hiệu quả để triển khai một mẫu singleton trong Java?


java5 sử dụng một enum:

public enum Elvis {
   INSTANCE;

   private final String[] favoriteSongs =
      { "Hound Dog", "Heartbreak Hotel" };
   public void printFavorites() {
      System.out.println(Arrays.toString(favoriteSongs));
   }
}

Cách tiếp cận này tương đương về mặt chức năng với cách tiếp cận public field, ngoại trừ nó ngắn gọn hơn, nó cung cấp miễn phí bộ máy tuần tự hóa và cung cấp sự đảm bảo vững chắc để chống lại việc khởi tạo nhiều lần, ngay cả khi đối mặt với các cuộc tấn công tuần tự hóa tinh vi hoặc reflection. Mặc dù cách tiếp cận này vẫn chưa được áp dụng rộng rãi, nhưng kiểu enum một phần tử là cách tốt nhất để triển khai một singleton.

Trước java5, trường hợp đơn giản nhất là:

public final class Foo {
   private static final Foo INSTANCE = new Foo();

   private Foo() {
      if (INSTANCE != null) {
         throw new IllegalStateException("Already instantiated");
      }
   }

   public static Foo getInstance() {
      return INSTANCE;
   }

   public Object clone() throws CloneNotSupportedException{
      throw new CloneNotSupportedException("Cannot clone instance of this class");
   }
}

Khi nào khối finally không được thực thi trong Java?


Thông thường, finally sẽ được gọi sau khi thực hiện các khối mã try hoặc catch. Các lần finally không được gọi là:

  • Nếu bạn gọi System.exit().
  • Nếu JVM gặp sự cố trước.
  • Nếu JVM đạt đến một vòng lặp vô hạn (hoặc một số câu lệnh không ngừng, hoặc không kết thúc khác) trong khối try hoặc catch.
  • Nếu hệ điều hành buộc phải chấm dứt JVM process; ví dụ: kill -9 <pid> trên UNIX.
  • Nếu hệ thống máy chủ bị chết; ví dụ: mất điện, lỗi phần cứng, hệ điều hành bị lỗi, ...
  • Nếu khối finally được thực thi bởi một thread daemon và tất cả các thread non-daemon khác kết thúc trước khi finally được gọi.

Tại sao length() của String không chính xác?


Nó không chính xác vì nó sẽ chỉ tính đến số ký tự trong String. Nói cách khác, nó sẽ không tính đến các điểm mã (code points) bên ngoài cái được gọi là BMP (Basic Multilingual Plane - Mặt phẳng đa ngôn ngữ cơ bản), tức là các điểm mã có giá trị U + 10000 hoặc lớn hơn.

Lý do là lịch sử: khi Java lần đầu tiên được định nghĩa, một trong những mục tiêu của nó là coi tất cả văn bản là Unicode; nhưng tại thời điểm này, Unicode không xác định các điểm mã bên ngoài BMP. Vào thời điểm Unicode xác định các điểm mã như vậy, đã quá muộn để thay đổi ký tự.

Cách chính xác để đếm số ký tự thực trong một String, tức là số điểm mã, là:

someString.codePointCount(0, someString.length())
// or, with Java 8:
someString.codePoints().count()
Avatar Techmely Team
VIẾT BỞI

Techmely Team