Java:同步操作与波动性到底有什么关系?

Java:同步操作与波动性到底有什么关系?,java,multithreading,thread-safety,volatile,Java,Multithreading,Thread Safety,Volatile,对不起,这是一个很长的问题 我最近一直在做很多关于多线程的研究,我慢慢地将它应用到一个个人项目中。然而,可能是由于有大量稍微不正确的示例,在某些情况下使用同步块和波动性对我来说仍然有点不清楚 我的核心问题是:当线程位于同步块内时,对引用和原语的更改是否自动可变(即在主内存而不是缓存上执行),或者读取是否也必须同步才能正常工作 如果是同步一个简单的getter方法的目的是什么?(参见示例1)另外,只要线程对任何内容进行了同步,所有更改是否都会发送到主内存?例如,如果它被发送到一个非常高级别的同步中

对不起,这是一个很长的问题

我最近一直在做很多关于多线程的研究,我慢慢地将它应用到一个个人项目中。然而,可能是由于有大量稍微不正确的示例,在某些情况下使用同步块和波动性对我来说仍然有点不清楚

我的核心问题是:当线程位于同步块内时,对引用和原语的更改是否自动可变(即在主内存而不是缓存上执行),或者读取是否也必须同步才能正常工作

  • 如果是同步一个简单的getter方法的目的是什么?(参见示例1)另外,只要线程对任何内容进行了同步,所有更改是否都会发送到主内存?例如,如果它被发送到一个非常高级别的同步中去做大量的工作,那么每一次更改都会是对主内存的更改,而不会缓存任何内容,直到它再次解锁
  • 如果不是那么更改是否必须显式地在同步块中进行,或者java是否可以实际使用锁对象?(参见示例3)
  • 如果其中一个同步对象是否需要以任何方式与正在更改的引用/原语相关(例如包含它的立即对象)?如果安全的话,我可以在一个对象上同步写,然后在另一个对象上读吗?(参见示例2)
  • (请注意,对于以下示例,我知道不赞成使用同步方法和同步(这一点)及其原因,但关于这一点的讨论超出了我的问题范围)

    示例1:

    class Counter{
      int count = 0;
    
      public synchronized void increment(){
        count++;
      }
    
      public int getCount(){
        return count;
      }
    }
    
    在本例中,需要同步increment(),因为++不是原子操作。因此,两个线程同时递增可能会导致总数增加1。count原语需要是原子的(例如notlong/double/reference),这样就可以了

    getCount()是否需要在此处同步?具体原因是什么?我听到最多的解释是,我无法保证返回的计数是在增量之前还是之后。然而,这似乎是对稍有不同的解释,它发现自己在错误的地方。我的意思是,如果我要同步getCount(),那么我仍然看不到任何保证——现在是因为不知道锁定顺序,而不知道实际读取是否发生在实际写入之前/之后

    示例2:

    class Counter{
      int count = 0;
    
      public synchronized void increment(){
        count++;
      }
    
      public int getCount(){
        return count;
      }
    }
    
    下面的示例是线程安全的吗?如果您假设通过这里没有显示的技巧,这些方法都不会被同时调用?如果每次使用随机方法计算增量,然后正确读取,是否会以预期方式计算增量,或者锁必须是同一个对象?(顺便说一句,我完全意识到这个例子有多么不可思议,但我对理论比对实践更感兴趣)

    示例3:

    class Counter{
      int count = 0;
    
      public synchronized void increment(){
        count++;
      }
    
      public int getCount(){
        return count;
      }
    }
    
    “先发生后发生”关系只是一个java概念,还是构建在JVM中的一个实际事物?尽管我可以保证在下一个例子中,概念上的关系发生在关系之前,但如果它是内置的,java是否足够聪明来接受它呢?我假设不是,但是这个例子真的是线程安全的吗?如果它是线程安全的,那么如果getCount()没有锁定呢

    class Counter{
      private final Lock lock = new Lock();
      int count = 0;
    
      public void increment(){
        lock.lock();
        count++;
        lock.unlock();
      }
    
      public int getCount(){
        lock.lock();
        int count = this.count;
        lock.unlock();
        return count;
      }
    }
    

    试着从两个不同的简单操作的角度来看待它:

  • 锁定(互斥)
  • 内存障碍(缓存同步、指令重新排序障碍)
  • 进入
    同步
    块需要锁定和内存屏障;保持
    同步
    块需要解锁+内存屏障;读取/写入
    易失性
    字段仅需要内存屏障。用这些术语思考,我认为你可以为自己澄清上述所有问题

    如例1所示,读取线程将没有任何类型的内存屏障。这不仅仅是在读取前/读取后查看值之间,而是在线程启动后观察变量的任何变化

    例2。是你提出的最有趣的问题。在这种情况下,JLS确实没有向您提供任何保证。在实践中,您不会得到任何排序保证(就好像根本没有锁定方面),但您仍然可以享受内存屏障的好处,因此您可以观察到更改,这与第一个示例不同。基本上,这与删除
    synchronized
    并将
    int
    标记为
    volatile
    完全相同(除了获取锁的运行时成本)


    关于示例3,通过“just a Java thing”,我觉得您考虑了带有擦除的泛型,这是只有静态代码检查才知道的。这不是那样的——锁和内存屏障都是纯运行时工件。事实上,编译器根本无法对它们进行推理。

    是的,读取也必须同步。说:

    一个线程写入的结果保证对一个线程可见 仅当写入操作发生在 读取操作

    [……]

    监视器的解锁(同步块或方法退出) 在每个后续锁(同步块或方法)之前发生 同一监视器的输入)

    同一页说:

    “释放”同步器之前的操作方法,如Lock.unlock, Semaphore.release和CountDownLatch.countDown在操作之前发生 在成功的“获取”方法(如Lock.Lock)之后

    因此,锁提供了与同步块相同的可见性保证

    无论您使用同步块还是锁,只有当读卡器线程使用与写卡器线程相同的监视器或锁时,才能保证可见性

    • 您的示例1是inco