我从一个非常简单的多线程示例开始。我试图做一个线程安全的计数器。我想创建两个线程,使计数器间歇地增加到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
我该如何解决?如何获得共享计数器的线程?
两个线程都可以访问您的变量。
您看到的现象称为线程饥饿。输入代码的受保护部分后(很抱歉,我之前错过了它),其他线程将需要阻塞,直到持有监视器的线程完成(即, 释放 监视器时)。尽管可能希望当前线程将监视器传递给等待排队的下一个线程,但是对于同步块,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 的关键字。这种具有公平性策略的系统允许长时间等待的线程有机会执行,从而消除了饥饿。
synchronized