JAVA Thread & ThreadLocal

Keywords: Java jvm Database

Note: Based on jdk11

Thread

Thread is a thread executed in a program. The JVM allows multiple threads in an application to execute concurrently.
Each thread has a priority, and the high priority thread is better than the low priority thread. At the same time, threads can also be marked as daemons. When a thread is created, the priority is equal to the priority of the creator by default.

There are several ways to create a Thread:

  1. Inherit Thread class, override run() method

    public class ConcreteThread extends Thread(){
        public void run() {
          ....
        } 
    }
    new ConcreteThread().start()
  2. Implement the Runnable interface and rewrite the run() method

    public class ConcreteThread implements Runnable(){
       public void run() {
         ....
       } 
    }
    new ConcreteThread().start()
  3. Anonymous class mode

    new Thread(new Runnable() {
      public void run() {
        ....
      } 
    }).start()

The following interfaces are implemented

  1. The interface annotated by functional interface defines the public abstract void run() method for subclasses to implement.

Several important member variables

  1. private volatile String name; the name decorated by volatile. Each thread must have a unique name, which is convenient for debugging. Generally, it is thread nextthreadnum()
  2. private boolean daemon = false; daemons or not, no by default
  3. private boolean stillborn = false;
  4. private long eetop;
  5. private Runnable target; execution target
  6. private ThreadGroup group; thread group, default to security.getthreadgroup() or parent thread group
  7. private ClassLoader contextClassLoader;
  8. private AccessControlContext inheritedAccessControlContext;
  9. private static int threadInitNumber; and Thread splicing form the default name of Thread, which is incremented by private static synchronized int nextThreadNum()++
  10. ThreadLocal.ThreadLocalMap threadLocals = null;
  11. ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
  12. private final long stackSize; refers to the stack space applied by the current thread, which is 0 by default, depending on the vm design and implementation. Some VMS will directly ignore this configuration
  13. private long nativeParkEventPointer;
  14. private final long tid; current thread ID
  15. private static long threadSeqNumber; thread id counter, private static synchronized long nextThreadID() to increase + + threadSeqNumber
  16. private volatile int threadStatus;
  17. volatile Object parkBlocker;
  18. private volatile Interruptible blocker;
  19. private final Object blockerLock = new Object();
  20. public static final int MIN_PRIORITY = 1; the minimum priority that a thread can set
    Public static final int norm'u priority = 5; thread default priority
    public static final int MAX_PRIORITY = 10; the maximum priority that a thread can set
    The priority of the thread will correspond to the priority of different operating systems. The JVM does not necessarily set the priority for thread scheduling
  21. Exception handling related
    //handler of current thread exception handling, decorated by volatile
    private volatile UncaughtExceptionHandler uncaughtExceptionHandler;
    //All threads default exception handler, decorated by static volatile
    private static volatile UncaughtExceptionHandler defaultUncaughtExceptionHandler;

Several important methods

  1. Start the thread, the JVM will call the run method of the current thread

      public synchronized void start() {
            /**
             * This method is not invoked for the main method thread or "system"
             * group threads created/set up by the VM. Any new functionality added
             * to this method in the future may have to also be added to the VM.
             *
             * A zero status value corresponds to state "NEW".
             */
            if (threadStatus != 0)
                throw new IllegalThreadStateException();
    
            /* Notify the group that this thread is about to be started
             * so that it can be added to the group's list of threads
             * and the group's unstarted count can be decremented. */
            group.add(this);
    
            boolean started = false;
            try {
                start0();
                started = true;
            } finally {
                try {
                    if (!started) {
                        group.threadStartFailed(this);
                    }
                } catch (Throwable ignore) {
                    /* do nothing. If start0 threw a Throwable then
                      it will be passed up the call stack */
                }
            }
        }
    
        private native void start0();
  2. Thread stopped, obsolete

        @Deprecated(since="1.2")
      public final void stop() {
          SecurityManager security = System.getSecurityManager();
          if (security != null) {
              checkAccess();
              if (this != Thread.currentThread()) {
                  security.checkPermission(SecurityConstants.STOP_THREAD_PERMISSION);
              }
          }
          // A zero status value corresponds to "NEW", it can't change to
          // not-NEW because we hold the lock.
          if (threadStatus != 0) {
              resume(); // Wake up thread if it was suspended; no-op otherwise
          }
    
          // The VM can handle all thread states
          stop0(new ThreadDeath());
      }
      private native void stop0(Object o);

    stop threads are prone to the following two situations:

    1. Immediately stop the remaining work in the run() method (including in catch or finally statements), and throw a ThreadDeath exception (usually this exception does not need to be displayed for capture), which may cause some cleaning work not to be executed, such as the closing of file stream, database connection, etc.
    2. All locks held by this thread will be released immediately, resulting in data not being processed synchronously and data inconsistency
  3. interrupt

      public void interrupt() {
              if (this != Thread.currentThread()) {
                  checkAccess();
    
                  // thread may be blocked in an I/O operation
                  synchronized (blockerLock) {
                      Interruptible b = blocker;
                      if (b != null) {
                          interrupt0();  // set interrupt status
                          b.interrupt(this);
                          return;
                      }
                  }
              }
    
              // set interrupt status
              interrupt0();
      }
      private native void interrupt0();
      
      public static boolean interrupted() {
          return currentThread().isInterrupted(true);
      }
      private native boolean isInterrupted(boolean ClearInterrupted);
  4. join queue and block the current execution thread, using loop + wait

      public final void join() throws InterruptedException {
        join(0);
      }
      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;
              }
          }
      }
      public final native boolean isAlive();
  5. Suspend and resume should appear in pairs. If A thread accesses A resource x, suspend(), then no thread can access resource x until A thread is resume()

      @Deprecated(since="1.2")
      public final void suspend() {
          checkAccess();
          suspend0();
      }
      @Deprecated(since="1.2")
      public final void resume() {
          checkAccess();
          resume0();
      }

Thread state and state transition

  1. State definition

      public enum State {
            NEW,
            RUNNABLE,
            BLOCKED,
            WAITING,
            TIMED_WAITING,
            TERMINATED;
        }
  2. state diagram
  3. Example:

    package chapter02;
    
    public class TestThread {
    
      public static void main(String [] args) throws InterruptedException {
          final Thread thread0
                  = new Thread(new Runnable() {
              @Override
              public void run() {
                  System.out.println("Get into run");
                  try {
                      System.out.printf("enter run(), thread0' state: %s\n",  Thread.currentThread().getState());
                      Thread.sleep(5000);
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                      System.out.println("exception handling");
                      System.out.printf("on catch interrupt, thread0 isInterrupted or not ? %s \n", Thread.currentThread().isInterrupted());
                      System.out.printf("on catch interrupt, thread0' state: %s\n",  Thread.currentThread().getState());
    
                      return;
                  }
                  System.out.println("Sign out run");
              }
          });
    
          Thread thread1 = new Thread(new Runnable() {
              @Override
              public void run() {
                  System.out.println("Get into thread1's run");
                  try {
                      Thread.sleep(1000);
                      System.out.printf("before interrupt, thread0 isInterrupted or nott ?  %s  \n", thread0.isInterrupted());
                      System.out.printf("enter thread1's run(), thread0' state: %s\n",  thread0.getState());
                      Thread.sleep(1000);
                      thread0.interrupt();
                      System.out.printf("after interrupt, thread0 isInterrupted or not ?  %s  \n", thread0.isInterrupted());
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
                  System.out.println("Sign out thread1's run");
              }
          });
          System.out.printf("after new(), thread0' state: %s\n", thread0.getState());
          thread0.start();
          System.out.printf("after start(), thread0' state: %s\n", thread0.getState());
          thread1.start();
          thread0.join();
          System.out.printf("after join(), thread0' state: %s\n", thread0.getState());
          System.out.println("Sign out");
    
    
    
      }
    }

    The printing results are as follows:

    after new(), thread0' state: NEW
    after start(), thread0' state: RUNNABLE
    //Enter run
    enter run(), thread0' state: RUNNABLE
    //Enter thread1's run
    before interrupt, thread0 isInterrupted or nott ?  false  
    enter thread1's run(), thread0' state: TIMED_WAITING
    after interrupt, thread0 isInterrupted or not ?  false  
    //Exit thread1's run
    //exception handling
    on catch interrupt, thread0 isInterrupted or not ? false 
    on catch interrupt, thread0' state: RUNNABLE
    after join(), thread0' state: TERMINATED
    //Sign out

Anomaly capture

  1. Explain:
    //handler of current thread exception handling, decorated by volatile
    private volatile UncaughtExceptionHandler uncaughtExceptionHandler;
    //All threads default exception handler, decorated by static volatile
    private static volatile UncaughtExceptionHandler defaultUncaughtExceptionHandler;
  2. Example:

    package chapter02;
    
    public class TestThread {
    
        public static void main(String [] args) throws InterruptedException {
            //Global exception handler
            Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
                @Override
                public void uncaughtException(Thread t, Throwable e) {
                    System.out.println("-" + Thread.currentThread().getName());
                    String threadName = t.getName();
                    System.out.printf("global exception handler >> : current thread's name is %s, ", threadName);
                    System.out.printf("the error is %s \n",e.getLocalizedMessage());
                }
            });
    
            final Thread thread0
                    = new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println("Get into thread0's run");
                    System.out.printf("enter run(), thread0' state: %s\n",  Thread.currentThread().getState());
                    try {
                        Thread.sleep(5000);
                    } catch (InterruptedException e) {
    //                    e.printStackTrace();
    //                    System.out.println("exception handling");
    //                    System.out.printf("on catch interrupt, thread0 isInterrupted or not ? %s \n", Thread.currentThread().isInterrupted());
    //                    System.out.printf("on catch interrupt, thread0' state: %s\n",  Thread.currentThread().getState());
    //
    //                    return;
    
                        throw new RuntimeException(e);
                    }
                    System.out.println("Sign out thread0's run");
                }
            });
            //thread0 exception handler
            thread0.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
                @Override
                public void uncaughtException(Thread t, Throwable e) {
                    System.out.println("-" + Thread.currentThread().getName());
                    String threadName = t.getName();
                    System.out.printf("thread0 exception handler >> : current thread's name is %s, ", threadName);
                    System.out.printf("the error is %s \n",e.getLocalizedMessage());
                }
            });
    
            Thread thread1 = new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println("Get into thread1's run");
                    try {
                        Thread.sleep(1000);
                        System.out.printf("before interrupt, thread0 isInterrupted or nott ?  %s  \n", thread0.isInterrupted());
                        System.out.printf("enter thread1's run(), thread0' state: %s\n",  thread0.getState());
                        Thread.sleep(1000);
                        thread0.interrupt();
                        System.out.printf("after interrupt, thread0 isInterrupted or not ?  %s  \n", thread0.isInterrupted());
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("Sign out thread1's run");
                }
            });
            System.out.printf("after new(), thread0' state: %s\n", thread0.getState());
            thread0.start();
            System.out.printf("after start(), thread0' state: %s\n", thread0.getState());
            thread1.setDaemon(true);
            thread1.start();
            thread0.join();
            thread1.join();
            System.out.printf("after join(), thread0' state: %s\n", thread0.getState());
            System.out.println("Sign out");
    
        }
    }

    The printing results are as follows:

    after new(), thread0' state: NEW
    after start(), thread0' state: RUNNABLE
    //Enter thread0's run
    enter run(), thread0' state: RUNNABLE
    //Enter thread1's run
    before interrupt, thread0 isInterrupted or nott ?  false  
    enter thread1's run(), thread0' state: TIMED_WAITING
    after interrupt, thread0 isInterrupted or not ?  true  
    -Thread-0
    thread0 exception handler >> : current thread's name is Thread-0, the error is java.lang.InterruptedException: sleep interrupted 
    //Exit thread1's run
    after join(), thread0' state: TERMINATED
    //Sign out

ThreadLocal

  1. Explain:
    At the beginning of jdk1.2, a new idea ThreadLocal is provided to solve the concurrency problem of multithreaded programs. Using this tool class, you can write beautiful multithreading in a very simple way
    ThreadLocal is not a Thread, but a local variable of Thread.
  2. Source code:

    public class ThreadLocal<T> {
    
        private final int threadLocalHashCode = nextHashCode();
        private static AtomicInteger nextHashCode =
            new AtomicInteger();
        private static final int HASH_INCREMENT = 0x61c88647;
        private static int nextHashCode() {
            return nextHashCode.getAndAdd(HASH_INCREMENT);
        }
    
        protected T initialValue() {
            return null;
        }
    
        public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
            return new SuppliedThreadLocal<>(supplier);
        }
    
        public ThreadLocal() {
        }
    
        public T get() {
            Thread t = Thread.currentThread();
            ThreadLocalMap map = getMap(t);
            if (map != null) {
                ThreadLocalMap.Entry e = map.getEntry(this);
                if (e != null) {
                    @SuppressWarnings("unchecked")
                    T result = (T)e.value;
                    return result;
                }
            }
            return setInitialValue();
        }
    
        boolean isPresent() {
            Thread t = Thread.currentThread();
            ThreadLocalMap map = getMap(t);
            return map != null && map.getEntry(this) != null;
        }
    
        private T setInitialValue() {
            T value = initialValue();
            Thread t = Thread.currentThread();
            ThreadLocalMap map = getMap(t);
            if (map != null) {
                map.set(this, value);
            } else {
                createMap(t, value);
            }
            if (this instanceof TerminatingThreadLocal) {
                TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);
            }
            return value;
        }
        //Set the thread local value. If there is already an override, otherwise create a new ThreadLocalMap for the current thread and assign it to the threadLocals local variable of the current thread
        public void set(T value) {
            Thread t = Thread.currentThread();
            ThreadLocalMap map = getMap(t);
            if (map != null) {
                map.set(this, value);
            } else {
                createMap(t, value);
            }
        }
        
        //Delete the local value. If you do not call this method, the jvm will recycle it after the thread is destroyed. If you access the get() method multiple times after calling this method, you may trigger the initialValue() multiple times
        public void remove() {
             ThreadLocalMap m = getMap(Thread.currentThread());
             if (m != null) {
                 m.remove(this);
             }
         }
    
        ThreadLocalMap getMap(Thread t) {
            return t.threadLocals;
        }
    
        void createMap(Thread t, T firstValue) {
            t.threadLocals = new ThreadLocalMap(this, firstValue);
        }
    
        static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
            return new ThreadLocalMap(parentMap);
        }
    
        T childValue(T parentValue) {
            throw new UnsupportedOperationException();
        }
    
        static final class SuppliedThreadLocal<T> extends ThreadLocal<T> {
    
            private final Supplier<? extends T> supplier;
    
            SuppliedThreadLocal(Supplier<? extends T> supplier) {
                this.supplier = Objects.requireNonNull(supplier);
            }
    
            @Override
            protected T initialValue() {
                return supplier.get();
            }
        }
    
        static class ThreadLocalMap {
    
            /**
             * The entries in this hash map extend WeakReference, using
             * its main ref field as the key (which is always a
             * ThreadLocal object).  Note that null keys (i.e. entry.get()
             * == null) mean that the key is no longer referenced, so the
             * entry can be expunged from table.  Such entries are referred to
             * as "stale entries" in the code that follows.
             */
            static class Entry extends WeakReference<ThreadLocal<?>> {
                /** The value associated with this ThreadLocal. */
                Object value;
    
                Entry(ThreadLocal<?> k, Object v) {
                    super(k);
                    value = v;
                }
            }
    
            private static final int INITIAL_CAPACITY = 16;
            private Entry[] table;
            private int size = 0;
            private int threshold; // Default to 0
            private void setThreshold(int len) {
                threshold = len * 2 / 3;
            }
            private static int nextIndex(int i, int len) {
                return ((i + 1 < len) ? i + 1 : 0);
            }
            private static int prevIndex(int i, int len) {
                return ((i - 1 >= 0) ? i - 1 : len - 1);
            }
    
            ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
                table = new Entry[INITIAL_CAPACITY];
                int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
                table[i] = new Entry(firstKey, firstValue);
                size = 1;
                setThreshold(INITIAL_CAPACITY);
            }
            private ThreadLocalMap(ThreadLocalMap parentMap) {
                Entry[] parentTable = parentMap.table;
                int len = parentTable.length;
                setThreshold(len);
                table = new Entry[len];
    
                for (Entry e : parentTable) {
                    if (e != null) {
                        @SuppressWarnings("unchecked")
                        ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
                        if (key != null) {
                            Object value = key.childValue(e.value);
                            Entry c = new Entry(key, value);
                            int h = key.threadLocalHashCode & (len - 1);
                            while (table[h] != null)
                                h = nextIndex(h, len);
                            table[h] = c;
                            size++;
                        }
                    }
                }
            }
            
            private Entry getEntry(ThreadLocal<?> key) {
                int i = key.threadLocalHashCode & (table.length - 1);
                Entry e = table[i];
                if (e != null && e.get() == key)
                    return e;
                else
                    return getEntryAfterMiss(key, i, e);
            }
            private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
                Entry[] tab = table;
                int len = tab.length;
    
                while (e != null) {
                    ThreadLocal<?> k = e.get();
                    if (k == key)
                        return e;
                    if (k == null)
                        expungeStaleEntry(i);
                    else
                        i = nextIndex(i, len);
                    e = tab[i];
                }
                return null;
            }
            private void set(ThreadLocal<?> key, Object value) {
    
                // We don't use a fast path as with get() because it is at
                // least as common to use set() to create new entries as
                // it is to replace existing ones, in which case, a fast
                // path would fail more often than not.
    
                Entry[] tab = table;
                int len = tab.length;
                int i = key.threadLocalHashCode & (len-1);
    
                for (Entry e = tab[i];
                     e != null;
                     e = tab[i = nextIndex(i, len)]) {
                    ThreadLocal<?> k = e.get();
    
                    if (k == key) {
                        e.value = value;
                        return;
                    }
    
                    if (k == null) {
                        replaceStaleEntry(key, value, i);
                        return;
                    }
                }
    
                tab[i] = new Entry(key, value);
                int sz = ++size;
                if (!cleanSomeSlots(i, sz) && sz >= threshold)
                    rehash();
            }
    
            private void remove(ThreadLocal<?> key) {
                Entry[] tab = table;
                int len = tab.length;
                int i = key.threadLocalHashCode & (len-1);
                for (Entry e = tab[i];
                     e != null;
                     e = tab[i = nextIndex(i, len)]) {
                    if (e.get() == key) {
                        e.clear();
                        expungeStaleEntry(i);
                        return;
                    }
                }
            }
    
            private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                                           int staleSlot) {
                Entry[] tab = table;
                int len = tab.length;
                Entry e;
    
                // Back up to check for prior stale entry in current run.
                // We clean out whole runs at a time to avoid continual
                // incremental rehashing due to garbage collector freeing
                // up refs in bunches (i.e., whenever the collector runs).
                int slotToExpunge = staleSlot;
                for (int i = prevIndex(staleSlot, len);
                     (e = tab[i]) != null;
                     i = prevIndex(i, len))
                    if (e.get() == null)
                        slotToExpunge = i;
    
                // Find either the key or trailing null slot of run, whichever
                // occurs first
                for (int i = nextIndex(staleSlot, len);
                     (e = tab[i]) != null;
                     i = nextIndex(i, len)) {
                    ThreadLocal<?> k = e.get();
    
                    // If we find key, then we need to swap it
                    // with the stale entry to maintain hash table order.
                    // The newly stale slot, or any other stale slot
                    // encountered above it, can then be sent to expungeStaleEntry
                    // to remove or rehash all of the other entries in run.
                    if (k == key) {
                        e.value = value;
    
                        tab[i] = tab[staleSlot];
                        tab[staleSlot] = e;
    
                        // Start expunge at preceding stale entry if it exists
                        if (slotToExpunge == staleSlot)
                            slotToExpunge = i;
                        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
                        return;
                    }
    
                    // If we didn't find stale entry on backward scan, the
                    // first stale entry seen while scanning for key is the
                    // first still present in the run.
                    if (k == null && slotToExpunge == staleSlot)
                        slotToExpunge = i;
                }
    
                // If key not found, put new entry in stale slot
                tab[staleSlot].value = null;
                tab[staleSlot] = new Entry(key, value);
    
                // If there are any other stale entries in run, expunge them
                if (slotToExpunge != staleSlot)
                    cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
            }
    
            /**
             * Expunge a stale entry by rehashing any possibly colliding entries
             * lying between staleSlot and the next null slot.  This also expunges
             * any other stale entries encountered before the trailing null.  See
             * Knuth, Section 6.4
             *
             * @param staleSlot index of slot known to have null key
             * @return the index of the next null slot after staleSlot
             * (all between staleSlot and this slot will have been checked
             * for expunging).
             */
            private int expungeStaleEntry(int staleSlot) {
                Entry[] tab = table;
                int len = tab.length;
    
                // expunge entry at staleSlot
                tab[staleSlot].value = null;
                tab[staleSlot] = null;
                size--;
    
                // Rehash until we encounter null
                Entry e;
                int i;
                for (i = nextIndex(staleSlot, len);
                     (e = tab[i]) != null;
                     i = nextIndex(i, len)) {
                    ThreadLocal<?> k = e.get();
                    if (k == null) {
                        e.value = null;
                        tab[i] = null;
                        size--;
                    } else {
                        int h = k.threadLocalHashCode & (len - 1);
                        if (h != i) {
                            tab[i] = null;
    
                            // Unlike Knuth 6.4 Algorithm R, we must scan until
                            // null because multiple entries could have been stale.
                            while (tab[h] != null)
                                h = nextIndex(h, len);
                            tab[h] = e;
                        }
                    }
                }
                return i;
            }
    
            private boolean cleanSomeSlots(int i, int n) {
                boolean removed = false;
                Entry[] tab = table;
                int len = tab.length;
                do {
                    i = nextIndex(i, len);
                    Entry e = tab[i];
                    if (e != null && e.get() == null) {
                        n = len;
                        removed = true;
                        i = expungeStaleEntry(i);
                    }
                } while ( (n >>>= 1) != 0);
                return removed;
            }
    
            private void rehash() {
                expungeStaleEntries();
    
                // Use lower threshold for doubling to avoid hysteresis
                if (size >= threshold - threshold / 4)
                    resize();
            }
            private void resize() {
                Entry[] oldTab = table;
                int oldLen = oldTab.length;
                int newLen = oldLen * 2;
                Entry[] newTab = new Entry[newLen];
                int count = 0;
    
                for (Entry e : oldTab) {
                    if (e != null) {
                        ThreadLocal<?> k = e.get();
                        if (k == null) {
                            e.value = null; // Help the GC
                        } else {
                            int h = k.threadLocalHashCode & (newLen - 1);
                            while (newTab[h] != null)
                                h = nextIndex(h, newLen);
                            newTab[h] = e;
                            count++;
                        }
                    }
                }
    
                setThreshold(newLen);
                size = count;
                table = newTab;
            }
            private void expungeStaleEntries() {
                Entry[] tab = table;
                int len = tab.length;
                for (int j = 0; j < len; j++) {
                    Entry e = tab[j];
                    if (e != null && e.get() == null)
                        expungeStaleEntry(j);
                }
            }
        }
    }

Posted by beginneratphp on Wed, 08 Jan 2020 00:36:31 -0800