2015-04-26 5 views
4

Я начинаю с очень простого примера в многопоточности. Я пытаюсь сделать потолочный счетчик. Я хочу, чтобы создать два потока, что приращение счетчика периодически достигнет 1000. Код ниже:Java многопоточность - счетчик потоков

public class ThreadsExample implements Runnable { 
    static int counter = 1; // a global counter 

    public ThreadsExample() { 
    } 

    static synchronized void incrementCounter() { 
      System.out.println(Thread.currentThread().getName() + ": " + counter); 
      counter++; 
    } 

    @Override 
    public void run() { 
      while(counter<1000){ 
       incrementCounter(); 
      } 
    } 

    public static void main(String[] args) { 
      ThreadsExample te = new ThreadsExample(); 
      Thread thread1 = new Thread(te); 
      Thread thread2 = new Thread(te); 

      thread1.start(); 
      thread2.start();   
    } 
} 

Из того, что я могу сказать, петля в то время как сейчас означает, что только первый поток имеет доступ к счетчику, пока он не достигнет 1000. Выход:

Thread-0: 1 
. 
. 
. 
Thread-0: 999 
Thread-1: 1000 

Как это исправить? Как я могу получить потоки для обмена счетчиком?

+0

Почему, по-вашему, цикл while означает это? – RealSkeptic

+2

Ничего не нужно «исправлять». Переключение между потоками является дорогостоящим, и планировщик ** не ** чередуется, чтобы потоки выполнялись для одиночных вызовов 'incrementCounter'. Увеличьте переменную цикла от 1000 до, скажем, 1000000, тогда вы увидите, что потоки действительно * делают * изменяются между ними. – Marco13

+1

@ Marco13 Хотя это примитивный пример, и нет никакого смысла переключаться между потоками, существуют случаи, когда короткие, но важные задачи, подобные этому, не выполняются из-за того, что один поток забивает монитор, вызывая головокружение нити, что потенциально останавливает прогресс. В этом случае, безусловно, есть что-то, что можно «исправить», реализуя политику справедливости. – initramfs

ответ

10

Оба потока имеют доступ к вашей переменной.

Явление, которое вы видите, называется потоком голода. После ввода защищенной части вашего кода (извините, что я пропустил это ранее), другие потоки должны будут блокироваться до тех пор, пока не будет выполнена нить, удерживающая монитор (т. Е. Когда выдается монитор). Хотя можно ожидать, что текущая нить пройдет монитор до следующего потока, ожидающего очереди, для синхронизированных блоков java не гарантирует никакой политики справедливости или порядка, к которой поток затем получает монитор. Это вполне возможно (и даже возможно) для потока, который выпускает и пытается повторно захватить монитор, чтобы схватить его за другой поток, который ждал некоторое время.

От Oracle:

Голодание описывает ситуацию, когда поток не в состоянии получить постоянный доступ к общим ресурсам и не в состоянии добиться прогресса. Это происходит, когда общие ресурсы становятся недоступными в течение длительного времени «жадными» потоками. Например, предположим, что объект предоставляет синхронизированный метод, который часто требует много времени для возврата. Если один поток часто вызывает этот метод, часто блокируются другие потоки, которые также нуждаются в частом синхронном доступе к одному и тому же объекту.

Хотя ваши потоки являются примерами «жадных» нитей (поскольку они многократно выпускают и снова захватывают монитор), сначала начинается нить-0, поэтому голодает нить-1.

Решение состоит в том, чтобы использовать параллельный метод синхронизации, который поддерживает справедливость (например ReentrantLock), как показано ниже:

public class ThreadsExample implements Runnable { 
    static int counter = 1; // a global counter 

    static ReentrantLock counterLock = new ReentrantLock(true); // enable fairness policy 

    static void incrementCounter(){ 
     counterLock.lock(); 

     // Always good practice to enclose locks in a try-finally block 
     try{ 
      System.out.println(Thread.currentThread().getName() + ": " + counter); 
      counter++; 
     }finally{ 
      counterLock.unlock(); 
     } 
    } 

    @Override 
    public void run() { 
     while(counter<1000){ 
      incrementCounter(); 
     } 
    } 

    public static void main(String[] args) { 
     ThreadsExample te = new ThreadsExample(); 
     Thread thread1 = new Thread(te); 
     Thread thread2 = new Thread(te); 

     thread1.start(); 
     thread2.start();   
    } 
} 

примечание удаление synchronized ключевого слова в пользу ReentrantLock в рамках метода. Такая система, с политикой справедливости, позволяет долго ждать потоков, чтобы выполнить, устраняя голод.

0

пытаются спать нить, чтобы гарантировать, что другой будет идти:

 @Override 
    public void run() { 
      while(counter<1000){ 
       incrementCounter(); 
       Thread.sleep(1); 
      } 
    } 
+0

Вызов 'sleep()' ОК для демонстраций и экспериментов, но если вы когда-либо называете sleep() в реальной программе, то вы, вероятно, либо заново изобретаете 'ScheduledThreadPoolExecutor', либо ошибаетесь. –

7

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

public class ThreadsExample implements Runnable { 
    static AtomicInteger counter = new AtomicInteger(1); // a global counter 

    public ThreadsExample() { 
    } 

    static void incrementCounter() { 
      System.out.println(Thread.currentThread().getName() + ": " + counter.getAndIncrement()); 
    } 

    @Override 
    public void run() { 
      while(counter.get() < 1000){ 
       incrementCounter(); 
      } 
    } 

    public static void main(String[] args) { 
      ThreadsExample te = new ThreadsExample(); 
      Thread thread1 = new Thread(te); 
      Thread thread2 = new Thread(te); 

      thread1.start(); 
      thread2.start();   
    } 
} 
1

Ну, с вашим кодом я не знаю, как получить «точно» с перерывами, но если вы используете Thread.yield() после звонка incrementCounter(), у вас будет лучшее распределение.

public void run() { 
     while(counter<1000){ 
       incrementCounter(); 
       Thread.yield(); 

     } 
    } 

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

public class SharedVariable { 
    private int value; 
    private boolean turn; //false = ThreadsExample1 --> true = ThreadsExample2 

    public SharedVariable(){ 
     this.value = 0; 
     this.turn = false; 
    } 

    public void set (int v){ 
     this.value = v; 
    } 

    public int get(){ 
     return this.value; 
    } 

    public void inc(){ 
     this.value++; 
    } 

    public void shiftTurn(){ 
     if (this.turn){ 
      this.turn=false; 
     }else{ 
      this.turn=true; 
     } 
    } 

    public boolean getTurn(){ 
     return this.turn; 
    } 

} 

Теперь главное может быть:

public static void main(String[] args) { 
     SharedVariable vCom = new SharedVariable(); 
     ThreadsExample1 hThread1 = new ThreadsExample1 (vCom); 
     ThreadsExample2 hThread2 = new ThreadsExample2 (vCom); 

     hThread1.start(); 
     hThread2.start(); 

     try { 
      hThread1.join(); 
      hThread2.join(); 
     } catch (InterruptedException e) { 
      e.printStackTrace(); 
     } 

    } 

И вы должны изменить свою линию static int counter = 1; // a global counter для private SharedVariable counter;

И новый пробег:

public void run() { 
    for (int i = 0; i < 20; i++) { 
     while (!counter.getTurno()){ 
      Thread.yield(); 
     } 
     System.out.println(this.counter.get()); 
     this.counter.cambioTurno(); 
    } 
} 

}

Да, это еще один код, но я думаю, что это может помочь вам немного.