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