Действительно ли AtomicInteger производит атомарное целое число?

Я погуглил AtomicInteger и увидел, что кто-то сказал, что мы можем использовать AtomicInteger(AtomicLong) для секвенсора памяти (http://www.cs.hut.fi/u/tlilja/multicore/slides/java_multicore.pdf). Вот мой тест:

public class TestAtomicInteger {

public static void main(String[] args) {

    final AtomicInteger sequencer = new AtomicInteger(1);
    final Set<Integer> integers = new HashSet<>();
    //final Set<Integer> integers = new ConcurrentSkipListSet<>();


    final Runnable task = new Runnable() {

        @Override
        public void run() {
            int next = sequencer.getAndIncrement();
            integers.add(next);
            System.out.println(integers.size());
        }
    };
    for (int i = 0; i < 1000; i++) {
        Thread t = new Thread(task);
        t.start();
    }
}
}

После многократного запуска этого кода я обнаружил, что иногда общее количество атомарных выходных данных не равно 1000. Это означает, что метод getAndIncrement возвращает DUPLICATE.

Кто-нибудь объяснит, почему? Спасибо.

*ПРИМЕЧАНИЕ. В функции запуска. Если я использую System.out.println(next); Иногда я также вижу некоторые отсутствующие секвенсоры. *

ОБРАЗЕЦ РЕЗУЛЬТАТА 1:

1 6 8 5 2 10 3 4 12 11 9 14 15 7 13 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 42 41 43 45 44 47 49 46 48 52 52 50 53 55 54 58 56 57 59 60 61 62 63 64 65 67 68 69 66 70 71 72 73 74 75 76 77 78 80 81 79 82 83 84 85 86 87 88 89 90 91 92 93 94 95 97 96 98 99 104 103 106 102 109 100 101 108 107 111 105 112 114 110 117 116 119 115 120 113 118 121 122 126 125 124 123 127 128 130 131 129 132 133 134 135 136 137 138 139 140 141 142 144 145 143 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 170 169 171 172 173 174 175 176 177 178 179 180 181 182 186 188 189 185 190 184 191 192 183 187 193 194 196 197 198 199 195 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 216 217 219 215 218 220 232 231 230 229 228 227 226 225 224 223 221 222 233 234 235 237 239 240 236 238 241 243 242 245 244 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 285 284 283 289 288 287 286 292 291 293 294 290 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 320 321 319 322 323 324 325 326 327 328 330 329 331 332 333 334 335 337 336 338 339 340 341 343 344 345 346 347 348 342 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 366 365 364 368 367 369 370 371 372 373 375 374 379 378 377 376 380 381 382 383 384 385 386 389 388 387 392 394 395 391 397 390 399 398 400 396 393 403 404 402 401 406 405 407 408 409 410 411 412 413 414 415 416 417 419 418 420 421 423 422 424 425 426 427 429 430 428 434 433 435 432 431 436 437 440 439 445 446 447 438 458 459 460 457 456 462 463 464 455 454 453 470 452 473 475 451 450 477 479 480 449 482 448 444 442 509 443 441 511 510 508 507 506 515 516 518 519 520 505 504 522 503 502 525 532 501 500 499 498 497 496 495 535 494 493 538 492 491 490 541 543 489 546 488 548 549 487 486 485 484 550 552 483 481 478 556 557 476 558 474 472 471 469 468 567 467 569 570 466 465 461 574 573 575 576 572 571 568 566 565 564 563 562 561 560 559 555 554 553 551 547 545 544 540 578 585 542 539 537 536 595 534 597 533 531 599 530 529 528 600 602 527 526 604 524 523 521 608 519 517 514 513 614 512 613 612 616 617 611 610 619 609 621 607 606 622 623 605 603 601 598 625 627 596 594 631 593 592 632 633 591 590 638 589 588 587 640 641 586 584 583 582 648 651 653 655 657 581 658 659 662 664 580 668 579 669 577 667 666 672 665 673 674 675 663 661 676 677 680 683 685 687 689 661 660 656 654 692 652 650 649 646 647 703 645 644 643 642 639 637 636 635 634 630 713 629 628 626 715 624 717 620 720 722 618 615 721 723 719 718 716 714 712 711 710 709 708 728 729 707 706 705 704 702 701 700 699 698 697 696 695 694 693 691 690 688 686 684 730 731 732 733 682 681 679 678 671 670 734 735 727 739 726 725 724 738 740 742 737 744 745 736 746 747 743 741 749 748 751 750 753 752 754 755 756 757 758 759 760 761 762 763 765 764 770 771 772 773 774 769 768 776 767 780 766 779 778 777 775 784 783 782 781 787 788 790 791 792 796 786 798 799 800 785 803 805 807 802 801 797 795 794 793 812 789 813 811 816 810 809 808 806 819 821 823 824 826 827 828 830 831 832 839 840 842 843 844 846 847 848 849 850 851 853 855 804 856 854 852 858 848 860 861 845 841 838 837 836 835 834 864 865 833 829 825 822 868 869 820 818 871 817 874 875 815 814 876 877 878 879 880 881 882 883 885 887 888 889 891 892 894 896 898 901 902 903 905 906 875 873 872 870 867 866 863 862 859 857 904 900 899 897 895 893 907 890 908 886 884 909 910 911 912 913 914 927 928 926 929 931 932 934 938 939 940 942 925 944 945 924 923 922 948 921 949 920 919 918 915 916 917 951 950 947 946 943 941 937 936 935 952 953 933 930 954 955 956 957 958 959 960 961 962 963 964 965 966 968 969 970 971 972 985 986 987 967 989 988 990 985 984 991 983 982 993 981 980 979 978 977 976 975 974 973 995 995 992


person Loc    schedule 02.01.2014    source источник
comment
Можете ли вы сделать такой вывод, который не содержит 1000?   -  person Sotirios Delimanolis    schedule 03.01.2014
comment
Создание 1000 потоков в тесном цикле может серьезно нагрузить JVM. Гораздо более вероятно, что поток либо не завершился, либо вы что-то пропустили в выводе, чем то, что AtomicInteger не работает должным образом.   -  person Ted Hopp    schedule 03.01.2014
comment
Мы видим 1000 в вашем выводе.   -  person Alexis C.    schedule 03.01.2014
comment
Кажется гораздо более вероятным, что именно System.out.println на вашей платформе не любит 1000 потоков, пытающихся использовать его одновременно, чем огромная ошибка в модели памяти Java, которую никто не нашел до тех пор, пока теперь... пробовали ли вы накапливать результаты в потокобезопасной коллекции, а затем синхронно распечатывать их?   -  person Affe    schedule 03.01.2014
comment
@ZouZou: Пожалуйста, посмотрите мое обновление. Это случается когда-нибудь.   -  person Loc    schedule 03.01.2014
comment
@Sotirios Delimanolis: Вы видите мое последнее обновление?   -  person Loc    schedule 03.01.2014
comment
Ваш код в том виде, в котором он опубликован, ошибочен. Вам нужно использовать синхронизированный набор, чтобы безопасно изменять его из нескольких потоков. Вам также необходимо дождаться завершения всех потоков, прежде чем тестировать содержимое набора. Чтобы разработать правильный тест, рассмотрите возможность использования службы-исполнителя, где вы можете дождаться завершения всех потоков.   -  person Ted Hopp    schedule 03.01.2014
comment
Пожалуйста, НЕ редактируйте код в вопросе таким образом - вы изменили характер вопроса, закомментировав одну строку.   -  person Robin Green    schedule 03.01.2014
comment
@Робин Грин. Я обновил его до исходного вопроса. Извините всех. Спасибо.   -  person Loc    schedule 03.01.2014


Ответы (6)


Одна из основных проблем с вашим кодом заключается в том, что вы добавляете числа в HashSet, который не является потокобезопасным. Это причина того, что вы наблюдаете. Если вы использовали потокобезопасную коллекцию, вы получите ожидаемый результат:

final Set<Integer> integers = new ConcurrentSkipListSet<>();

Но это вводит дополнительный уровень синхронизации. Другой альтернативой является использование простого массива int[] numbers = new int[1000]; и использование: numbers[next-1]++; для подсчета вхождений.

Новый код для справки:

public static void main(String[] args) throws Exception {

    final AtomicInteger sequencer = new AtomicInteger(1);
    final int[] integers = new int[1000];

    final Runnable task = new Runnable() {

        @Override
        public void run() {
            int next = sequencer.getAndIncrement();
            integers[next-1]++;
        }
    };
    List<Thread> threads = new ArrayList<>();
    for (int i = 0; i < 1000; i++) {
        Thread t = new Thread(task);
        t.start();
        threads.add(t);
    }
    for (Thread t : threads) {
        t.join();
    }
    for (int i = 0; i < 1000; i++) {
        if (integers[i] != 1) System.out.println(i + " -> " + integers[i]);
    }
}
person assylias    schedule 02.01.2014
comment
Я использовал ConcurrentSkipListSet и повторил тест. Я была такая же проблема. ( Когда-то). - person Loc; 03.01.2014
comment
@Loc - Вы гарантируете, что все потоки завершатся перед проверкой результата (как это делает assylias во втором цикле)? - person Ted Hopp; 03.01.2014
comment
@Loc HashSet (или, скорее, HashMap, который его поддерживает) начинается с размера по умолчанию 16 и удваивается при необходимости. Итак, 16, 32, 64, 128, 256, 512, 1024 и т. д. 1000 должно быть потеряно при изменении размера. - person Sotirios Delimanolis; 03.01.2014

Если я запускаю вашу программу на своем компьютере, я наблюдаю неправильный вывод примерно в 1 из 10 раз. (Это с эмулятором консоли в Netbeans 7.)

Однако, если я изменю его, чтобы проверить размер набора после завершения всех потоков, он всегда будет содержать 1000 уникальных целых чисел. Это справедливо даже в том случае, если в выходных данных, генерируемых отдельными потоками во время их выполнения, есть ошибки. (То есть в строках, напечатанных потоками, печатаются дубликаты, но истинный размер набора после завершения выполнения на самом деле 1000)

Поэтому я прихожу к выводу, что проблема заключается в том, что 1000 потоков, записывающих все в PrintStream, который ваша платформа предоставляет для System.out, искажают вывод. На моей платформе я вижу поврежденный вывод во время выполнения, но в конце всегда есть 1000 уникальных целых чисел.

public static void main(String[] args) throws InterruptedException {

    final AtomicInteger sequencer = new AtomicInteger(1);
    final Set<Integer> integers = new ConcurrentSkipListSet<Integer>();

    final Runnable task = new Runnable() {

        @Override
        public void run() {
            int next = sequencer.getAndIncrement();
            integers.add(next);
            System.out.println(next);
        }
    };
    List<Thread> threads = new ArrayList<Thread>(1000);
    for (int i = 0; i < 1000; i++) {
        Thread t = new Thread(task);
        t.start();
        threads.add(t);
    }

    for (Thread t : threads) {
        t.join();
    }
    System.out.println(integers.size());
}
person Affe    schedule 02.01.2014
comment
@SotiriosDelimanolis Я перепроверил размер набора целых чисел после завершения всех потоков и убедился, что в нем есть 1000 уникальных целых чисел, чтобы подтвердить мое утверждение, что проблема заключается в том, что реализация PrintStream, установленная на System.out, не обрабатывает 1000 потоков. писать его одновременно и получать повреждения. - person Affe; 03.01.2014
comment
@SotiriosDelimanolis переформулировал это, чтобы попытаться сказать это более ясно :) - person Affe; 03.01.2014
comment
Ага, это тоже. Печать должна выполняться внутри синхронизированного метода. Хороший улов. - person David Tonhofer; 03.01.2014

AtomicInteger вызывает класс Unsafe, который, в свою очередь, выполняет собственные вызовы. Так что да, AtomicInteger теоретически может возвращать неатомарные обновления. Это будет зависеть от JVM и базовой архитектуры, на которой она работает.

Однако, учитывая характер потоков, я всегда предполагаю, что ошибка в моем коде, а не в JVM.

Я запустил этот код и не смог обнаружить никаких столкновений. JDK 1_7_45 победа 7

  public static void main(String[] args) {

      final AtomicInteger sequencer = new AtomicInteger(1);
      final Set<Integer> integers = new HashSet<Integer>();

      final Runnable task = new Runnable() {

          @Override
          public void run() {
              int next = sequencer.getAndIncrement();
              synchronized (integers){
                  if(integers.contains(next)){
                      System.out.println("duplicate detected " + next);
                  }
                  integers.add(next);
              }
          }
      };


      for(int j = 0; j < 1000; j++){
          System.out.print("testing " + j +" ");
          sequencer.set(0);
          integers.clear();
          List<Thread> threads = new ArrayList<Thread>(10000);
          for (int i = 0; i < 1000; i++) {
              Thread t = new Thread(task);
              threads.add(t);
              t.start();
          }
          for (Thread t : threads) {
              try {
                  t.join();
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
          }
          System.out.println("integers size " + integers.size());
      }
  }
person BevynQ    schedule 02.01.2014

Как говорили другие:

  1. Дождитесь завершения всех потоков в конце цикла, используя Thread.join(). Ссылки на все потоки должны быть сохранены в коллекции, конечно
  2. close() STDOUT перед выходом

Вокруг программы:

  1. Направьте вывод через sort и/или wc -l
  2. Используйте diff для проверки списка чисел, созданного простым циклом
  3. Выход с предупреждением, если diff находит разницу

Повторите 10 000 раз в запрограммированном цикле.

Доложить.

person David Tonhofer    schedule 02.01.2014
comment
Я повторно обновил свой код и протестировал 1000 потоков. Как я уже сказал, он произвел неправильный вывод. (Редко бывает) - person Loc; 03.01.2014
comment
@Loc - вы не ждете завершения всех потоков. Это очень важно для правильного теста. - person Ted Hopp; 03.01.2014

Обновленный вывод содержит 7 целых чисел, напечатанных дважды.

(взял ваш вывод | sort | uniq --count | sort )

печатает это в конце (первый столбец - это количество просмотров, 2-й столбец - это значение)

  1 991
  1 992
  1 993
  2 519
  2 52
  2 661
  2 848
  2 875
  2 985
  2 995

Это связано с тем, что вы печатаете размер HashSet, который не является потокобезопасным, поэтому иногда размер не показывает обновленное значение.

Переход на поточно-ориентированную реализацию Set должен работать.

person dkatzel    schedule 02.01.2014

Для меня это из-за ConcurrentSkipListSet.size(). Этот тип слабо согласованной коллекции, хотя и потокобезопасный, не является точным в отношении итераций или подсчета в целом.

Из Javadoc ConcurrentSkipListSet.size()

Имейте в виду, что, в отличие от большинства коллекций, метод size не является операцией с постоянным временем. Из-за асинхронного характера этих наборов определение текущего количества элементов требует обхода элементов. Кроме того, не гарантируется атомарное выполнение массовых операций addAll, removeAll, keepAll и containsAll. Например, итератор, работающий одновременно с операцией addAll, может просматривать только некоторые добавленные элементы.

person Pitiphan    schedule 02.01.2014