Как реализовать счетчик объектов в Java

Интервьюер спросил меня, что

Как вы можете реализовать класс Foo, где вы сможете рассчитывать экземпляры этого класса. Есть больше потоков, которые создают экземпляр этого класса Foo.

Я ответил, что со следующим кодом

public class Foo {
    private static int count = 0;

    public Foo() {
    incrementCount();
    }

    public void incrementCount() {
        synchronize (Foo.class) {
            count++;
        }
    }
} 

Она снова спросила меня, что

Если поток заканчивается, счетчик должен быть декрементом, как вы можете это сделать?

Я не ответил на этот вопрос.

Я знаю о методе finalize(), но зависит от Garbage collector, что при вызове этого метода, даже если мы переопределим finalize().

У меня пока нет решения, можете ли вы это объяснить?

Ответ 1

Вы можете обернуть Thread Runnable внутри другого Runnable, который уменьшит счетчик:

Thread createThread(final Runnable r) {
  return new Thread(new Runnable() {
    @Override public void run() {
      try {
        r.run();
      } finally {
        Foo.decrementCounter();
      }
    }
  });
}

Проблема с этим заключается в том, что Runnable r создает несколько экземпляров Foo. Вам нужно каким-то образом отслеживать количество экземпляров, созданных потоком. Вы можете сделать это с помощью ThreadLocal<Integer>, а затем вызвать decrementCounter() в блоке finally соответствующее количество раз. Ниже приведен полный рабочий пример.

Если вы можете избежать этого, вы не должны полагаться на поведение GC, поскольку это довольно непредсказуемо! Если вы настаиваете на том, чтобы иметь дело с Мусороуборочным комбайном, тогда вы должны использовать ссылочные очереди - и использовать его правильно, вы должны изучить концепцию достижимости объекта: http://docs.oracle.com/javase/7/docs/api/index.html?java/lang/ref/package-summary.html

Как последнее замечание, если бы я брал интервью у вас, я попытался бы понять, что предлагаемый вами код не полностью удовлетворяет требованиям: вам нужно будет сделать класс final или метод incrementCount() final или private. Или, проще говоря, вы можете увеличить счетчик в блоке инициализатора экземпляра: не нужно думать о переопределении методов в подклассах или новых добавленных конструкторах, не увеличивая счет.


Полный пример:

public class Foo {
  private static final AtomicInteger liveInstances = new AtomicInteger(0);
  private static final ThreadLocal<Integer> threadLocalLiveInstances = new ThreadLocal<Integer>() {
    @Override protected Integer initialValue() { return 0; }
  }

  // instance initializer (so you won't have problems with multiple constructors or virtual methods called from them):
  {
    liveInstances.incrementAndGet();
    threadLocalLiveInstances.set(threadLocalLiveInstances.get() + 1);
  }

  public static int getTotalLiveInstances() {
    return liveInstances.get();
  }

  public static int getThreadLocalLiveInstances() {
    return threadLocalLiveInstances.get();
  }

  public static void decrementInstanceCount() {
    threadLocalLiveInstances.set(threadLocalLiveInstances.get() - 1);
    liveInstaces.decrementAndGet();
  }

  // ... rest of the code of the class ...
}

class FooCountingThreadFactory implements ThreadFactory {
  public Thread newThread(final Runnable r) {
    return new Thread(new Runnable() {
      @Override public void run() {
        try {
          r.run();
        } finally {
          while (Foo.getThreadLocalLiveInstances() > 0) {
            Foo.decrementInstanceCount();
          }
        }
      }
    });
  }
}

Таким образом, вы можете подавать этот ThreadFactory в пул потоков, например, или вы можете использовать его самостоятельно, когда хотите построить поток: (new FooCountingThreadFactory()).newThread(job);

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

Ответ 2

Выполняя то же самое в обратном порядке.

Так как Sun (Oracle) устарел на небезопасные методы убийства потоков (Почему Thread... устарел?), ваш поток "выходит" возвращаясь из своего метода run().

Просто создайте метод decrementCount() в своем классе Foo и обязательно вызовите его перед возвратом из run() в своем потоке.

Так как в Java нет деструкторов, и, как вы указываете, finalize() полагается на GC... нет действительно автоматического способа сделать это. Единственный другой вариант, о котором я мог подумать, - создать/использовать пул, но немного отличающийся.

Ответ 3

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

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

Таким образом, счетчик ссылок уменьшается, когда сборщик мусора выполнил свою работу.