2012-01-22 11 views
9

"Java Concurrency in Practice" cung cấp ví dụ sau về một lớp không an toàn do bản chất của mô hình bộ nhớ java có thể kết thúc chạy mãi mãi hoặc in 0.Đồng bộ hóa mô hình bộ nhớ Java: cách tạo lỗi hiển thị dữ liệu?

Vấn đề lớp này đang cố gắng chứng minh là các biến ở đây không được "chia sẻ" giữa các chủ đề. Vì vậy, giá trị trên thread thấy có thể khác với một chuỗi khác vì chúng không dễ bay hơi hoặc được đồng bộ hóa. Cũng do sắp xếp lại các câu lệnh cho phép bởi JVM ready = true có thể được đặt trước số = 42.

Đối với tôi, lớp này luôn hoạt động tốt bằng JVM 1.6. Bất kỳ ý tưởng nào về cách để lớp này thực hiện hành vi không chính xác (ví dụ: in 0 hoặc chạy vĩnh viễn)?

public class NoVisibility { 
    private static boolean ready; 
    private static int number; 

    private static class ReaderThread extends Thread { 
     public void run() { 
      while (!ready) 
       Thread.yield(); 
      System.out.println(number); 
     } 
    } 

    public static void main(String[] args) { 
     new ReaderThread().start(); 
     number = 42; 
     ready = true; 
    } 
} 
+4

Câu trả lời ngắn không. Nhưng bạn có thực sự cần phải kiểm tra xem nhảy từ mái nhà là nguy hiểm sau khi ai đó nói với bạn và đưa ra lý do tốt? – Voo

+1

Sử dụng máy tính có 2 lõi. –

+1

@Voo ... bạn đang nói cái quái gì vậy? –

Trả lời

6

Sự cố bạn gặp phải là bạn không đợi đủ lâu để mã được tối ưu hóa và giá trị được lưu vào bộ nhớ cache.

Khi chuỗi trên hệ thống x86_64 lần đọc giá trị lần đầu tiên, nó sẽ nhận được bản sao an toàn chủ đề. Chỉ những thay đổi sau này của nó mới có thể không nhìn thấy được. Điều này có thể không phải là trường hợp trên các CPU khác.

Nếu bạn thử điều này, bạn có thể thấy rằng mỗi luồng bị mắc kẹt với giá trị cục bộ của nó.

public class RequiresVolatileMain { 
    static volatile boolean value; 

    public static void main(String... args) { 
     new Thread(new MyRunnable(true), "Sets true").start(); 
     new Thread(new MyRunnable(false), "Sets false").start(); 
    } 

    private static class MyRunnable implements Runnable { 
     private final boolean target; 

     private MyRunnable(boolean target) { 
      this.target = target; 
     } 

     @Override 
     public void run() { 
      int count = 0; 
      boolean logged = false; 
      while (true) { 
       if (value != target) { 
        value = target; 
        count = 0; 
        if (!logged) 
         System.out.println(Thread.currentThread().getName() + ": reset value=" + value); 
       } else if (++count % 1000000000 == 0) { 
        System.out.println(Thread.currentThread().getName() + ": value=" + value + " target=" + target); 
        logged = true; 
       } 
      } 
     } 
    } 
} 

in dưới đây cho thấy giá trị lật của nó, nhưng bị kẹt.

Sets true: reset value=true 
Sets false: reset value=false 
... 
Sets true: reset value=true 
Sets false: reset value=false 
Sets true: value=false target=true 
Sets false: value=true target=false 
.... 
Sets true: value=false target=true 
Sets false: value=true target=false 

Nếu tôi thêm -XX:+PrintCompilation chuyển đổi này xảy ra khoảng thời gian mà bạn nhìn thấy

1705 1 % RequiresVolatileMain$MyRunnable::run @ -2 (129 bytes) made not entrant 
1705 2 % RequiresVolatileMain$MyRunnable::run @ 4 (129 bytes) 

Những gợi ý mã đã được biên soạn để có nguồn gốc là một cách mà không phải là thread an toàn.

nếu bạn làm cho giá trị volatile bạn nhìn thấy nó lật giá trị vô tận (hoặc cho đến khi tôi đã chán)

EDIT: Điều gì kiểm tra này không có gì; khi nó phát hiện giá trị không phải là giá trị mục tiêu của chủ đề, nó đặt giá trị. I E. chuỗi 0 đặt thành true và chuỗi 1 đặt thành false Khi hai chuỗi đang chia sẻ trường đúng cách, chúng sẽ thấy mỗi thay đổi khác và giá trị liên tục lật giữa giá trị đúng và sai.

Không có biến động này không thành công và mỗi chuỗi chỉ thấy giá trị riêng của chúng, do đó cả hai đều thay đổi giá trị và chuỗi 0 xem true và chuỗi 1 xem false cho cùng một trường.

+1

Bạn có thể cung cấp một ví dụ với vòng lặp sẽ chứng minh sự cố không? –

+0

Không chắc chắn tôi nhận được điều này! –

+0

Tôi đã thêm giải thích. Hãy cho tôi biết nếu bạn có nhiều nghi ngờ/câu hỏi. –

1

Tùy thuộc vào hệ điều hành của bạn, Thread.yield() có thể có hoặc không hoạt động. Thread.yield() thực sự không thể được coi là nền tảng độc lập, và không nên được sử dụng nếu bạn cần giả định đó.

Làm ví dụ làm những gì bạn mong đợi, tôi nghĩ đó là vấn đề về kiến ​​trúc bộ xử lý hơn bất cứ thứ gì khác ... thử chạy trên các máy khác nhau, với hệ điều hành khác nhau, xem bạn có thể thoát ra khỏi nó.

+0

Điều này ít liên quan đến thread.yield ... về mô hình bộ nhớ dùng chung trong java. –

2

Không chắc chắn 100% về vấn đề này, nhưng this có thể liên quan:

nghĩa là gì sắp xếp lại?

Có một số trường hợp, trong đó truy cập vào chương trình biến (trường hợp đối tượng, lĩnh vực tĩnh lớp, và các phần tử mảng) có thể xuất hiện để thực hiện theo một thứ tự khác nhau hơn đã được chỉ định bởi các chương trình . Trình biên dịch là miễn phí để có quyền tự do với thứ tự của hướng dẫn trong tên tối ưu hóa. Bộ xử lý có thể thực thi các lệnh không đúng theo một số trường hợp nhất định. Dữ liệu có thể được di chuyển giữa các thanh ghi, bộ nhớ cache của bộ xử lý và bộ nhớ chính trong thứ tự khác với chương trình được chỉ định. Ví dụ, nếu một luồng ghi vào trường a và sau đó đến trường b và giá trị của b không phụ thuộc vào giá trị của a, thì trình biên dịch là miễn phí để sắp xếp lại các hoạt động này và bộ nhớ cache là miễn phí để tuôn ra b đến bộ nhớ chính trước khi a. Có một số nguồn tiềm năng của sắp xếp lại, chẳng hạn như trình biên dịch, JIT và bộ nhớ cache.

Trình biên dịch, chạy, và phần cứng có nghĩa vụ phải quy tụ lại để tạo ảo giác về as-if-sê-ri ngữ nghĩa, có nghĩa là trong một chương trình đơn luồng , chương trình không nên có thể quan sát các tác sắp xếp lại. Tuy nhiên, sắp xếp lại có thể phát trong các chương trình đa luồng được đồng bộ hóa không chính xác, trong đó một chủ đề là có thể quan sát các hiệu ứng của các chủ đề khác và có thể phát hiện các truy cập biến đó thành các chủ đề khác trong một đơn đặt hàng khác nhau hơn được thực hiện hoặc được chỉ định trong chương trình.

+0

+1 Bản in của 0 là do sắp xếp lại. Từ chính cuốn sách: "NoVisibility có thể in bằng không vì ghi để sẵn sàng có thể được hiển thị cho chủ đọc trước khi ghi vào số, một hiện tượng được gọi là sắp xếp lại" – zengr

6

Mô hình bộ nhớ java xác định những gì được yêu cầu để hoạt động và những gì không. "vẻ đẹp" của mã đa luồng không an toàn là trong hầu hết các tình huống (đặc biệt là trong các môi trường dev bị contolled) nó thường hoạt động.đó là chỉ khi bạn nhận được để sản xuất với một máy tính tốt hơn và tải tăng và JIT thực sự đá trong đó các lỗi bắt đầu cắn.

+0

không phải là câu trả lời tôi tin. – zengr

+1

không phải là câu trả lời chắc chắn mà là một thực tế thô lỗ! +1 – mawia

2

Tôi nghĩ rằng điểm chính về điều này là nó không được đảm bảo rằng tất cả các jvms sẽ sắp xếp lại các hướng dẫn theo cùng một cách. Nó được sử dụng như là một ví dụ mà các sắp xếp lại khác nhau có thể tồn tại, và do đó đối với một số triển khai của jvm, bạn có thể nhận được các kết quả khác nhau. Nó chỉ xảy ra khi bạn jvm sắp xếp lại theo cùng một cách, nhưng đó có thể không phải là trường hợp khác. Cách duy nhất để đảm bảo đặt hàng là sử dụng đồng bộ hóa thích hợp.

0

Vui lòng xem mã bên dưới, Nó giới thiệu lỗi hiển thị dữ liệu trên x86. Cố gắng với jdk8 và JDK7

package com.snippets; 


public class SharedVariable { 

    private static int sharedVariable = 0;// declare as volatile to make it work 
    public static void main(String[] args) throws InterruptedException { 

     new Thread(new Runnable() { 

      @Override 
      public void run() { 
       try { 
        Thread.sleep(1000); 
       } catch (InterruptedException e) { 
        e.printStackTrace(); 
       } 
       sharedVariable = 1; 
      } 
     }).start(); 

     for(int i=0;i<1000;i++) { 
      for(;;) { 
       if(sharedVariable == 1) { 
        break; 
       } 
      } 
     } 
     System.out.println("Value of SharedVariable : " + sharedVariable); 
    } 

} 

Lừa không phải là để mong đợi bộ xử lý để thực hiện sắp xếp lại chứ không phải làm cho trình biên dịch để làm cho một số tối ưu hóa trong đó giới thiệu các lỗi tầm nhìn.

Nếu bạn chạy mã trên, bạn sẽ thấy mã treo vô thời hạn vì không bao giờ thấy giá trị được cập nhật được chia sẻVariable.

Để sửa mã, hãy khai báo sharedVariable là dễ bay hơi.

Tại sao biến thông thường không hoạt động và chương trình trên bị treo?

  1. sharedVariable không được khai báo là dễ bay hơi.
  2. Bây giờ vì sharedVariable không được khai báo là trình biên dịch biến động tối ưu hóa mã. Nó thấy rằng sharedVariable sẽ không được thay đổi vì vậy tại sao tôi nên đọc từ bộ nhớ mỗi lần trong vòng lặp. Nó sẽ đưa sharedVariable ra khỏi vòng lặp. Một cái gì đó tương tự như dưới đây.

f

for(int i=0;i<1000;i++)/**compiler reorders sharedVariable 
as it is not declared as volatile 
and takes out the if condition out of the loop 
which is valid as compiler figures out that it not gonna 
change sharedVariable is not going change **/ 
    if(sharedVariable != 1) { 
    for(;;) {} 
    }  
} 

chung tại github: https://github.com/lazysun/concurrency/blob/master/Concurrency/src/com/snippets/SharedVariable.java