Java Многопоточность-Threadsafe Счетчик
Я начинаю с очень простого примера многопоточности. Я пытаюсь сделать резьбозащитный счетчик. Я хочу создать два потока, которые периодически увеличивают счетчик до 1000. Код ниже:
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();
}
}
Из того, что я могу сказать, цикл while прямо сейчас означает, что только первый поток имеет доступ к счетчику, пока он не достигнет 1000. Вывод:
Thread-0: 1
.
.
.
Thread-0: 999
Thread-1: 1000
Как это исправить? Как я могу заставить потоки совместно использовать счетчик?
4 ответов:
Оба потока имеют доступ к вашей переменной.
Явление, которое вы видите, называется голоданием нитей. После ввода защищенной части вашего кода (извините, что я пропустил это ранее), другие потоки должны будут блокировать, пока поток, удерживающий монитор, не будет завершен (т. е. когда монитор будет освобожден). Хотя можно ожидать, что текущий поток передаст монитор следующему потоку, ожидающему в очереди, для синхронизированных блоков java не гарантирует никакой честности или политики упорядочения к какому потоку далее поступает монитор. это вполне возможно (и даже вероятно) для потока, который освобождает и пытается повторно получить монитор, чтобы получить его через другой поток, который ждал некоторое время.От Оракула:
Голодание описывает ситуацию, когда поток не может получить регулярный доступ к общим ресурсам и не может добиться прогресса. Это происходит, когда общие ресурсы становятся недоступными в течение длительного времени "жадными" нити. Например, предположим, что объект предоставляет синхронизированный метод, возврат которого часто занимает много времени. Если один поток часто вызывает этот метод, другие потоки, которые также нуждаются в частом синхронизированном доступе к тому же объекту, часто будут заблокированы.
Хотя оба ваших потока являются примерами "жадных" потоков (поскольку они неоднократно освобождают и повторно запрашивают монитор), поток-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 внутри метода. Такая система, с политикой справедливости, позволяет долго ждать нити шанс выполнить, снимая голодание.
Вы можете использовать
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(); } }
Ну, с вашим кодом я не знаю, как получить" точно " периодически, но если вы используете
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(); } }}
Да, это другой код, но я думаю, что он может немного помочь вам.
Попробуйте переспать поток, чтобы гарантировать, что другой будет иметь ход:
@Override public void run() { while(counter<1000){ incrementCounter(); Thread.sleep(1); } }
Comments