java原始的设计或意外原子?

时间:2009-06-17 12:22:42

标签: java multithreading

原始java原始整数(int)是什么原因?有两个线程共享一个int的实验似乎表明它们,但当然没有证据证明它们并不意味着它们是。

具体来说,我跑的测试是这样的:

public class IntSafeChecker {
    static int thing;
    static boolean keepWatching = true;

    // Watcher just looks for monotonically increasing values   
    static class Watcher extends Thread {
        public void run() {
            boolean hasBefore = false;
            int thingBefore = 0;

            while( keepWatching ) {
                // observe the shared int
                int thingNow = thing;
                // fake the 1st value to keep test happy
                if( hasBefore == false ) {
                    thingBefore = thingNow;
                    hasBefore = true;
                }
                // check for decreases (due to partially written values)
                if( thingNow < thingBefore ) {
                    System.err.println("MAJOR TROUBLE!");
                }
                thingBefore = thingNow;
            }
        }
    }

    // Modifier just counts the shared int up to 1 billion
    static class Modifier extends Thread {
        public void run() {
            int what = 0;
            for(int i = 0; i < 1000000000; ++i) {
                what += 1;
                thing = what;
            }
            // kill the watcher when done
            keepWatching = false;
        }
    }

    public static void main(String[] args) {
        Modifier m = new Modifier();
        Watcher w = new Watcher();
        m.start();
        w.start();
    }
}

(仅在32位Windows PC上尝试使用java jre 1.6.0_07)

实质上,Modifier将计数序列写入共享整数,而Watcher检查观察值是否永远不会减少。在必须以四个独立字节(或甚至两个16位字)访问32位值的机器上,观察者有可能在不一致的半更新状态下捕获共享整数,并检测到值减少而不是增加。无论(假设的)数据字节是收集/写入LSB 1st还是MSB 1st,这都应该有效,但最多只能是概率。

考虑到今天的宽数据路径,即使java规范不需要它,32位值也可能是有效的原子,这似乎是非常可能的。事实上,使用32位数据总线,您可能需要更加努力地获得字节的原子访问权限而不是32位数据。

谷歌搜索“java原始线程安全”会在线程安全的类和对象上显示大量内容,但是查找基元的信息似乎是在大海捞针中寻找谚语。

9 个答案:

答案 0 :(得分:59)

默认情况下,Java中的所有内存访问都是原子的,longdouble除外(可能是原子的,但不一定是)。说实话并不清楚非常,但我相信这就是含义。

来自JLS的section 17.4.3

  

顺序一致   执行,总订单结束   所有个人行为(如阅读   和写道)这是一致的   程序的顺序,以及每个程序   个人行为是原子的,是   每个线程立即可见。

然后在17.7

  

某些实现可能会找到它   方便划分单个写入   动作为64位长或双   值为两个写操作   相邻的32位值。对于   效率的缘故,这种行为是   具体实施; Java虚拟   机器可以自由执行写入   原子或长的双重值   分两部分。

请注意,原子性与波动性有很大不同。

当一个线程将整数更新为5时,保证另一个线程不会看到1或4或任何其他中间状态,但没有任何明显的波动或锁定,另一个线程可以永远看到0。

关于努力获得字节的原子访问权,你是对的:VM可能必须努力......但它确实必须这样做。来自规范的section 17.6

  

有些处理器没有提供   能够写入单个字节。它   实现字节是非法的   这种处理器上的数组更新   只是阅读整个单词,   更新适当的字节,和   然后将整个单词写回   记忆。有时这个问题   被称为单词撕裂,并且   处理器无法轻松更新   单个字节隔离其他一些   方法是必需的。

换句话说,由JVM决定是否正确。

答案 1 :(得分:27)

  • 没有多少测试可以证明线程安全 - 它只能反驳;
  • 我在JLS 17.7中找到了间接引用状态
  

某些实现可能会发现将64位长或双值上的单个写操作分成相邻32位值的两个写操作很方便。

并进一步向下

  

出于Java编程语言内存模型的目的,对非易失性long或double值的单次写入被视为两个单独的写入:每个32位半写一个。

这似乎意味着对int的写入是原子的。

答案 2 :(得分:4)

我同意Jon Skeet,我想补充一点,很多人混淆了原子性,波动性和线程安全的概念,因为有时这些术语可以互换使用。
例如,考虑一下:

private static int i = 0;
public void increment() { i++; }

虽然有人可能认为这种操作是原子的,但提到的假设是错误的 声明i++;执行三项操作:
1)阅读
2)更新
3)写上

因此,对此变量进行操作的线程应该像这样同步:

private static int i = 0;
private static final Object LOCK = new Object();
public void increment() {
   synchronized(LOCK) {
       i++;
    } 
}

或者这个:

private static int i = 0;
public static synchronized void increment() {
   i++; 
}

请注意,对于单个对象实例,调用多个线程正在访问的方法并对共享的可变数据进行操作时,必须考虑到方法的参数,局部变量和返回值是本地的每个线程。

有关更多信息,请查看此链接:
http://www.javamex.com/tutorials/synchronization_volatile.shtml

希望这有帮助。

UPDATE :还有一种情况是您可以在类对象本身上进行同步。更多信息:How to synchronize a static variable among threads running different instances of a class in java?

答案 3 :(得分:4)

我认为它不像你期望的那样有效:

private static int i = 0;
public void increment() {
   synchronized (i) { 
      i++; 
   }
}

integer是不可变的,因此您始终在不同的对象上进行同步。 int“i”被自动装箱到Integer对象,然后你设置锁定它。 如果另一个线程进入此方法,则int被自动装箱到另一个Integer对象,然后在之前设置对另一个对象的锁定。

答案 4 :(得分:3)

从整数或任何较小类型读取或写入应该是原子的,但正如Robert所说,long和double可能会或可能不会取决于实现。但是,任何同时使用读取和写入的操作(包括所有增量操作符)都不是原子操作。因此,如果你必须在一个整数i = 0上运行的线程,一个是i ++,另一个是i = 10,结果可能是1,10或11。

对于这样的操作,您应该查看AtomicInteger,它具有在检索旧值时原子修改值的方法或以原子方式递增值。

最后,线程可以缓存变量的值,并且不会看到从其他线程对其进行的更改。要确保两个线程始终看到另一个线程所做的更改,您需要将变量标记为volatile。

答案 5 :(得分:1)

这不是原子的:

i++;

但是,这是:

i = 5;

我认为这是一些混乱的地方。

答案 6 :(得分:0)

这有点复杂,与系统字大小有关。 Bruce Eckel更详细地讨论了它:Java Threads

答案 7 :(得分:0)

原子读写只是意味着你永远不会读,例如int更新的前16位和旧值的另一位。

这没有说明其他线程何时看到这些写道。

长话短说,当两条线程在没有记忆障碍的情况下竞争时,会丢失一些东西。

旋转两个或多个增加单个共享整数的线程,并计算自己的增量。当整数达到某个值时(例如,INT_MAX。为了让事情变暖,很好和很大)停止一切并返回int的值和每个线程执行的增量数。

    import java.util.Stack;

public class Test{

  static int ctr = Integer.MIN_VALUE;
  final static int THREADS = 4;

  private static void runone(){
    ctr = 0;
    Stack<Thread> threads = new Stack<>();
    for(int i = 0; i < THREADS; i++){
      Thread t = new Thread(new Runnable(){
        long cycles = 0;

        @Override
        public void run(){
          while(ctr != Integer.MAX_VALUE){
            ctr++;
            cycles++;
          }
          System.out.println("Cycles: " + cycles + ", ctr: " + ctr);
        }
      });
      t.start();
      threads.push(t);
    }
    while(!threads.isEmpty())
      try{
        threads.pop().join();
      }catch(InterruptedException e){
        // TODO Auto-generated catch block
        e.printStackTrace();
      }
    System.out.println();
  }

  public static void main(String args[]){
    System.out.println("Int Range: " + ((long) Integer.MAX_VALUE - (long) Integer.MIN_VALUE));
    System.out.println("  Int Max: " + Integer.MAX_VALUE);
    System.out.println();
    for(;;)
      runone();
  }
}

这是我的四核盒子上的测试结果(可以随意使用代码中的线程数,我只是匹配我的核心数,显然):

Int Range: 4294967295
Int Max: 2147483647

Cycles: 2145700893, ctr: 76261202
Cycles: 2147479716, ctr: 1825148133
Cycles: 2146138184, ctr: 1078605849
Cycles: 2147282173, ctr: 2147483647

Cycles: 2147421893, ctr: 127333260
Cycles: 2146759053, ctr: 220350845
Cycles: 2146742845, ctr: 450438551
Cycles: 2146537691, ctr: 2147483647

Cycles: 2110149932, ctr: 696604594
Cycles: 2146769437, ctr: 2147483647
Cycles: 2147095646, ctr: 2147483647
Cycles: 2147483647, ctr: 2147483647

Cycles: 2147483647, ctr: 330141890
Cycles: 2145029662, ctr: 2147483647
Cycles: 2143136845, ctr: 2147483647
Cycles: 2147007903, ctr: 2147483647

Cycles: 2147483647, ctr: 197621458
Cycles: 2076982910, ctr: 2147483647
Cycles: 2125642094, ctr: 2147483647
Cycles: 2125321197, ctr: 2147483647

Cycles: 2132759837, ctr: 330963474
Cycles: 2102475117, ctr: 2147483647
Cycles: 2147390638, ctr: 2147483647
Cycles: 2147483647, ctr: 2147483647

答案 8 :(得分:0)

在线程之间共享数据时,需要同步。当处理整数(可以从主内存到多处理器系统中的处理器缓存)时,线程可能正在更新绑定到特定处理器的整数的本地副本。

Java中的易失性(See Wiki in Java Section)关键字将确保对Integer的任何更新都将在内存中发生,而不是在本地副本中发生。

此外,要将更新同步到Integer,请考虑使用AtomicInteger。此实现具有方法(compareAndSet)来检查某个值是否是线程期望的值,并设置该值。如果不匹配,则另一个线程可能已经更新了该值。 AtomicInteger将以原子操作执行对Integer的读取和更新,其优点是不必阻塞。