Пятничная история про synchronized-методы в классе Thread

в 20:52, , рубрики: concurrency, java, метки: ,

Недавно я наткнулся на интересную особенность синхронизации на классе Thread. Я создал класс, наследующий Thread и использовал в нем паттерн lock object для внутренней синхронизации. В какой-то момент мне показалось что lock object раздувает код, а поскольку созданный поток я использую из очень малого числа мест, я его выкинул, заменив на synchronized методы и wait/notify на this. Если хотите знать, что из этого маленького нарушения «кодекса» вышло — добро пожаловать под кат.

В-общем ничего хорошего не вышло. Программа работала нормально почти всегда, но на CI периодически начала вести себя некорректно.
Беглый просмотр класса Thread показал, что механизм java synchronization используется в самом классе Thread, а именно в методе join:

    public final synchronized void join(long millis) 
    throws InterruptedException {
	long base = System.currentTimeMillis();
	long now = 0;

	if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
	}

	if (millis == 0) {
	    while (isAlive()) {
		wait(0);
	    }
	} else {
	    while (isAlive()) {
		long delay = millis - now;
		if (delay <= 0) {
		    break;
		}
		wait(delay);
		now = System.currentTimeMillis() - base;
	    }
	}
    }

Собственно сам notify() в классе Thread не вызывается, а вызывается в cpp-шном коде после завершения потока:

static void ensure_join(JavaThread* thread) {
  // We do not need to grap the Threads_lock, since we are operating on ourself.
  Handle threadObj(thread, thread->threadObj());
  assert(threadObj.not_null(), "java thread object must exist");
  ObjectLocker lock(threadObj, thread);
  // Ignore pending exception (ThreadDeath), since we are exiting anyway
  thread->clear_pending_exception();
  // It is of profound importance that we set the stillborn bit and reset the thread object,
  // before we do the notify. Since, changing these two variable will make JVM_IsAlive return
  // false. So in case another thread is doing a join on this thread , it will detect that the thread
  // is dead when it gets notified.
  java_lang_Thread::set_stillborn(threadObj());
  // Thread is exiting. So set thread_status field in  java.lang.Thread class to TERMINATED.
  java_lang_Thread::set_thread_status(threadObj(), java_lang_Thread::TERMINATED);
  java_lang_Thread::set_thread(threadObj(), NULL);
  lock.notify_all(thread); 
  // Ignore pending exception (ThreadDeath), since we are exiting anyway
  thread->clear_pending_exception();
}

Таким образом, создается две проблемы — делая notify() вы, если не повезет, пробуждаете поток join, что не страшно, но не пробуждаете свой поток, что страшно.
И второе, после завершения потока, на нем вызывается notifyAll() что разбудит все ваши wait(), хотя вы очевидно этого не хотели бы.

В-общем мораль пятничной истории банальна — используйте lock object, если есть хотя бы потенциальная возможность что на объекты вашего класса будет синхронизоваться кто-то кроме Вас. А если вы наследуетесь от чужого класса, который не Object, то такая вероятность точно есть.

Автор: andll

Источник


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js