首页 文章

私有构造函数,以避免竞争条件

提问于
浏览
44

我正在读这本书 Java Concurrency in Practice session 4.3.5

@ThreadSafe
  public class SafePoint{

       @GuardedBy("this") private int x,y;

       private SafePoint (int [] a) { this (a[0], a[1]); }

       public SafePoint(SafePoint p) { this (p.get()); }

       public SafePoint(int x, int y){
            this.x = x;
            this.y = y;
       }

       public synchronized int[] get(){
            return new int[] {x,y};
       }

       public synchronized void set(int x, int y){
            this.x = x;
            this.y = y;
       }

  }

我不清楚它在哪里说

私有构造函数的存在是为了避免在复制构造函数实现为此时发生的竞争条件(p.x,p.y);这是私有构造函数捕获习惯的一个例子(Bloch和Gafter,2005) .

据我所知,它提供了一个getter,可以在一个数组中同时检索x和y,而不是每个都有一个单独的getter,因此调用者将看到一致的值,但为什么是私有构造函数?这里有什么诀窍

9 回答

  • 0

    Java中的构造函数无法同步 .

    我们无法将 public SafePoint(SafePoint p) 实现为 { this (p.x, p.y); } 因为

    因为我们没有同步(并且不能像我们在构造函数中那样),在构造函数的执行期间,有人可能正在从不同的线程调用 SafePoint.set()

    public synchronized void set(int x, int y){
            this.x = x; //this value was changed
    -->     this.y = y; //this value is not changed yet
       }
    

    所以我们将在不一致的状态下读取对象 .

    因此,我们以线程安全的方式创建快照,并将其传递给私有构造函数 . 堆栈限制保护对阵列的引用,因此没有什么可担心的 .

    update 哈!至于诀窍一切都很简单 - 你错过了 @ThreadSafe 您示例中的书中的注释:

    @ThreadSafe公共类SafePoint {}

    因此,如果将int数组作为参数的构造函数将是公共的或受保护的,则该类将不再是线程安全的,因为该数组的内容可能会改变与SafePoint类相同的方式(即某人可能在更改期间更改它)构造函数执行)!

  • 36

    这里已经有很多答案,但我真的想深入了解一些细节(就像我的知识让我一样) . 我强烈建议您运行答案中的每个样本,以便亲自了解事情的发生和原因 .

    要了解解决方案,您需要首先了解问题 .

    假设SafePoint类实际上如下所示:

    class SafePoint {
        private int x;
        private int y;
    
        public SafePoint(int x, int y){
            this.x = x;
            this.y = y;
        }
    
        public SafePoint(SafePoint safePoint){
            this(safePoint.x, safePoint.y);
        }
    
        public synchronized int[] getXY(){
            return new int[]{x,y};
        }
    
        public synchronized void setXY(int x, int y){
            this.x = x;
            //Simulate some resource intensive work that starts EXACTLY at this point, causing a small delay
            try {
                Thread.sleep(10 * 100);
            } catch (InterruptedException e) {
             e.printStackTrace();
            }
            this.y = y;
        }
    
        public String toString(){
          return Objects.toStringHelper(this.getClass()).add("X", x).add("Y", y).toString();
        }
    }
    

    什么变量创建了这个对象的状态?只有两个:x,y . 它们是否受到某些同步机制的保护?那么它们是通过内部锁定,通过synchronized关键字 - 至少在setter和getter中 . 他们在其他地方'感动'吗?当然在这里:

    public SafePoint(SafePoint safePoint){
        this(safePoint.x, safePoint.y);
    }
    

    你在这里做的是从你的对象中读取 . 要使类成为线程安全,您必须协调对它的读/写访问,或者在同一个锁上进行同步 . 但这里没有发生这样的事情 . setXY 方法确实是同步的,但克隆构造函数不是,因此调用这两个方法可以以非线程安全的方式完成 . 我们可以制动这门课吗?

    我们来试试吧:

    public class SafePointMain {
    public static void main(String[] args) throws Exception {
        final SafePoint originalSafePoint = new SafePoint(1,1);
    
        //One Thread is trying to change this SafePoint
        new Thread(new Runnable() {
            @Override
            public void run() {
                originalSafePoint.setXY(2, 2);
                System.out.println("Original : " + originalSafePoint.toString());
            }
        }).start();
    
        //The other Thread is trying to create a copy. The copy, depending on the JVM, MUST be either (1,1) or (2,2)
        //depending on which Thread starts first, but it can not be (1,2) or (2,1) for example.
        new Thread(new Runnable() {
            @Override
            public void run() {
                SafePoint copySafePoint = new SafePoint(originalSafePoint);
                System.out.println("Copy : " + copySafePoint.toString());
            }
        }).start();
    }
    }
    

    输出很容易这个:

    Copy : SafePoint{X=2, Y=1}
     Original : SafePoint{X=2, Y=2}
    

    这是逻辑,因为一个Thread更新=写入我们的对象而另一个是从它读取 . 它们不会在某些常见的锁上同步,从而导致输出 .

    解?

    • synchronized构造函数,以便读取将在同一个锁上同步,但Java中的构造函数不能使用synchronized关键字 - 当然这是逻辑 .

    • 可能使用不同的锁,例如Reentrant lock(如果不能使用synchronized关键字) . 但它也行不通,因为构造函数中的第一个语句必须是对this / super的调用 . 如果我们实现一个不同的锁,那么第一行必须是这样的:

    lock.lock()//其中lock是ReentrantLock,由于上述原因,编译器不允许这样做 .

    • 如果我们将构造函数设为方法怎么办?当然这会奏效!

    例如,请参阅此代码

    /*
     * this is a refactored method, instead of a constructor
     */
    public SafePoint cloneSafePoint(SafePoint originalSafePoint){
         int [] xy = originalSafePoint.getXY();
         return new SafePoint(xy[0], xy[1]);    
    }
    

    电话会是这样的:

    public void run() {
          SafePoint copySafePoint = originalSafePoint.cloneSafePoint(originalSafePoint);
          //SafePoint copySafePoint = new SafePoint(originalSafePoint);
          System.out.println("Copy : " + copySafePoint.toString());
     }
    

    这次代码按预期运行,因为读取和写入在同一个锁上同步,但 we have dropped the constructor . 如果不允许怎么办?

    我们需要找到一种在同一个锁上同步读取和写入SafePoint的方法 .

    理想情况下,我们想要这样的东西:

    public SafePoint(SafePoint safePoint){
         int [] xy = safePoint.getXY();
         this(xy[0], xy[1]);
     }
    

    但编译器不允许这样做 .

    我们可以通过调用* getXY方法安全地阅读,所以我们需要一种方法来使用它,但是我们没有一个构造函数来接受这样的参数 - 创建一个 .

    private SafePoint(int [] xy){
        this(xy[0], xy[1]);
    }
    

    然后,实际的调用:

    public  SafePoint (SafePoint safePoint){
        this(safePoint.getXY());
    }
    

    请注意,构造函数是私有的,这是因为我们不想暴露另一个公共构造函数并再次考虑类的不变量,因此我们将其设为私有 - 只有我们可以调用它 .

  • 5

    私有构造函数是以下的替代:

    public SafePoint(SafePoint p) {
        int[] a = p.get();
        this.x = a[0];
        this.y = a[1];
    }
    

    但允许构造函数链接以避免重复初始化 .

    如果 SafePoint(int[]) 是公共的,那么 SafePoint 类不能保证线程安全,因为在 SafePoint 类读取的 xy 的值之间,由另一个持有对同一数组的引用的线程可以修改数组的内容 .

  • 6

    我明白它提供了一个getter来同时在一个数组中检索x和y,而不是每个都有一个单独的getter,所以调用者会看到一致的值,但为什么是私有构造函数呢?这里的诀窍是什么?

    我们在这里想要的是链接构造函数调用以避免代码重复 . 理想情况下,我们想要的是这样的:

    public SafePoint(SafePoint p) {
        int[] values = p.get();
        this(values[0], values[1]);
    }
    

    但这不起作用,因为我们会得到一个编译器错误:

    call to this must be first statement in constructor
    

    我们也不能使用它:

    public SafePoint(SafePoint p) {
        this(p.get()[0], p.get()[1]); // alternatively this(p.x, p.y);
    }
    

    因为那时我们有一个条件,在调用 p.get() 之间可能已经改变了值 .

    所以我们想要从SafePoint和链中捕获值到另一个构造函数 . 这就是为什么我们将使用私有构造函数捕获习惯用法并将私有构造函数和链中的值捕获到"real"构造函数:

    private SafePoint(int[] a) {
        this(a[0], a[1]);
    }
    

    另请注意

    private SafePoint (int [] a) { this (a[0], a[1]); }
    

    在课外没有任何意义 . 二维点有两个值,而不是数组建议的任意值 . 它没有检查数组的长度,也没有 null . 它仅在类中使用,并且调用者知道使用数组中的两个值调用是安全的 .

  • 14

    使用SafePoint的目的是始终提供x&y的一致视图 .

    例如,考虑SafePoint是(1,1) . 一个线程正在尝试读取此SafePoint,而另一个线程正在尝试将其修改为(2,2) . 如果安全点不是线程安全的,那么就可以看到SafePoint将是(1,2)(或(2,1))不一致的视图 .

    提供线程安全一致视图的第一步不是提供对x&y的独立访问;但要提供一种同时访问它们的方法 . 类似的 Contract 适用于修饰方法 .

    此时,如果未在SafePoint内部实现复制构造函数,则完全是 . 但如果我们实施一个,我们需要小心 . 构造函数无法同步 . 诸如以下的实现将暴露不一致的状态,因为p.x和p.y是独立访问的 .

    public SafePoint(SafePoint p){
            this.x = p.x;
            this.y = p.y;
       }
    

    但以下不会破坏线程安全 .

    public SafePoint(SafePoint p){
            int[] arr = p.get();
            this.x = arr[0];
            this.y = arr[1];
       }
    

    为了重用代码,实现了一个接受int数组的私有构造函数,该构造函数委托给它(x,y) . int数组构造函数可以公开,但实际上它将类似于this(x,y) .

  • 2

    构造函数不应该在此类之外使用 . 客户端不应该能够构建数组并将其传递给此构造函数 .

    所有其他公共构造函数都暗示将调用SafePoint的get方法 .

    私有构造函数允许你以一种可能的Thread不安全方式构建自己的构造函数(即通过分别检索x,y,构建一个数组并传递它)

  • 0

    私有SafePoint(int [] a)提供两个功能:

    首先,防止其他人使用以下构造函数,因为其他线程可以获取数组的ref,并且可能在构造时更改数组

    int[] arr = new int[] {1, 2};
    // arr maybe obtained by other threads, wrong constructor
    SafePoint safepoint = new SafePoint(arr);
    

    其次,防止后来的程序员错误地实现复制构造函数,如下所示 . 这就是为什么作者说:

    私有构造函数的存在是为了避免在复制构造函数被实现时会发生的竞争条件(p.x,p.y)

    //p may be obtined by other threads, wrong constructor
    public SafePoint(SafePoint p) { this(p.x, p.y);}
    

    请参阅作者's implementation: you don' t不得不担心p被其他线程修改,因为p.get()返回一个新副本,p.get()也被p's守护,所以p不会改变,甚至不会被其他线程获得!

    public SafePoint(SafePoint p) {
        this(p.get());
    }
    public synchronized int[] get() {
        return new int[] {x, y};
    }
    
  • 0

    这意味着,如果您没有私有构造函数,并且您以下列方式实现了复制构造函数:

    public SafePoint(SafePoint p) {
        this(p.x, p.y);
    }
    

    现在假设线程A正在访问SafePoint p 正在执行复制构造函数的这个(px,py)指令,并且在不幸的时机,另一个也可以访问SafePoint的线程B p 在SafePoint上执行setter set(int x,int y) p . 由于您的复制构造函数直接访问 pxy 实例变量而没有正确锁定,因此可能会看到SafePoint p 的状态不一致 .

    私有构造函数访问 p 的位置变量 xy 通过getter进行同步,以确保您看到SafePoint p 的一致状态 .

  • 0

    我们的要求是:我们希望有一个像下面这样的复制构造函数(同时确保类仍然是线程安全的):

    public SafePoint(SafePoint p){
        // clones 'p' passed a parameter and return a new SafePoint object.
    }
    

    让我们尝试制作复制构造函数 .

    Approach 1:

    public SafePoint(SafePoint p){
        this(p.x, p.y);
    }
    

    上述方法的问题在于它将使我们的类不是安全的

    怎么样 ?

    因为构造函数不是同步的,这意味着两个线程可能同时作用于同一个对象(一个线程可能使用它来克隆该对象's copy constructor and other thread might invoke object'的setter方法) . 如果发生这种情况,调用setter方法的线程可能已更新 x 字段(并且尚未更新 y 字段),从而使对象处于不一致状态 . 现在,此时,如果另一个线程(克隆对象)执行(并且它可以执行因为构造函数未通过内部锁同步),则复制构造函数 this(p.x, p.y)p.x 将是新值,而 p.y 仍旧 .

    因此,我们的方法不是线程安全的,因为构造函数不同步 .

    Approach 2: (试图让方法1线程安全)

    public SafePoint(SafePoint p){
        int[] temp = p.get();
        this(temp[0], temp[1]);
    }
    

    这是线程安全的,因为 p.get() 由内部锁定同步 . 因此,当 p.get() 执行时,其他线程无法执行setter,因为getter和setter都由同一个内部锁保护 .

    但遗憾的是编译器不允许我们这样做,因为 this(p.x, p.y) 应该是第一个声明 .

    这将我们带到最后的方法 .

    Approach 3: (解决方法2的编译问题)

    public SafePoint(SafePoint p){
        this(p.get());
    }
    
    private SafePoint(int[] a){
        this(a[0], a[1]);
    }
    

    使用这种方法,我们保证我们的类是线程安全的,我们有我们的复制构造函数 .

    剩下的最后一个问题是为什么第二个构造函数是私有的?这只是因为我们仅为了内部目的而创建此构造函数,并且我们不希望客户端通过调用此方法来创建SafePoint对象 .

相关问题