diff --git a/jsrc/ct.c b/jsrc/ct.c
index c3d44e7a..d9f40c3c 100644
--- a/jsrc/ct.c
+++ b/jsrc/ct.c
@@ -362,23 +362,22 @@ typedef struct jobstruct {
  };
 } JOB;
 
-// we use the 6 LSBs of jobq->ht[0] as the lock, so that when we get the lock we also have the job pointer.  The job is always on a cacheline boundary
+// We use a separate lock counter field to support more threads (up to MAXTHREADSINPOOL).
 // We take JOBLOCK before taking the mutex, always.  By measurement (20220516 SkylakeX, 4 cores) the job lock keeps contention low until the tasks are < 400ns
 // long, while using the mutex gives out at < 1000ns
-_Static_assert(MAXTHREADSINPOOL<64,"JOBLOCK fails if > 63 threads in pool");
-#define JOBLOCK(jobq) ({I z; if(unlikely(((z=__atomic_fetch_add((I*)&jobq->ht[0],1,__ATOMIC_ACQ_REL))&(ABDY-1))!=0))z=joblock(jobq); (JOB*)z; })
-#define JOBUNLOCK(jobq,oldh) __atomic_store_n(&jobq->ht[0],oldh,__ATOMIC_RELEASE);
-static NOINLINE I joblock(JOBQ *jobq){I z;
+_Static_assert(MAXTHREADSINPOOL<=65536,"JOBLOCK may have issues with extremely high thread counts");
+#define JOBLOCK(jobq) ({UI z; if(unlikely((z=__atomic_fetch_add(&jobq->lockcount,1,__ATOMIC_ACQ_REL))!=0))joblock(jobq); jobq->ht[0]; })
+#define JOBUNLOCK(jobq,oldh) __atomic_store_n(&jobq->lockcount,0,__ATOMIC_RELEASE);
+static NOINLINE void joblock(JOBQ *jobq){
  // loop until we get the lock
  do{
   I nspins=10;  // good upper bound on the amount of time a lock could reasonably take, in poll delays.  It's the time of an uncontended lock of the mutex
   // we are delaying while a writer finishes.  Usually this will be fairly short, as controlled by nspins.  The danger is that the
   // writer will be preempted, leaving us in a tight spin.  If the spin counter goes to 0, we decide this must have happened, and we
   // do a low-power delay for a little while (method TBD)
-  do{if(--nspins==0){nspins=50; YIELD} POLLDELAY}while((__atomic_load_n((I*)&jobq->ht[0],__ATOMIC_ACQUIRE)&(ABDY-1))!=0);  // loop till lock released
+  do{if(--nspins==0){nspins=50; YIELD} POLLDELAY}while(__atomic_load_n(&jobq->lockcount,__ATOMIC_ACQUIRE)!=0);  // loop till lock released
   // try to reacquire the lock, loop if can't
- }while(((z=__atomic_fetch_add((I*)&jobq->ht[0],1,__ATOMIC_ACQ_REL))&(ABDY-1))!=0);
- R z;
+ }while(__atomic_fetch_add(&jobq->lockcount,1,__ATOMIC_ACQ_REL)!=0);
 }
 // It would be possible to save a little time in going from active to waiting in all threads, by having a 'conditional lock' that
 // would not wait if the lock is held but empty.  Each thread would see the empty and process it simultaneously on the way to waiting.
diff --git a/jsrc/j.h b/jsrc/j.h
index bced4567..7ed28e2e 100644
--- a/jsrc/j.h
+++ b/jsrc/j.h
@@ -781,9 +781,9 @@ struct jtimespec jmtfclk(void); //'fast clock'; maybe less inaccurate; intended
 
 #define ANASARGEEMENT 0 // set to check whether or not AN() is equal to */AS()
 
-#define MAXTHREADS 63    // maximum number of tasks running at once, including the master thread.   System lock polls every thread, allocated or not, which is the only real limit on size.  Unactivated
+#define MAXTHREADS 255    // maximum number of tasks running at once, including the master thread.   System lock polls every thread, allocated or not, which is the only real limit on size.  Unactivated
                        // threads will be paged out.
-#define MAXTHREADSRND 64  // MAXTHREADS+1, rounded up to power-of-2 bdy to get the the JST block aligned on a multiple of its size.  The JTT blocks come after the JTT block, which has the same size
+#define MAXTHREADSRND 256  // MAXTHREADS+1, rounded up to power-of-2 bdy to get the the JST block aligned on a multiple of its size.  The JTT blocks come after the JTT block, which has the same size
 #if MAXTHREADS>255
 #define WLOCKBIT 0x8000  // the LSB of the part of a 16-bit lock used for write locks.
 #else
@@ -791,8 +791,8 @@ struct jtimespec jmtfclk(void); //'fast clock'; maybe less inaccurate; intended
 #endif
 
 #define MAXTHREADPOOLS 8  // max # thread pools supported
-#define MAXTHREADSINPOOL 63  // Max threads in a single pool.  The low bits of the task pointer are used as a lock, so that code will have to be rewritten if MAXTHREADSINPOOL>63 (which is the allocation boundary for the
-                       // task block).  As of now, this limit is immaterial, because every thread in the system might choose to start a job on a single threadpool, which limits the total number of threads to 63.  But
+#define MAXTHREADSINPOOL 128  // Max threads in a single pool.  We use a separate lock counter field instead of pointer LSBs to support more threads.
+                       // As of now, this limit is immaterial, because every thread in the system might choose to start a job on a single threadpool, which limits the total number of threads to MAXTHREADSINPOOL.  But
                        // we could force job/task/thread creators to serialize on a lock, which would limit the number of waits from outside the pool to 1, and then we could have more threads total as long as
                        // the number in a single pool is limited
 
diff --git a/jsrc/jt.h b/jsrc/jt.h
index 2a79d955..6b336d04 100644
--- a/jsrc/jt.h
+++ b/jsrc/jt.h
@@ -56,7 +56,8 @@ typedef struct rngdata {
 typedef struct jobstruct JOB;
 // job queue for each threadpool.  ht[0] is used as the job lock, so we align to cacheline boundary to avoid false sharing
 typedef struct __attribute__((aligned(CACHELINESIZE))) {
- JOB *ht[2];  // queue head/tail.  When empty, ht[0] is 0 and ht[1] points to ht[1] as a harmless store location.  The job MUST be on a ABDY boundary, because LSBs are used as a lock.  Modified only when the job lock is held
+ JOB *ht[2];  // queue head/tail.  When empty, ht[0] is 0 and ht[1] points to ht[1] as a harmless store location.  The job MUST be on a ABDY boundary.  Modified only when the job lock is held
+ UI lockcount;  // Lock counter for this job queue. 0 = unlocked, >0 = number of threads trying to acquire lock
  UI4 futex;  // futex used by all threads in this JOBQ
  UI4 nuunfin;   // Number of unfinished user jobs, queued and running.  Modified only when the job lock is held
  UI4 keepwarmns;  // time in ns to spin-poll this jobq before going into wait state
