当前位置: 首页 > 面试题库 >

Java多线程-线程安全计数器

巴照
2023-03-14
问题内容

我从一个非常简单的多线程示例开始。我试图做一个线程安全的计数器。我想创建两个线程,使计数器间歇地增加到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
的关键字。这种具有公平性策略的系统允许长时间等待的线程有机会执行,从而消除了饥饿。



 类似资料:
  • 我找到了关于线程安全的代码,但它没有来自给出示例的人的任何解释。我想知道为什么如果我不在“count”之前设置“synchronized”变量,那么count值将是非原子的(总是=200是期望的结果)。谢谢

  • 如果有多个Java线程同时写入同一个套接字实例,这会影响从同一个套接字读取的对象的完整性吗?例如,对象的内容是否会被弄乱等。对象的顺序可以是随机的。

  • 问题内容: 鉴于以下多态: 我们如何在没有昂贵的getInstance()方法同步和双重检查锁定争议的情况下使它保持线程安全和懒惰?这里提到了单例的有效方法,但似乎并没有扩展到多例。 问题答案: 使用Java 8,它甚至可以更简单:

  • 问题内容: 在阅读了“ 实践中的Java并发 ”和“ 实践OSGI ”之后,我发现了一个非常有趣的特定主题。安全发布。以下是来自JCIP的内容: 为了安全地发布对象,必须同时使对该对象的引用和该对象的状态对其他线程可见。可以通过以下方式安全地发布正确构造的对象: 从静态初始化程序初始化对象引用。 将对它的引用存储到可变字段中。 将对它的引用存储到最终字段中。 将对它的引用存储到由(同步)锁适当保护

  • 我有一个应用程序,它有一个ConcurrentHashMap本地存储一个存储在外部服务器上的数据副本。地图每隔几秒钟就会更新一次数据的新副本。 我有一个循环,每隔几秒钟运行一次,它可以访问HashMap并按照值的顺序将元素添加到数组中(实际上它做的事情还多一些,但这并不相关)。我的问题是,如果数据在创建数组的过程中发生了变化,您可能会在不同的地方有重复的键,或者完全省略一些键。 示例: 如您所见,

  • 问题内容: 如果我有多个Java线程同时写入同一Socket实例,这会影响从同一套接字读取的对象的完整性吗?即,对象的内容是否会被弄乱等等。对象的顺序可以是随机的。 问题答案: 通常,没有任何保证。一点点不同的对象很可能最终会在电线上交错,使结果难以辨认。 因此,您需要提供外部同步。 有趣的是,即使在OS级别进行单个套接字写入也不一定是原子操作。有关进一步的讨论,请参见注意sendmsg()系列函