是否可以同步所有改变对象状态但不同步任何原子的方法?在这种情况下,只返回一个字段?
考虑:
public class A
{
private int a = 5;
private static final Object lock = new Object();
public void incrementA()
{
synchronized(lock)
{
a += 1;
}
}
public int getA()
{
return a;
}
}
我听说有人认为getA()
和incrementA()
可能会在大致相同的时间被调用,getA()
会回到错误的位置。然而,似乎在它们同时被调用的情况下,即使吸气剂同步,你也可能得到错误的东西。实际上,如果同时调用这些“正确的东西”,它们似乎甚至没有被定义。对我来说最重要的是国家保持一致。
我也听说过关于JIT优化的讨论。给定上述类的实例和以下代码(代码将依赖于a
在另一个线程中设置):
while(myA.getA() < 10)
{
//incrementA is not called here
}
显然,合法的JIT优化将其更改为:
int temp = myA.getA();
while(temp < 10)
{
//incrementA is not called here
}
显然会导致无限循环。
为什么这是合法的优化?如果a
不稳定,这会非法吗?
我做了一些测试。
public class Test
{
private int a = 5;
private static final Object lock = new Object();
public void incrementA()
{
synchronized(lock)
{
a += 1;
}
}
public int getA()
{
return a;
}
public static void main(String[] args)
{
final Test myA = new Test();
Thread t = new Thread(new Runnable(){
public void run() {
while(true)
{
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
myA.incrementA();
}
}});
t.start();
while(myA.getA() < 15)
{
System.out.println(myA.getA());
}
}
}
使用几种不同的睡眠时间,即使a不易变,也能正常工作。这当然不是决定性的,它仍然可能是合法的。有没有人有一些可以触发这种JIT行为的例子?
答案 0 :(得分:7)
是否可以同步所有改变对象状态但不同步任何原子的方法?在这种情况下,只返回一个字段?
取决于具体细节。重要的是要意识到同步做了两件重要的事情。它不是只是关于原子性,但由于内存同步,它也是必需的。如果一个线程更新a
字段,则由于本地处理器上的内存缓存,其他线程可能看不到更新。将int a
字段设为volatile
可以解决此问题。将get和set方法同时设为synchronized
也会更加昂贵。
如果您希望能够从多个线程更改和阅读a
,最好的机制是使用AtomicInteger
。
private AtomicInteger a = new AtomicInteger(5);
public void setA(int a) {
// no need to synchronize because of the magic of the `AtomicInteger` code
this.a.set(a);
}
public int getA() {
// AtomicInteger also takes care of the memory synchronization
return a.get();
}
我听说有人认为getA()和setA()几乎可以同时被调用并且让getA()返回错误的东西。
这是正确的,但如果稍后调用它,则可能会得到错误的值。错误的缓存值可能永远存在。
显然会导致无限循环。为什么这是合法的优化?
这是一种合法的优化,因为以异步方式运行自己的内存缓存的线程是查看性能改进的重要原因之一。如果所有内存访问都与主内存同步,则不会使用每CPU内存缓存,并且线程程序运行速度会慢很多。
如果a不稳定,这会非法吗?
如果a
有某种方式被改变 - 可能是另一个线程,这是不合法的。如果a
是最终的,那么JIT可以进行优化。如果a
为volatile
或get方法标记为synchronized
,则肯定不会是合法优化。
答案 1 :(得分:3)
它不是线程安全的,因为getter不能确保线程会看到最新的值,因为该值可能是陈旧的。让getter同步可确保调用getter的任何线程都能看到最新的值而不是可能的过时值。
答案 2 :(得分:0)
您基本上有两个选择:
1)让你的int变得不稳定 2)使用原子类型,如AtomicInt
使用没有同步的普通int
根本不是线程安全的。
答案 3 :(得分:0)
您最好的解决方案是使用AtomicInteger,它们基本上是针对这个用例而设计的。
如果这更像是一个理论上的“这可以做到这个问题”,我认为以下内容是安全的(但仍然不如AtomicInteger那样好):
public class A
{
private volatile int a = 5;
private static final Object lock = new Object();
public void incrementA()
{
synchronized(lock)
{
final int tmp = a + 1;
a = tmp;
}
}
public int getA()
{
return a;
}
}
答案 4 :(得分:0)
简短的答案是,如果您的示例是线程安全的
volatile
,或synchronized
。示例类A
并非线程安全的原因是,可以使用该类创建没有具有“格式正确的执行程序”的程序(请参见{ {3}})。
例如,考虑
// in thread #1
int a1 = A.getA();
Thread.sleep(...);
int a2 = A.getA();
if (a1 == a2) {
System.out.println("no increment");
// in thread #2
A.incrementA();
在睡眠期间发生增量的情况。
为使此执行格式正确,在线程#2调用a
中对incrementA
的{{1}}的赋值与随后的读取之间,必须有一个“发生之前”(HB)链a
中的getA
由线程#1调用。
如果两个线程使用同一个锁对象进行同步,则一个线程释放锁与第二个线程获取锁之间存在HB。所以我们得到了:
thread #2 acquires lock --HB-->
thread #2 reads a --HB-->
thread #2 writes a --HB-->
thread #2 releases lock --HB-->
thread #1 acquires lock --HB-->
thread #1 reads a
如果两个线程共享一个volatile变量,则任何写操作与任何后续读操作之间都存在HB(无中间写操作)。所以我们通常会得到这个:
thread #2 acquires lock --HB-->
thread #2 reads a --HB-->
thread #2 writes a --HB-->
thread #1 reads a
请注意,incrementA
必须与其他调用incrementA
的线程进行同步以避免竞争。
如果以上两个都不成立,我们会得到:
thread #2 acquires lock --HB-->
thread #2 reads a --HB-->
thread #2 writes a // No HB!!
thread #1 reads a
由于线程2的写入与线程1的后续读取之间没有HB,因此JLS不保证后者将看到前者写入的值。
请注意,这是规则的简化版本。要获取完整版本,您需要阅读所有JLS 17.4.7。