2011-12-15 62 views
29

Tôi đã có một tranh chấp nhỏ về hiệu suất của khối đồng bộ trong Java. Đây là câu hỏi lý thuyết, không ảnh hưởng đến ứng dụng thực tế. Xem xét ứng dụng đơn luồng, sử dụng khóa và đồng bộ hóa các phần. Mã này có hoạt động chậm hơn mã giống nhau mà không đồng bộ hóa các phần không? Nếu vậy, tại sao? Chúng tôi không thảo luận về đồng thời, vì nó chỉ đơn ứng dụng chủ đềHiệu suất của phần đồng bộ hóa trong Java

UPD

Tìm thấy thú vị benchmark thử nghiệm nó. Nhưng đó là từ năm 2001. Mọi thứ có thể đã thay đổi đáng kể trong phiên bản mới nhất của JDK

+0

đẹp như bài viết đó là, mọi thứ đã phát triển rất nhiều ** ** vì nó đã được viết cách đây mười năm. – NPE

+0

câu trả lời ngắn gọn! – bestsss

+0

Câu trả lời dài: có. JVM sẽ luôn luôn cần phải giải quyết xem khóa của đối tượng có sẵn hay không, bất kể Java đã phát triển như thế nào. –

Trả lời

27

Có 3 loại khóa trong HotSpot

  1. Fat: JVM dựa trên mutexes OS để có được khóa.
  2. Thin: JVM đang sử dụng thuật toán CAS.
  3. Biased: CAS hoạt động khá tốn kém trên một số kiến ​​trúc. Khóa thiên vị - là loại khóa đặc biệt được tối ưu hóa cho kịch bản khi chỉ có một luồng đang hoạt động trên đối tượng.

Theo mặc định JVM sử dụng khóa mỏng. Sau đó nếu JVM xác định rằng không có khóa mỏng ganh đua được chuyển đổi thành thiên vị khóa. Hoạt động thay đổi kiểu khóa là khá tốn kém, do đó JVM không áp dụng tối ưu hóa này ngay lập tức. Có tùy chọn JVM đặc biệt - XX: BiasedLockingStartupDelay = delay cho JVM biết khi áp dụng loại tối ưu hóa này.

Sau khi thiên vị, luồng đó sau đó có thể khóa và mở khóa đối tượng mà không cần phải hướng dẫn nguyên tử đắt tiền.

Trả lời cho câu hỏi: tùy thuộc. Nhưng nếu thiên vị, mã đơn luồng có khóa và không khóa có hiệu suất giống nhau trung bình.

+4

Rất nhiều thông tin . Tuy nhiên, bạn có thể chỉ ra phiên bản nào của Java/VM câu trả lời này đã được viết không? –

17

Có một số chi phí trong việc mua một khóa không bị tranh cãi, nhưng trên các JVM hiện đại thì rất nhỏ.

Tối ưu hóa thời gian chạy chính liên quan đến trường hợp này được gọi là "Khóa thiên vị" và được giải thích trong Java SE 6 Performance White Paper.

Nếu bạn muốn có một số hiệu suất có liên quan đến JVM và phần cứng của mình, bạn có thể xây dựng một điểm chuẩn nhỏ để thử và đo lường chi phí này.

+5

Tôi đã thử nghiệm điều này. Nó quá nhỏ đến nỗi bạn không thể đo được hiệu ứng. Họ nói rằng hiệu ứng này quan trọng hơn nhiều đối với các phiên bản JVM cũ hơn. – AlexR

+0

@ AlexR: Rất tốt, cảm ơn bạn đã chia sẻ. Nó không làm tôi ngạc nhiên rằng hiệu ứng được sử dụng là quan trọng hơn, vì tối ưu hóa Khóa thiên vị chỉ được thêm vào trong Java 6. – NPE

+4

* quá nhỏ đến nỗi bạn không thể đo lường hiệu ứng này * yêu cầu đó không thể được thực hiện một cách nhẹ nhàng. khi kiểm tra điều gì đó trong một vòng lặp chặt chẽ, JVM có thể làm những phép thuật vĩ đại. nhưng điều đó không đại diện cho ứng dụng "thế giới thực". JVM được ngu ngốc thực sự nhanh chóng khi thực hiện trở nên phức tạp. – irreputable

-1

Giả sử bạn đang sử dụng máy ảo HotSpot, tôi tin rằng JVM có thể nhận ra rằng không có tranh chấp cho bất kỳ tài nguyên nào trong khối synchronized và coi đó là mã "bình thường".

+3

Trích dẫn, vui lòng. Tôi không nghĩ rằng JVM có thể loại bỏ các mục giám sát và thoát hoàn toàn. – erickson

+0

Tôi cũng đã đọc ở đâu đó. Nếu trình biên dịch Hotspot chắc chắn rằng mã chỉ có thể truy cập từ một luồng, nó sẽ bỏ qua hoàn toàn việc đồng bộ hóa.Tôi không hoàn toàn chắc chắn về "chắc chắn ..." một phần mặc dù và tôi đã không bao giờ thực sự quản lý để có được VM để làm điều đó. Ngay cả trong một ứng dụng đơn luồng, chi phí đồng bộ hóa cũng không được đánh giá thấp. – jarnbjo

+0

Không chắc chắn rằng JVM có thể thực hiện tối ưu hóa này – Anton

8

Sử dụng khóa khi bạn không cần làm chậm ứng dụng của mình. Nó có thể là quá nhỏ để đo lường hoặc nó có thể cao đáng ngạc nhiên.

IMHO Thường thì cách tiếp cận tốt nhất là sử dụng mã khóa miễn phí trong một chương trình luồng đơn để làm cho nó rõ ràng mã này không có ý định chia sẻ qua chuỗi. Điều này có thể quan trọng hơn để bảo trì hơn bất kỳ vấn đề hiệu suất nào.

public static void main(String... args) throws IOException { 
    for (int i = 0; i < 3; i++) { 
     perfTest(new Vector<Integer>()); 
     perfTest(new ArrayList<Integer>()); 
    } 
} 

private static void perfTest(List<Integer> objects) { 
    long start = System.nanoTime(); 
    final int runs = 100000000; 
    for (int i = 0; i < runs; i += 20) { 
     // add items. 
     for (int j = 0; j < 20; j+=2) 
      objects.add(i); 
     // remove from the end. 
     while (!objects.isEmpty()) 
      objects.remove(objects.size() - 1); 
    } 
    long time = System.nanoTime() - start; 
    System.out.printf("%s each add/remove took an average of %.1f ns%n", objects.getClass().getSimpleName(), (double) time/runs); 
} 

in

Vector each add/remove took an average of 38.9 ns 
ArrayList each add/remove took an average of 6.4 ns 
Vector each add/remove took an average of 10.5 ns 
ArrayList each add/remove took an average of 6.2 ns 
Vector each add/remove took an average of 10.4 ns 
ArrayList each add/remove took an average of 5.7 ns 

Từ quan điểm thực hiện xem, nếu 4 ns là quan trọng với bạn, bạn phải sử dụng phiên bản không đồng bộ.

Đối với 99% trường hợp sử dụng, độ rõ của mã quan trọng hơn hiệu suất. Rõ ràng, mã đơn giản thường thực hiện tốt một cách hợp lý.

BTW: Tôi đang sử dụng 4,6 GHz i7 2600 với Oracle Java 7u1.


Để so sánh nếu tôi làm như sau trong đó perfTest1,2,3 là giống nhau.

perfTest1(new ArrayList<Integer>()); 
    perfTest2(new Vector<Integer>()); 
    perfTest3(Collections.synchronizedList(new ArrayList<Integer>())); 

tôi nhận được

ArrayList each add/remove took an average of 2.6 ns 
Vector each add/remove took an average of 7.5 ns 
SynchronizedRandomAccessList each add/remove took an average of 8.9 ns 

Nếu tôi sử dụng một perfTest phương pháp phổ biến nó không thể inline mã như một cách tối ưu và tất cả chúng đều chậm

ArrayList each add/remove took an average of 9.3 ns 
Vector each add/remove took an average of 12.4 ns 
SynchronizedRandomAccessList each add/remove took an average of 13.9 ns 

Trao đổi thứ tự của các bài kiểm tra

ArrayList each add/remove took an average of 3.0 ns 
Vector each add/remove took an average of 39.7 ns 
ArrayList each add/remove took an average of 2.0 ns 
Vector each add/remove took an average of 4.6 ns 
ArrayList each add/remove took an average of 2.3 ns 
Vector each add/remove took an average of 4.5 ns 
ArrayList each add/remove took an average of 2.3 ns 
Vector each add/remove took an average of 4.4 ns 
ArrayList each add/remove took an average of 2.4 ns 
Vector each add/remove took an average of 4.6 ns 

cùng một lúc

ArrayList each add/remove took an average of 3.0 ns 
ArrayList each add/remove took an average of 3.0 ns 
ArrayList each add/remove took an average of 2.3 ns 
ArrayList each add/remove took an average of 2.2 ns 
ArrayList each add/remove took an average of 2.4 ns 

Vector each add/remove took an average of 28.4 ns 
Vector each add/remove took an average of 37.4 ns 
Vector each add/remove took an average of 7.6 ns 
Vector each add/remove took an average of 7.6 ns 
Vector each add/remove took an average of 7.6 ns 
+0

Tôi đã thử nghiệm nó trên một JDK của IBM và ngoại trừ chạy Vector và ArrayList đầu tiên có khoảng 10% hiệu suất khác biệt trên máy của tôi (54ns so với 48-50ns). Tôi cũng đã thử nghiệm nó với Collections.synchronizedList và đã rất ngạc nhiên bởi hiệu quả xấu của nó. Tốc độ chậm gấp đôi so với Vector/ArrayList (110ns). – Stefan

+0

Đây là một lý do khác để quan tâm đến vi điều chỉnh. Sử dụng một hệ thống, phần cứng khác, JVM có thể cung cấp cho bạn một kết quả khác. –

+0

btw, mã như thế được tối ưu hóa lần đầu tiên cho Vector, sau đó được deoptimized và tối ưu hóa một lần nữa, kể từ khi thay đổi mục tiêu cuộc gọi (Danh sách ). Vì không thể chắc chắn về việc khử địa chỉ thích hợp (có thể chỉ được bảo vệ gọi tới vector + trap) trường hợp ArrayList sẽ bị ảnh hưởng. Bạn có thể trao đổi thử nghiệm không, ví dụ: ArrayList rồi Vector. Chủ yếu là tò mò. OTOH trường hợp là thử nghiệm khóa thiên vị hoàn hảo đẫm máu quá. Ngoài ra CAS là khá rẻ trên CPU của bạn, trên kiến ​​trúc cũ hơn CAS là một cuộc gọi khá đắt tiền (nếu khóa thiên vị bị tắt) – bestsss

42

Single-ren mã vẫn sẽ chạy chậm hơn khi sử dụng synchronized khối. Rõ ràng bạn sẽ không có chủ đề khác bị trì hoãn trong khi chờ đợi cho các chủ đề khác để kết thúc, tuy nhiên bạn sẽ phải đối phó với các hiệu ứng khác của đồng bộ hóa, cụ thể là bộ nhớ cache coherency.

khối đồng bộ không chỉ được sử dụng cho đồng thời, mà còn tầm nhìn. Mỗi khối đồng bộ là một rào cản bộ nhớ: JVM được tự do làm việc trên các biến trong thanh ghi, thay vì bộ nhớ chính, dựa trên giả định rằng nhiều luồng sẽ không truy cập biến đó. Nếu không có khối đồng bộ hóa, dữ liệu này có thể được lưu trữ trong bộ nhớ cache của CPU và các luồng khác nhau trên các CPU khác nhau sẽ không thấy cùng một dữ liệu. Bằng cách sử dụng khối đồng bộ hóa, bạn buộc JVM ghi dữ liệu này vào bộ nhớ chính để hiển thị các luồng khác.

Vì vậy, ngay cả khi bạn rảnh rỗi khỏi tranh chấp khóa, JVM sẽ vẫn phải thực hiện dịch vụ vệ sinh trong việc xả dữ liệu vào bộ nhớ chính.

Ngoài ra, điều này có các ràng buộc tối ưu hóa. JVM là miễn phí để sắp xếp lại các hướng dẫn để cung cấp tối ưu hóa: hãy xem xét một ví dụ đơn giản:

foo++; 
bar++; 

so:

foo++; 
synchronized(obj) 
{ 
    bar++; 
} 

Trong ví dụ đầu tiên, trình biên dịch là miễn phí để tải foobar tại cùng một lúc, sau đó tăng cả hai, sau đó lưu cả hai. Trong ví dụ thứ hai, trình biên dịch phải thực hiện tải/thêm/lưu trên foo, sau đó thực hiện tải/thêm/lưu trên bar. Do đó, việc đồng bộ hóa có thể ảnh hưởng đến khả năng của JRE để tối ưu hóa các hướng dẫn.

(Một cuốn sách tuyệt vời trên Memory Model Java là Brian Goetz của Java Concurrency In Practice.)

0

này mẫu mã (100 bài làm 1.000.000 lặp mỗi người) cho thấy sự khác biệt về hiệu năng giữa tránh và không tránh khối đồng bộ.

Output:

Total time(Avoid Sync Block): 630ms 
Total time(NOT Avoid Sync Block): 6360ms 
Total time(Avoid Sync Block): 427ms 
Total time(NOT Avoid Sync Block): 6636ms 
Total time(Avoid Sync Block): 481ms 
Total time(NOT Avoid Sync Block): 5882ms 

Code:

import org.apache.commons.lang.time.StopWatch; 

public class App { 
    public static int countTheads = 100; 
    public static int loopsPerThead = 1000000; 
    public static int sleepOfFirst = 10; 

    public static int runningCount = 0; 
    public static Boolean flagSync = null; 

    public static void main(String[] args) 
    {   
     for (int j = 0; j < 3; j++) {  
      App.startAll(new App.AvoidSyncBlockRunner(), "(Avoid Sync Block)"); 
      App.startAll(new App.NotAvoidSyncBlockRunner(), "(NOT Avoid Sync Block)"); 
     } 
    } 

    public static void startAll(Runnable runnable, String description) { 
     App.runningCount = 0; 
     App.flagSync = null; 
     Thread[] threads = new Thread[App.countTheads]; 

     StopWatch sw = new StopWatch(); 
     sw.start(); 
     for (int i = 0; i < threads.length; i++) { 
      threads[i] = new Thread(runnable); 
     } 
     for (int i = 0; i < threads.length; i++) { 
      threads[i].start(); 
     } 
     do { 
      try { 
       Thread.sleep(10); 
      } catch (InterruptedException e) { 
       e.printStackTrace(); 
      } 
     } while (runningCount != 0); 
     System.out.println("Total time"+description+": " + (sw.getTime() - App.sleepOfFirst) + "ms"); 
    } 

    public static void commonBlock() { 
     String a = "foo"; 
     a += "Baa"; 
    } 

    public static synchronized void incrementCountRunning(int inc) { 
     runningCount = runningCount + inc; 
    } 

    public static class NotAvoidSyncBlockRunner implements Runnable { 

     public void run() { 
      App.incrementCountRunning(1); 
      for (int i = 0; i < App.loopsPerThead; i++) { 
       synchronized (App.class) { 
        if (App.flagSync == null) { 
         try { 
          Thread.sleep(App.sleepOfFirst); 
         } catch (InterruptedException e) { 
          e.printStackTrace(); 
         } 
         App.flagSync = true; 
        } 
       } 
       App.commonBlock(); 
      } 
      App.incrementCountRunning(-1); 
     } 
    } 

    public static class AvoidSyncBlockRunner implements Runnable { 

     public void run() { 
      App.incrementCountRunning(1); 
      for (int i = 0; i < App.loopsPerThead; i++) { 
       // THIS "IF" MAY SEEM POINTLESS, BUT IT AVOIDS THE NEXT 
       //ITERATION OF ENTERING INTO THE SYNCHRONIZED BLOCK 
       if (App.flagSync == null) { 
        synchronized (App.class) { 
         if (App.flagSync == null) { 
          try { 
           Thread.sleep(App.sleepOfFirst); 
          } catch (InterruptedException e) { 
           e.printStackTrace(); 
          } 
          App.flagSync = true; 
         } 
        } 
       } 
       App.commonBlock(); 
      } 
      App.incrementCountRunning(-1); 
     } 
    } 
} 
Các vấn đề liên quan