Два потока, одна и та же статическая переменная, одно и то же значение, параллельный доступ


я пытался подготовиться к SCJP экзамен, который я должен принять на следующей неделе, и я столкнулся с этим вопрос о Java Threads.

1-public class Stone implements Runnable {
2-  static int id = 1;
3-
4-  public void run() {
5-      try {
6-          id = 1 - id;
7-          if (id == 0) {
8-                      pick();
9-          } else {
10-             release();
11-         }
12-
13-     } catch (Exception e) {
14-     }
15- }
16-
17- private static synchronized void pick() throws Exception {
18-     System.out.print("P ");
19-     System.out.print("Q ");
20- }
21-
22- private synchronized void release() throws Exception {
23-     System.out.print("R ");
24-     System.out.print("S ");
25- }
26-
27- public static void main(String[] args) {
28-     Stone st = new Stone();
29-     new Thread(st).start();
30-     new Thread(st).start();
31- }
32-}
  • какие из них истинны? (Выберите все, что применимо.)
  • выход может быть P Q R s
  • выход может быть P R S Q
  • выход может быть P R Q s
  • выход может быть P Q p Q
  • программа может вызвать тупик.
  • компиляция завершается с ошибкой.

ключ ответа говорит:
A, B и C верны. Поскольку pick () статичен, а release () нестатичен, там два замка. Если pick () был нестатическим, только A будет правильным.

Он также говорит, что выход P Q P Q на самом деле не вариант, и получить такие результаты невозможно.

Сначала я не очень верил в ключ ответа, но потом увидел что на самом деле невозможно увидеть этот вывод в результате этого приложения. (После запуска класса.)

теперь, это часть, которая меня немного смущает, и вот почему

Я думал, что p Q P Q или R S R S результат должен быть возможен. Потому что всегда есть шанс для ситуации, которая делает идентификатор переменной точно таким же для обоих потоков. Другими словами, например, когда первый поток только что закончил выполнение строки 6, он мог отдать свою очередь другому, и после этого можно изменить значение переменной ID и вуаля! Они могли бы пойти в то же самое, если блок счастливо.

Я пытался увидеть эту ситуацию снова и снова (с Eclipse Juno и Java 7). Этого просто не бывает. Я уверен, что с моим мышлением что-то не так, и мне интересно, что именно. мне нужно знать, какое правило мешает этим двум потокам получить доступ к переменная id в том же состоянии.

5 ответов


на самом деле, есть довольно много возможностей, некоторые крайне маловероятны, но они все еще возможны, тем не менее, и после 1 миллиона казней это то, что я нашел.

код:

public class Stone implements Runnable {
    static int id = 1;
    static StringBuffer buffer = new StringBuffer();

    public void run() {
        try {
            id = 1 - id;
            if (id == 0) {
                pick();
            } else {
                release();
            }

        } catch (Exception e) {
        }
    }

    private static synchronized void pick() throws Exception {
        buffer.append("P ");
        buffer.append("Q ");
    }

    private synchronized void release() throws Exception {
        buffer.append("R ");
        buffer.append("S ");
    }

    public static void main(String[] args) {
        int count = 1000000;
        Map<String, Integer> results = new HashMap<String, Integer>();
        System.out.println("Running " + count + " times...");
        for (int i = 0; i< count; i++) {
            buffer = new StringBuffer();
            Stone stone = new Stone();
            Thread t1 = new Thread(stone);
            Thread t2 = new Thread(stone);
            t1.start();
            t2.start();
            while (t1.isAlive() || t2.isAlive()) {
                // wait
            }
            String result = buffer.toString();
            Integer x = results.get(result);
            if (x == null) x = 0;
            results.put(result, x + 1);
            if (i > 0 && i % 50000 == 0) System.out.println(i + "... " + results.keySet());
        }
        System.out.println("done, results were:");
        for (String key : results.keySet()) {
            System.out.println(" " + key + ": " + results.get(key));
        }
    }
}

результаты:

Running 1000000 times...
50000... [R S P Q , P Q R S , P R S Q , R P Q S ]
100000... [R S P Q , P Q R S , P R S Q , R P Q S ]
150000... [R S P Q , P Q R S , P R S Q , R P Q S ]
200000... [R S P Q , P Q R S , P R S Q , R P Q S ]
250000... [R S P Q , P Q R S , P R S Q , R P Q S ]
300000... [R S P Q , P Q R S , P R S Q , R P Q S ]
350000... [R S P Q , P Q R S , P R S Q , P R Q S , R P Q S ]
400000... [R S P Q , P Q R S , P R S Q , P R Q S , R P Q S ]
450000... [R S P Q , P Q R S , P R S Q , P R Q S , R P Q S ]
500000... [R S P Q , P Q R S , P R S Q , P R Q S , R P Q S ]
550000... [R S P Q , P Q R S , P R S Q , P R Q S , R P Q S ]
600000... [R S P Q , P Q R S , P R S Q , P R Q S , R P Q S ]
650000... [R S P Q , P Q R S , P R S Q , P R Q S , R P Q S ]
700000... [R S P Q , P Q R S , P R S Q , P R Q S , R P Q S ]
750000... [R S P Q , P Q R S , P R S Q , P R Q S , R P Q S ]
800000... [R S P Q , P Q R S , P R S Q , P R Q S , R P Q S ]
850000... [R S P Q , P Q R S , P R S Q , P R Q S , R P Q S ]
900000... [R S P Q , P Q R S , P R S Q , P R Q S , R P Q S ]
950000... [P Q P Q , R S P Q , P Q R S , P R S Q , P R Q S , R P Q S ]
done, results were:
P Q P Q : 1
R S P Q : 60499
P Q R S : 939460
P R S Q : 23
P R Q S : 2
R P Q S : 15

Я думаю, мы доказали, что P Q P Q действительно возможно, хотя при чрезвычайно низкой вероятности вокруг один на миллион...

[edit: другой запуск, другие результаты, показывающие R S R S тоже возможно:]

done, results were:
 R S R S : 1
 R P S Q : 2
 P Q P Q : 1
 R S P Q : 445102
 P Q R S : 554877
 P R S Q : 5
 P R Q S : 2
 R P Q S : 10

Да, вы правы, P Q P Q возможно.

вы можете увеличить вероятность этого события следующие изменения (это не влияет на семантику программы):

public class Stone implements Runnable {
    static int id = 1;
    static CyclicBarrier b = new CyclicBarrier(2);

    public void run() {
        try {
            b.await(); // Increase probability of concurrent execution of subsequent actions

            int t = id;

            Thread.yield(); // Increase probability of thread switch at this point

            id = 1 - t;

            Thread.yield(); // Increase probability of thread switch at this point

            if (id == 0) {
                pick();
            } else {
                release();
            }
        } catch (Exception e) {}
    }
    ...
}

после применения этих модификаций я получил P Q P Q после нескольких десятков запусков.


Да ваше подозрение верно. Однако код в run () метод достаточно прост, чтобы быть выполненным в одном пакете CPU, если не ждали каким-то другим способом.


вы правы в своем предположении. P Q P Q действительно возможно, потому что спецификация JLS 17.4.3 указано следующее:

среди всех действий между потоками, выполняемых каждым потоком t, порядок программы t является общим порядком, который отражает порядок, в котором эти действия будут выполняться в соответствии с семантикой внутри потока t.

набор действий последовательно согласован, если все действия происходят в общем порядке ( порядок выполнения), который согласуется с порядком программы, и, кроме того, каждое чтение r переменной v видит значение, записанное записью w в v, такое, что:

  • w идет перед r в порядке выполнения, и
  • нет другой записи w 'такой, что w предшествует w' и w ' предшествует r в порядке выполнения.

последовательная последовательность очень сильная гарантия которая сделана о видимости и приказывать в исполнении программа. В рамках последовательного последовательного выполнения существует общий порядок над всеми отдельными действиями (такими как чтение и запись), который согласуется с порядком программы, и каждое отдельное действие является атомарным и сразу видно каждому потоку.

AtomicIntegerбыло бы лучше кандидатов, чтобы избежать этой ситуации.


когда первый поток только что закончил выполнение строки 6, он может отказаться от своей очереди к другому, и после этого другой может изменить значение переменной id, а затем вуаля! Они могли бы пойти в то же самое, если блок счастливо.

предположим, поток 1 запускается первым. Он переворачивает значение id в 0. Поток 1 теперь приостановлен в строке 8.

запускается поток 2. Он либо видит значение id

  • 1

    все потоки разрешено кэшировать поля локально, если они не помечены как volatile. Поток 2 кэширует значение id.

    запускается поток 2. Он меняет значение на 0. И они оба входят в первый блок if. Если бы нить 1 была подвешена на линии 7. Результаты могут быть разными.

    возможно, что выход P Q P Q

  • 0

    Это видит перевернутое значение из потока 1

    снова изменяет значение на 1. Входит в блок else.

    случай вариантов A, B, C

даже не гарантируется, что поток 1 запускается до потока 2.