diff a/src/hotspot/share/runtime/synchronizer.cpp b/src/hotspot/share/runtime/synchronizer.cpp
--- a/src/hotspot/share/runtime/synchronizer.cpp
+++ b/src/hotspot/share/runtime/synchronizer.cpp
@@ -609,10 +609,18 @@
 //  2) wait on lock2
 //  3) when notified on lock2, unlock lock2
 //  4) reenter lock1 with original recursion count
 //  5) lock lock2
 // NOTE: must use heavy weight monitor to handle complete_exit/reenter()
+// NOTE(TSAN): We cannot instrument complete_exit/reenter in ObjectSynchronizer
+//             in a manner similar to wait and waitUninterruptibly, because
+//             (1) recursion count stored by inflated monitor is different from
+//             the absolute recursion count tracked by Tsan, and (2) in the
+//             general case, we cannot merely store Tsan's recursion count
+//             once: we must track it for *each invocation* of complete_exit.
+//             Hence, the best place to instrument for Tsan is at the call site
+//             for complete_exit/reenter. Luckily, there is only one call site.
 intx ObjectSynchronizer::complete_exit(Handle obj, TRAPS) {
   if (UseBiasedLocking) {
     BiasedLocking::revoke(obj, THREAD);
     assert(!obj->mark().has_bias_pattern(), "biases should be revoked by now");
   }
@@ -643,10 +651,11 @@
     assert(!obj->mark().has_bias_pattern(), "biases should be revoked by now");
   }
   THREAD->set_current_pending_monitor_is_from_java(false);
   inflate(THREAD, obj(), inflate_cause_jni_enter)->enter(THREAD);
   THREAD->set_current_pending_monitor_is_from_java(true);
+  TSAN_RUNTIME_ONLY(SharedRuntime::tsan_oop_lock(THREAD, obj()));
 }
 
 // NOTE: must use heavy weight monitor to handle jni monitor exit
 void ObjectSynchronizer::jni_exit(oop obj, Thread* THREAD) {
   if (UseBiasedLocking) {
@@ -659,10 +668,11 @@
   ObjectMonitor* monitor = inflate(THREAD, obj, inflate_cause_jni_exit);
   // If this thread has locked the object, exit the monitor. We
   // intentionally do not use CHECK here because we must exit the
   // monitor even if an exception is pending.
   if (monitor->check_owner(THREAD)) {
+    TSAN_RUNTIME_ONLY(SharedRuntime::tsan_oop_unlock(THREAD, obj));
     monitor->exit(true, THREAD);
   }
 }
 
 // -----------------------------------------------------------------------------
@@ -674,15 +684,17 @@
   _thread->check_for_valid_safepoint_state();
   _obj = obj;
 
   if (_dolock) {
     ObjectSynchronizer::enter(_obj, &_lock, _thread);
+    TSAN_RUNTIME_ONLY(SharedRuntime::tsan_oop_lock(_thread, _obj()));
   }
 }
 
 ObjectLocker::~ObjectLocker() {
   if (_dolock) {
+    TSAN_RUNTIME_ONLY(SharedRuntime::tsan_oop_unlock(_thread, _obj()));
     ObjectSynchronizer::exit(_obj(), &_lock, _thread);
   }
 }
 
 
@@ -698,12 +710,21 @@
     THROW_MSG_0(vmSymbols::java_lang_IllegalArgumentException(), "timeout value is negative");
   }
   ObjectMonitor* monitor = inflate(THREAD, obj(), inflate_cause_wait);
 
   DTRACE_MONITOR_WAIT_PROBE(monitor, obj(), THREAD, millis);
+
+  TSAN_ONLY(int tsan_rec = 0;)
+  TSAN_RUNTIME_ONLY(
+    tsan_rec = SharedRuntime::tsan_oop_rec_unlock(THREAD, obj());
+    assert(tsan_rec > 0, "tsan: unlocking unlocked mutex");
+  );
+
   monitor->wait(millis, true, THREAD);
 
+  TSAN_RUNTIME_ONLY(SharedRuntime::tsan_oop_rec_lock(THREAD, obj(), tsan_rec));
+
   // This dummy call is in place to get around dtrace bug 6254741.  Once
   // that's fixed we can uncomment the following line, remove the call
   // and change this function back into a "void" func.
   // DTRACE_MONITOR_PROBE(waited, monitor, obj(), THREAD);
   return dtrace_waited_probe(monitor, obj, THREAD);
@@ -715,11 +736,18 @@
     assert(!obj->mark().has_bias_pattern(), "biases should be revoked by now");
   }
   if (millis < 0) {
     THROW_MSG(vmSymbols::java_lang_IllegalArgumentException(), "timeout value is negative");
   }
-  inflate(THREAD, obj(), inflate_cause_wait)->wait(millis, false, THREAD);
+  ObjectMonitor* monitor = inflate(THREAD, obj(), inflate_cause_wait);
+  TSAN_ONLY(int tsan_rec;)
+  TSAN_RUNTIME_ONLY(
+    tsan_rec = SharedRuntime::tsan_oop_rec_unlock(THREAD, obj());
+    assert(tsan_rec > 0, "tsan: unlocking unlocked mutex");
+  );
+  monitor->wait(millis, false, THREAD);
+  TSAN_RUNTIME_ONLY(SharedRuntime::tsan_oop_rec_lock(THREAD, obj(), tsan_rec));
 }
 
 void ObjectSynchronizer::notify(Handle obj, TRAPS) {
   if (UseBiasedLocking) {
     BiasedLocking::revoke(obj, THREAD);
@@ -2089,10 +2117,14 @@
 
  public:
   ReleaseJavaMonitorsClosure(Thread* thread) : THREAD(thread) {}
   void do_monitor(ObjectMonitor* mid) {
     if (mid->owner() == THREAD) {
+      // Note well -- this occurs ONLY on thread exit, and is a last ditch
+      // effort to release all locks. Hence, we don't need to record tsan's
+      // recursion count -- it will never be locked again.
+      TSAN_RUNTIME_ONLY(SharedRuntime::tsan_oop_rec_unlock(THREAD, (oop)mid->object()));
       (void)mid->complete_exit(CHECK);
     }
   }
 };
 
