代码之家  ›  专栏  ›  技术社区  ›  JustJeff

Java原语是通过设计还是意外设计成原子?

  •  40
  • JustJeff  · 技术社区  · 16 年前

    那就是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上用JavaJRE 1.60Y07尝试的)

    本质上,修饰符将计数序列写入共享整数,而观察者则检查观察到的值是否从未减少。在一台必须以四个单独字节(甚至两个16位字)访问32位值的计算机上,观察程序有可能在不一致的半更新状态下捕获共享整数,并检测该值的减少而不是增加。无论(假设的)数据字节是收集/写入LSB 1st还是MSB 1st,这都应该有效,但充其量只是可能的。

    在今天的广泛数据路径中,即使Java规范不需要32位的值也可以是原子的,这是很有可能的。事实上,使用32位数据总线,您可能需要更加努力才能获得原子访问 字节 大于32位整数。

    Google在“Java原始线程安全”上发现了线程安全类和对象上的大量负载,但是寻找原语上的信息似乎是在大海捞针。

    9 回复  |  直到 7 年前
        1
  •  58
  •   Jon Skeet    13 年前

    默认情况下,Java中的所有内存访问都是原子的,除非 long double (哪个) 可以 要原子化,但不一定要原子化)。不放 非常 很明显,说实话,但我相信这就是含义。

    section 17.4.3 JLS:

    在连续一致的 执行,总订单结束 所有单个动作(如读取 和写作)这与 程序的顺序,以及每个 个体行为是原子的,是 每根线都能立即看到。

    然后在 17.7 :

    有些实现可能会找到它 方便分写 对64位长或双位的操作 值转换为两个写操作 相邻的32位值。为了 为了效率,这种行为是 具体实现 计算机可以自由执行写入 原子上的长值和双值或 分两部分。

    注意原子性和波动性是非常不同的。

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

    关于努力获得对字节的原子访问,你是对的:虚拟机可能需要努力…但这是必须的。从 section 17.6 规格:

    某些处理器不提供 能够写入单个字节。它 实现字节是非法的 在这样的处理器上阵列更新 简单地读完一个单词, 更新适当的字节,以及 然后把整个单词写回 记忆。这个问题有时是 被称为撕裂,等等 无法轻松更新的处理器 单字节隔离 需要进近。

    换言之,这取决于JVM如何正确处理。

        2
  •  26
  •   reevesy onejigtwojig    13 年前
    • 没有多少测试可以证明线程的安全性-它只能 反驳 它;
    • 我在 JLS 17.7 哪些状态

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

    再往下走

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

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

        3
  •  4
  •   Michal B    14 年前

    我认为这并不像你预期的那样有效:

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

    整数是不可变的,所以您一直在同步处理不同的对象。 int“i”被自动框定为整数对象,然后在其上设置锁。 如果另一个线程进入这个方法int,我将被自动定位到另一个整型对象,并且您在另一个对象上设置了锁,那么之前。

        4
  •  4
  •   Kounavi    8 年前

    我同意乔恩·斯基特的观点,我想补充一下,许多人混淆了原子性、波动性和线程安全的概念,因为有时术语可以互换使用。
    例如,考虑一下:

    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

    希望这有帮助。

    更新 :也可以在类对象本身上进行同步。更多信息在这里: How to synchronize a static variable among threads running different instances of a class in java?

        5
  •  3
  •   James    16 年前

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

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

    最后,线程可能会缓存变量的值,并且不会看到其他线程对它所做的更改。为了确保两个线程都能看到另一个线程所做的更改,需要将变量标记为易失性。

        6
  •  1
  •   adekock11    12 年前

    这不是原子的:

    i++;
    

    但是,这是:

    i = 5;
    

    我想这就是一些混乱的地方。

        7
  •  0
  •   GaryF    16 年前

    这有点复杂,并且与系统字大小有关。布鲁斯·埃克尔更详细地讨论了这个问题: Java Threads .

        8
  •  0
  •   electroCutie    13 年前

    原子读写仅仅意味着永远不会读,例如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
    
        9
  •  0
  •   Rodrigo Gomez    7 年前

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

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

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