001/*
002 *  This file is part of the Jikes RVM project (http://jikesrvm.org).
003 *
004 *  This file is licensed to You under the Eclipse Public License (EPL);
005 *  You may not use this file except in compliance with the License. You
006 *  may obtain a copy of the License at
007 *
008 *      http://www.opensource.org/licenses/eclipse-1.0.php
009 *
010 *  See the COPYRIGHT.txt file distributed with this work for information
011 *  regarding copyright ownership.
012 */
013package org.jikesrvm.scheduler;
014
015import static org.jikesrvm.objectmodel.ThinLockConstants.TL_LOCK_ID_MASK;
016import static org.jikesrvm.objectmodel.ThinLockConstants.TL_LOCK_ID_SHIFT;
017import static org.jikesrvm.objectmodel.ThinLockConstants.TL_THREAD_ID_SHIFT;
018
019import org.jikesrvm.VM;
020import org.jikesrvm.objectmodel.ObjectModel;
021import org.jikesrvm.runtime.Callbacks;
022import org.jikesrvm.runtime.Magic;
023import org.jikesrvm.util.Services;
024import org.vmmagic.pragma.Inline;
025import org.vmmagic.pragma.Interruptible;
026import org.vmmagic.pragma.Uninterruptible;
027import org.vmmagic.pragma.Unpreemptible;
028import org.vmmagic.pragma.UnpreemptibleNoWarn;
029import org.vmmagic.unboxed.Offset;
030import org.vmmagic.unboxed.Word;
031
032/**
033 Lock provides RVM support for monitors and Java level
034 synchronization.
035
036 <p>
037 This class may be decomposed into four sections:
038 <OL>
039 <LI> support for synchronization methods of java.lang.Object,
040 <LI> heavy weight locking mechanism,
041 <LI> management of heavy weight locks, and
042 <LI> debugging and performance tuning support.
043 </OL>
044
045 <p><STRONG>Requirement 1:</STRONG>
046 It must be possible to lock an object when allocations are not
047 allowed.
048 </p>
049
050 <p><STRONG>Requirement 2:</STRONG>
051 After a lock has been obtained, the code of this class must return
052 without allowing a thread switch. The exception handler
053 of the baseline compiler assumes that until lock() returns the lock
054 has not been obtained.)
055 </p>
056
057 <p><STRONG>Section 1:</STRONG>
058 support for {@link java.lang.Object#notify}, {@link
059java.lang.Object#notifyAll}, and {@link java.lang.Object#wait()}.
060 When these methods are called, the indicated object must be locked
061 by the current thread.  </p>
062
063 <p><STRONG>Section 2:</STRONG>
064 has two sections.  <EM>Section 2a:</EM> locks (and unlocking)
065 objects with heavy-weight locks associated with them.  <EM>Section
066 2b:</EM> associates (and disassociates) heavy-weight locks with
067 objects.
068 </p>
069
070 <p><STRONG>Section 3:</STRONG>
071 Allocates (and frees) heavy weight locks consistent with Requirement
072 1.
073 </p>
074
075 <p><STRONG>Section 4:</STRONG>
076 debugging and performance tuning stuff.
077 </p>
078
079 <p>
080 The following performance tuning issues have not yet been addressed
081 adaquately:</p>
082 <OL>
083 <LI> <EM>What to do if the attempt to lock an object fails?</EM>  There
084 are three choices: try again (busy-wait), yield and then try again,
085 inflate the lock and yield to the heavy-weight lock's entering
086 queue.  Currently, yield n times, then inflate.
087 (This seemed to be best for the portBOB benchmark on a 12-way AIX
088 SMP in the Fall of '99.)
089 <LI> <EM>When should a heavy-weight lock be deflated?</EM>  Currently,
090 deflation happens when the lock is unlocked with nothing on either
091 of its queues.  Probably better, would be to periodically (what
092 period?) examine heavy-weight locks and deflate any that havn't
093 been held for a while (how long?).
094 <LI> <EM>How many heavy-weight locks are needed? and how should they be
095 managed?</EM>  Currently, each processor maintains a pool of free
096 locks.  When a lock is inflated by a processor it is taken from
097 this pool and when a lock is deflated by a processor it gets added
098 to the processors pool.  Since inflation can happen on one processor
099 and deflation on another, this can create an imbalance.  It might
100 be worth investigating a scheme for balancing these local pools.
101 <LI> <EM>Is there any advantage to using the {@link SpinLock#tryLock}
102 method?</EM>
103 </OL>
104 <p>
105 Once these questions, and the issue of using MCS locking in {@link SpinLock},
106 have been investigated, then a larger performance issue
107 comes into view.  A number of different light-weight locking schemes have
108 been proposed over the years (see last several OOPSLA's).  It should be
109 possible to implement each of them in RVM and compare their performance.
110 </p>
111
112 @see java.lang.Object
113 @see ThinLock
114 @see SpinLock
115 */
116
117@Uninterruptible
118public final class Lock {
119  /****************************************************************************
120   * Constants
121   */
122
123  /** do debug tracing? */
124  protected static final boolean trace = false;
125  /** Control the gathering of statistics */
126  public static final boolean STATS = false;
127
128  /** The (fixed) number of entries in the lock table spine */
129  protected static final int LOCK_SPINE_SIZE = 128;
130  /** The log size of each chunk in the spine */
131  protected static final int LOG_LOCK_CHUNK_SIZE = 11;
132  /** The size of each chunk in the spine */
133  protected static final int LOCK_CHUNK_SIZE = 1 << LOG_LOCK_CHUNK_SIZE;
134  /** The mask used to get the chunk-level index */
135  protected static final int LOCK_CHUNK_MASK = LOCK_CHUNK_SIZE - 1;
136  /** The maximum possible number of locks */
137  protected static final int MAX_LOCKS = LOCK_SPINE_SIZE * LOCK_CHUNK_SIZE;
138  /** The number of chunks to allocate on startup */
139  protected static final int INITIAL_CHUNKS = 1;
140
141  /**
142   * Should we give up or persist in the attempt to get a heavy-weight lock,
143   * if its <code>mutex</code> microlock is held by another procesor.
144   */
145  private static final boolean tentativeMicrolocking = false;
146
147  // Heavy lock table.
148
149  /** The table of locks. */
150  private static Lock[][] locks;
151  /** Used during allocation of locks within the table. */
152  private static final SpinLock lockAllocationMutex = new SpinLock();
153  /** The number of chunks in the spine that have been physically allocated */
154  private static int chunksAllocated;
155  /** The number of locks allocated (these may either be in use, on a global
156   * freelist, or on a thread's freelist. */
157  private static int nextLockIndex;
158
159  // Global free list.
160
161  /** A global lock free list head */
162  private static Lock globalFreeLock;
163  /** the number of locks held on the global free list. */
164  private static int globalFreeLocks;
165  /** the total number of allocation operations. */
166  private static int globalLocksAllocated;
167  /** the total number of free operations. */
168  private static int globalLocksFreed;
169
170  // Statistics
171
172  /** Number of lock operations */
173  public static int lockOperations;
174  /** Number of unlock operations */
175  public static int unlockOperations;
176  /** Number of deflations */
177  public static int deflations;
178
179  /****************************************************************************
180   * Instance
181   */
182
183  /** The object being locked (if any). */
184  protected Object lockedObject;
185  /** The id of the thread that owns this lock (if any). */
186  protected int ownerId;
187  /** The number of times the owning thread (if any) has acquired this lock. */
188  protected int recursionCount;
189  /** A spin lock to handle contention for the data structures of this lock. */
190  public final SpinLock mutex;
191  /** Is this lock currently being used? */
192  protected boolean active;
193  /** The next free lock on the free lock list */
194  private Lock nextFreeLock;
195  /** This lock's index in the lock table*/
196  protected int index;
197  /** Queue for entering the lock, guarded by mutex. */
198  ThreadQueue entering;
199  /** Queue for waiting on a notify, guarded by mutex as well. */
200  ThreadQueue waiting;
201
202  /**
203   * A heavy weight lock to handle extreme contention and wait/notify
204   * synchronization.
205   */
206  public Lock() {
207    mutex = new SpinLock();
208    entering = new ThreadQueue();
209    waiting = new ThreadQueue();
210  }
211
212  /**
213   * Acquires this heavy-weight lock on the indicated object.
214   *
215   * @param o the object to be locked
216   * @return true, if the lock succeeds; false, otherwise
217   */
218  @Unpreemptible
219  public boolean lockHeavy(Object o) {
220    if (tentativeMicrolocking) {
221      if (!mutex.tryLock()) {
222        return false;
223      }
224    } else {
225      mutex.lock();  // Note: thread switching is not allowed while mutex is held.
226    }
227    return lockHeavyLocked(o);
228  }
229
230  /**
231   * Completes the task of acquiring the heavy lock, assuming that the mutex
232      is already acquired (locked).
233   * @param o the object whose lock is to be acquired
234   * @return whether locking succeeded
235   */
236  @Unpreemptible
237  public boolean lockHeavyLocked(Object o) {
238    if (lockedObject != o) { // lock disappeared before we got here
239      mutex.unlock(); // thread switching benign
240      return false;
241    }
242    if (STATS) lockOperations++;
243    RVMThread me = RVMThread.getCurrentThread();
244    int threadId = me.getLockingId();
245    if (ownerId == threadId) {
246      recursionCount++;
247    } else if (ownerId == 0) {
248      ownerId = threadId;
249      recursionCount = 1;
250    } else {
251      entering.enqueue(me);
252      mutex.unlock();
253      me.monitor().lockNoHandshake();
254      while (entering.isQueued(me)) {
255        me.monitor().waitWithHandshake(); // this may spuriously return
256      }
257      me.monitor().unlock();
258      return false;
259    }
260    mutex.unlock(); // thread-switching benign
261    return true;
262  }
263
264  @UnpreemptibleNoWarn
265  private static void raiseIllegalMonitorStateException(String msg, Object o) {
266    throw new IllegalMonitorStateException(msg + o);
267  }
268
269  /**
270   * Releases this heavy-weight lock on the indicated object.
271   *
272   * @param o the object to be unlocked
273   */
274  @Unpreemptible
275  public void unlockHeavy(Object o) {
276    boolean deflated = false;
277    mutex.lock(); // Note: thread switching is not allowed while mutex is held.
278    RVMThread me = RVMThread.getCurrentThread();
279    if (ownerId != me.getLockingId()) {
280      mutex.unlock(); // thread-switching benign
281      raiseIllegalMonitorStateException("heavy unlocking", o);
282    }
283    recursionCount--;
284    if (0 < recursionCount) {
285      mutex.unlock(); // thread-switching benign
286      return;
287    }
288    if (STATS) unlockOperations++;
289    ownerId = 0;
290    RVMThread toAwaken = entering.dequeue();
291    if (toAwaken == null && entering.isEmpty() && waiting.isEmpty()) { // heavy lock can be deflated
292      // Possible project: decide on a heuristic to control when lock should be deflated
293      Offset lockOffset = Magic.getObjectType(o).getThinLockOffset();
294      if (!lockOffset.isMax()) { // deflate heavy lock
295        deflate(o, lockOffset);
296        deflated = true;
297      }
298    }
299    mutex.unlock(); // does a Magic.sync();  (thread-switching benign)
300    if (toAwaken != null) {
301      toAwaken.monitor().lockedBroadcastNoHandshake();
302    }
303  }
304
305  /**
306   * Disassociates this heavy-weight lock from the indicated object.
307   * This lock is not held, nor are any threads on its queues.  Note:
308   * the mutex for this lock is held when deflate is called.
309   *
310   * @param o the object from which this lock is to be disassociated
311   * @param lockOffset the lock's offset from the object
312   */
313  private void deflate(Object o, Offset lockOffset) {
314    if (VM.VerifyAssertions) {
315      VM._assert(lockedObject == o);
316      VM._assert(recursionCount == 0);
317      VM._assert(entering.isEmpty());
318      VM._assert(waiting.isEmpty());
319    }
320    if (STATS) deflations++;
321    ThinLock.markDeflated(o, lockOffset, index);
322    lockedObject = null;
323    free(this);
324  }
325
326  /**
327   * Set the owner of a lock
328   * @param id The thread id of the owner.
329   */
330  public void setOwnerId(int id) {
331    ownerId = id;
332  }
333
334  /**
335   * @return the thread id of the current owner of the lock.
336   */
337  public int getOwnerId() {
338    return ownerId;
339  }
340
341  /**
342   * Update the lock's recursion count.
343   *
344   * @param c new recursion count
345   */
346  public void setRecursionCount(int c) {
347    recursionCount = c;
348  }
349
350  /**
351   * @return the lock's recursion count.
352   */
353  public int getRecursionCount() {
354    return recursionCount;
355  }
356
357  /**
358   * Set the object that this lock is referring to.
359   *
360   * @param o the new locked object
361   */
362  public void setLockedObject(Object o) {
363    lockedObject = o;
364  }
365
366  /**
367   * @return the object that this lock is referring to.
368   */
369  public Object getLockedObject() {
370    return lockedObject;
371  }
372
373  /**
374   * Dump threads blocked trying to get this lock
375   */
376  protected void dumpBlockedThreads() {
377    VM.sysWrite(" entering: ");
378    entering.dump();
379  }
380  /**
381   * Dump threads waiting to be notified on this lock
382   */
383  protected void dumpWaitingThreads() {
384    VM.sysWrite(" waiting: ");
385    waiting.dump();
386  }
387
388  /**
389   * Reports the state of a heavy-weight lock, via {@link VM#sysWrite}.
390   */
391  private void dump() {
392    if (!active) {
393      return;
394    }
395    VM.sysWrite("Lock ");
396    VM.sysWriteInt(index);
397    VM.sysWrite(":\n");
398    VM.sysWrite(" lockedObject: ");
399    VM.sysWriteHex(Magic.objectAsAddress(lockedObject));
400    VM.sysWrite("   thin lock = ");
401    VM.sysWriteHex(Magic.objectAsAddress(lockedObject).loadAddress(ObjectModel.defaultThinLockOffset()));
402    VM.sysWrite(" object type = ");
403    VM.sysWrite(Magic.getObjectType(lockedObject).getDescriptor());
404    VM.sysWriteln();
405
406    VM.sysWrite(" ownerId: ");
407    VM.sysWriteInt(ownerId);
408    VM.sysWrite(" (");
409    VM.sysWriteInt(ownerId >>> TL_THREAD_ID_SHIFT);
410    VM.sysWrite(") recursionCount: ");
411    VM.sysWriteInt(recursionCount);
412    VM.sysWriteln();
413    dumpBlockedThreads();
414    dumpWaitingThreads();
415
416    VM.sysWrite(" mutexLatestContender: ");
417    if (mutex.latestContender == null) {
418      VM.sysWrite("<null>");
419    } else {
420      VM.sysWriteInt(mutex.latestContender.getThreadSlot());
421    }
422    VM.sysWrite("\n");
423  }
424
425  /**
426   * @param t a thread
427   * @return whether this lock is blocking the given thread
428   */
429  protected boolean isBlocked(RVMThread t) {
430    return entering.isQueued(t);
431  }
432
433  /**
434   * @param t a thread
435   * @return whether the thread is waiting on this lock
436   */
437  protected boolean isWaiting(RVMThread t) {
438    return waiting.isQueued(t);
439  }
440
441  /****************************************************************************
442   * Static Lock Table
443   */
444
445  /**
446   * Sets up the data structures for holding heavy-weight locks.
447   */
448  @Interruptible
449  public static void init() {
450    nextLockIndex = 1;
451    locks = new Lock[LOCK_SPINE_SIZE][];
452    for (int i = 0; i < INITIAL_CHUNKS; i++) {
453      chunksAllocated++;
454      locks[i] = new Lock[LOCK_CHUNK_SIZE];
455    }
456    if (VM.VerifyAssertions) {
457      // check that each potential lock is addressable
458      VM._assert(((MAX_LOCKS - 1) <=
459                  TL_LOCK_ID_MASK.rshl(TL_LOCK_ID_SHIFT).toInt()) ||
460                  TL_LOCK_ID_MASK.EQ(Word.fromIntSignExtend(-1)));
461    }
462  }
463
464  /**
465   * Delivers up an unassigned heavy-weight lock.  Locks are allocated
466   * from processor specific regions or lists, so normally no synchronization
467   * is required to obtain a lock.
468   * <p>
469   * Collector threads cannot use heavy-weight locks.
470   *
471   * @return a free Lock; or <code>null</code>, if garbage collection is not enabled
472   */
473  @UnpreemptibleNoWarn("The caller is prepared to lose control when it allocates a lock")
474  static Lock allocate() {
475    RVMThread me = RVMThread.getCurrentThread();
476    if (me.cachedFreeLock != null) {
477      Lock l = me.cachedFreeLock;
478      me.cachedFreeLock = null;
479      if (trace) {
480        VM.sysWriteln("Lock.allocate: returning ",Magic.objectAsAddress(l),
481                      ", a cached free lock from Thread #",me.getThreadSlot());
482      }
483      l.active = true;
484      return l;
485    }
486
487    Lock l = null;
488    while (l == null) {
489      if (globalFreeLock != null) {
490        lockAllocationMutex.lock();
491        l = globalFreeLock;
492        if (l != null) {
493          globalFreeLock = l.nextFreeLock;
494          l.nextFreeLock = null;
495          l.active = true;
496          globalFreeLocks--;
497        }
498        lockAllocationMutex.unlock();
499        if (trace && l != null) {
500          VM.sysWriteln("Lock.allocate: returning ",Magic.objectAsAddress(l),
501                        " from the global freelist for Thread #",me.getThreadSlot());
502        }
503      } else {
504        l = new Lock(); // may cause thread switch (and processor loss)
505        lockAllocationMutex.lock();
506        if (globalFreeLock == null) {
507          // ok, it's still correct for us to be adding a new lock
508          if (nextLockIndex >= MAX_LOCKS) {
509            VM.sysWriteln("Too many fat locks"); // make MAX_LOCKS bigger? we can keep going??
510            VM.sysFail("Exiting VM with fatal error");
511          }
512          l.index = nextLockIndex++;
513          globalLocksAllocated++;
514        } else {
515          l = null; // someone added to the freelist, try again
516        }
517        lockAllocationMutex.unlock();
518        if (l != null) {
519          if (l.index >= numLocks()) {
520            /* We need to grow the table */
521            growLocks(l.index);
522          }
523          addLock(l);
524          l.active = true;
525          /* make sure other processors see lock initialization.
526           * Note: Derek and I BELIEVE that an isync is not required in the other processor because the lock is newly allocated - Bowen */
527          Magic.sync();
528        }
529        if (trace && l != null) {
530          VM.sysWriteln("Lock.allocate: returning ",Magic.objectAsAddress(l),
531                        ", a freshly allocated lock for Thread #",
532                        me.getThreadSlot());
533        }
534      }
535    }
536    return l;
537  }
538
539  /**
540   * Recycles an unused heavy-weight lock.  Locks are deallocated
541   * to processor specific lists, so normally no synchronization
542   * is required to obtain or release a lock.
543   *
544   * @param l the unused lock
545   */
546  protected static void free(Lock l) {
547    l.active = false;
548    RVMThread me = RVMThread.getCurrentThread();
549    if (me.cachedFreeLock == null) {
550      if (trace) {
551        VM.sysWriteln("Lock.free: setting ",Magic.objectAsAddress(l),
552                      " as the cached free lock for Thread #",
553                      me.getThreadSlot());
554      }
555      me.cachedFreeLock = l;
556    } else {
557      if (trace) {
558        VM.sysWriteln("Lock.free: returning ",Magic.objectAsAddress(l),
559                      " to the global freelist for Thread #",
560                      me.getThreadSlot());
561      }
562      returnLock(l);
563    }
564  }
565  static void returnLock(Lock l) {
566    if (trace) {
567      VM.sysWriteln("Lock.returnLock: returning ",Magic.objectAsAddress(l),
568                    " to the global freelist for Thread #",
569                    RVMThread.getCurrentThreadSlot());
570    }
571    lockAllocationMutex.lock();
572    l.nextFreeLock = globalFreeLock;
573    globalFreeLock = l;
574    globalFreeLocks++;
575    globalLocksFreed++;
576    lockAllocationMutex.unlock();
577  }
578
579  /**
580   * Grow the locks table by allocating a new spine chunk.
581   *
582   * @param id the lock's index in the table
583   */
584  @UnpreemptibleNoWarn("The caller is prepared to lose control when it allocates a lock")
585  static void growLocks(int id) {
586    int spineId = id >> LOG_LOCK_CHUNK_SIZE;
587    if (spineId >= LOCK_SPINE_SIZE) {
588      VM.sysFail("Cannot grow lock array greater than maximum possible index");
589    }
590    for (int i = chunksAllocated; i <= spineId; i++) {
591      if (locks[i] != null) {
592        /* We were beaten to it */
593        continue;
594      }
595
596      /* Allocate the chunk */
597      Lock[] newChunk = new Lock[LOCK_CHUNK_SIZE];
598
599      lockAllocationMutex.lock();
600      if (locks[i] == null) {
601        /* We got here first */
602        locks[i] = newChunk;
603        chunksAllocated++;
604      }
605      lockAllocationMutex.unlock();
606    }
607  }
608
609  /**
610   * @return the number of lock slots that have been allocated. This provides
611   * the range of valid lock ids.
612   */
613  public static int numLocks() {
614    return chunksAllocated * LOCK_CHUNK_SIZE;
615  }
616
617  /**
618   * Read a lock from the lock table by id.
619   *
620   * @param id The lock id
621   * @return The lock object.
622   */
623  @Inline
624  public static Lock getLock(int id) {
625    return locks[id >> LOG_LOCK_CHUNK_SIZE][id & LOCK_CHUNK_MASK];
626  }
627
628  /**
629   * Add a lock to the lock table
630   *
631   * @param l The lock object
632   */
633  @Uninterruptible
634  public static void addLock(Lock l) {
635    Lock[] chunk = locks[l.index >> LOG_LOCK_CHUNK_SIZE];
636    int index = l.index & LOCK_CHUNK_MASK;
637    Services.setArrayUninterruptible(chunk, index, l);
638  }
639
640  /**
641   * Dump the lock table.
642   */
643  public static void dumpLocks() {
644    for (int i = 0; i < numLocks(); i++) {
645      Lock l = getLock(i);
646      if (l != null) {
647        l.dump();
648      }
649    }
650    VM.sysWrite("\n");
651    VM.sysWrite("lock availability stats: ");
652    VM.sysWriteInt(globalLocksAllocated);
653    VM.sysWrite(" locks allocated, ");
654    VM.sysWriteInt(globalLocksFreed);
655    VM.sysWrite(" locks freed, ");
656    VM.sysWriteInt(globalFreeLocks);
657    VM.sysWrite(" free locks\n");
658  }
659
660  /**
661   * Count number of locks held by thread
662   * @param id the thread locking ID we're counting for
663   * @return number of locks held
664   */
665  public static int countLocksHeldByThread(int id) {
666    int count = 0;
667    for (int i = 0; i < numLocks(); i++) {
668      Lock l = getLock(i);
669      if (l != null && l.active && l.ownerId == id && l.recursionCount > 0) {
670        count++;
671      }
672    }
673    return count;
674  }
675
676  /**
677   * scan lock queues for thread and report its state
678   * @param t the thread whose state is of interest
679   * @return the thread state in human readable form
680   */
681  @Interruptible
682  public static String getThreadState(RVMThread t) {
683    for (int i = 0; i < numLocks(); i++) {
684      Lock l = getLock(i);
685      if (l == null || !l.active) continue;
686      if (l.isBlocked(t)) return ("waitingForLock(blocked)" + i);
687      if (l.isWaiting(t)) return "waitingForNotification(waiting)";
688    }
689    return null;
690  }
691
692  /****************************************************************************
693   * Statistics
694   */
695
696  /**
697   * Set up callbacks to report statistics.
698   */
699  @Interruptible
700  public static void boot() {
701    if (STATS) {
702      Callbacks.addExitMonitor(new Lock.ExitMonitor());
703      Callbacks.addAppRunStartMonitor(new Lock.AppRunStartMonitor());
704    }
705  }
706
707  /**
708   * Initialize counts in preparation for gathering statistics
709   */
710  private static final class AppRunStartMonitor implements Callbacks.AppRunStartMonitor {
711    @Override
712    public void notifyAppRunStart(String app, int value) {
713      lockOperations = 0;
714      unlockOperations = 0;
715      deflations = 0;
716
717      ThinLock.notifyAppRunStart("", 0);
718    }
719  }
720
721  /**
722   * Report statistics at the end of execution.
723   */
724  private static final class ExitMonitor implements Callbacks.ExitMonitor {
725    @Override
726    public void notifyExit(int value) {
727      int totalLocks = lockOperations + ThinLock.fastLocks + ThinLock.slowLocks;
728
729      RVMThread.dumpStats();
730      VM.sysWrite(" notifyAll operations\n");
731      VM.sysWrite("FatLocks: ");
732      VM.sysWrite(lockOperations);
733      VM.sysWrite(" locks");
734      Services.percentage(lockOperations, totalLocks, "all lock operations");
735      VM.sysWrite("FatLocks: ");
736      VM.sysWrite(unlockOperations);
737      VM.sysWrite(" unlock operations\n");
738      VM.sysWrite("FatLocks: ");
739      VM.sysWrite(deflations);
740      VM.sysWrite(" deflations\n");
741
742      ThinLock.notifyExit(totalLocks);
743      VM.sysWriteln();
744
745      VM.sysWrite("lock availability stats: ");
746      VM.sysWriteInt(globalLocksAllocated);
747      VM.sysWrite(" locks allocated, ");
748      VM.sysWriteInt(globalLocksFreed);
749      VM.sysWrite(" locks freed, ");
750      VM.sysWriteInt(globalFreeLocks);
751      VM.sysWrite(" free locks\n");
752    }
753  }
754}
755