diff --git a/lucene/CHANGES.txt b/lucene/CHANGES.txt
index 41a6378..cf7d295 100644
--- a/lucene/CHANGES.txt
+++ b/lucene/CHANGES.txt
@@ -158,6 +158,10 @@ Optimizations
   posting lists. All index data is represented as consecutive byte/int arrays to 
   reduce GC cost and memory overhead. (Simon Willnauer) 
 
+* LUCENE-4538: DocValues now caches direct sources in a ThreadLocal exposed via SourceCache. 
+  Users of this API can now simply obtain an instance via DocValues#getDirectSource per thread.
+  (Simon Willnauer)
+
 Build
 
 * Upgrade randomized testing to version 2.0.4: avoid hangs on shutdown
diff --git a/lucene/codecs/src/java/org/apache/lucene/codecs/simpletext/SimpleTextPerDocProducer.java b/lucene/codecs/src/java/org/apache/lucene/codecs/simpletext/SimpleTextPerDocProducer.java
index f831efd..80e19c9 100644
--- a/lucene/codecs/src/java/org/apache/lucene/codecs/simpletext/SimpleTextPerDocProducer.java
+++ b/lucene/codecs/src/java/org/apache/lucene/codecs/simpletext/SimpleTextPerDocProducer.java
@@ -198,9 +198,14 @@ public class SimpleTextPerDocProducer extends PerDocProducerBase {
       assert scratch.equals(END);
       return reader.getSource();
     }
-
+    
     @Override
     public Source getDirectSource() throws IOException {
+      return this.getSource(); // don't cache twice
+    }
+
+    @Override
+    public Source loadDirectSource() throws IOException {
       return this.getSource();
     }
 
diff --git a/lucene/core/src/java/org/apache/lucene/codecs/lucene40/values/FixedDerefBytesImpl.java b/lucene/core/src/java/org/apache/lucene/codecs/lucene40/values/FixedDerefBytesImpl.java
index 5f73318..ced08da 100644
--- a/lucene/core/src/java/org/apache/lucene/codecs/lucene40/values/FixedDerefBytesImpl.java
+++ b/lucene/core/src/java/org/apache/lucene/codecs/lucene40/values/FixedDerefBytesImpl.java
@@ -84,7 +84,7 @@ class FixedDerefBytesImpl {
     }
 
     @Override
-    public Source getDirectSource()
+    public Source loadDirectSource()
         throws IOException {
       return new DirectFixedDerefSource(cloneData(), cloneIndex(), size, getType());
     }
diff --git a/lucene/core/src/java/org/apache/lucene/codecs/lucene40/values/FixedSortedBytesImpl.java b/lucene/core/src/java/org/apache/lucene/codecs/lucene40/values/FixedSortedBytesImpl.java
index 3e18fbc..4d7a235 100644
--- a/lucene/core/src/java/org/apache/lucene/codecs/lucene40/values/FixedSortedBytesImpl.java
+++ b/lucene/core/src/java/org/apache/lucene/codecs/lucene40/values/FixedSortedBytesImpl.java
@@ -141,7 +141,7 @@ class FixedSortedBytesImpl {
     }
 
     @Override
-    public Source getDirectSource() throws IOException {
+    public Source loadDirectSource() throws IOException {
       return new DirectFixedSortedSource(cloneData(), cloneIndex(), size,
           valueCount, comparator, type);
     }
diff --git a/lucene/core/src/java/org/apache/lucene/codecs/lucene40/values/FixedStraightBytesImpl.java b/lucene/core/src/java/org/apache/lucene/codecs/lucene40/values/FixedStraightBytesImpl.java
index 8b630e9..a56e241 100644
--- a/lucene/core/src/java/org/apache/lucene/codecs/lucene40/values/FixedStraightBytesImpl.java
+++ b/lucene/core/src/java/org/apache/lucene/codecs/lucene40/values/FixedStraightBytesImpl.java
@@ -291,7 +291,7 @@ class FixedStraightBytesImpl {
     }
    
     @Override
-    public Source getDirectSource() throws IOException {
+    public Source loadDirectSource() throws IOException {
       return new DirectFixedStraightSource(cloneData(), size, getType());
     }
     
diff --git a/lucene/core/src/java/org/apache/lucene/codecs/lucene40/values/PackedIntValues.java b/lucene/core/src/java/org/apache/lucene/codecs/lucene40/values/PackedIntValues.java
index 4f27653..26d8e1e 100644
--- a/lucene/core/src/java/org/apache/lucene/codecs/lucene40/values/PackedIntValues.java
+++ b/lucene/core/src/java/org/apache/lucene/codecs/lucene40/values/PackedIntValues.java
@@ -217,7 +217,7 @@ class PackedIntValues {
 
 
     @Override
-    public Source getDirectSource() throws IOException {
+    public Source loadDirectSource() throws IOException {
       return values != null ? new FixedStraightBytesImpl.DirectFixedStraightSource(datIn.clone(), 8, Type.FIXED_INTS_64) : new PackedIntsSource(datIn.clone(), true);
     }
   }
diff --git a/lucene/core/src/java/org/apache/lucene/codecs/lucene40/values/VarDerefBytesImpl.java b/lucene/core/src/java/org/apache/lucene/codecs/lucene40/values/VarDerefBytesImpl.java
index c766a36..9766511 100644
--- a/lucene/core/src/java/org/apache/lucene/codecs/lucene40/values/VarDerefBytesImpl.java
+++ b/lucene/core/src/java/org/apache/lucene/codecs/lucene40/values/VarDerefBytesImpl.java
@@ -104,7 +104,7 @@ class VarDerefBytesImpl {
     }
    
     @Override
-    public Source getDirectSource()
+    public Source loadDirectSource()
         throws IOException {
       return new DirectVarDerefSource(cloneData(), cloneIndex(), getType());
     }
diff --git a/lucene/core/src/java/org/apache/lucene/codecs/lucene40/values/VarSortedBytesImpl.java b/lucene/core/src/java/org/apache/lucene/codecs/lucene40/values/VarSortedBytesImpl.java
index 01d336d..dae14d6 100644
--- a/lucene/core/src/java/org/apache/lucene/codecs/lucene40/values/VarSortedBytesImpl.java
+++ b/lucene/core/src/java/org/apache/lucene/codecs/lucene40/values/VarSortedBytesImpl.java
@@ -167,7 +167,7 @@ final class VarSortedBytesImpl {
     }
 
     @Override
-    public Source getDirectSource() throws IOException {
+    public Source loadDirectSource() throws IOException {
       return new DirectSortedSource(cloneData(), cloneIndex(), comparator, getType());
     }
     
diff --git a/lucene/core/src/java/org/apache/lucene/codecs/lucene40/values/VarStraightBytesImpl.java b/lucene/core/src/java/org/apache/lucene/codecs/lucene40/values/VarStraightBytesImpl.java
index 9a23cbe..fdd7e5e 100644
--- a/lucene/core/src/java/org/apache/lucene/codecs/lucene40/values/VarStraightBytesImpl.java
+++ b/lucene/core/src/java/org/apache/lucene/codecs/lucene40/values/VarStraightBytesImpl.java
@@ -252,7 +252,7 @@ class VarStraightBytesImpl {
     }
 
     @Override
-    public Source getDirectSource()
+    public Source loadDirectSource()
         throws IOException {
       return new DirectVarStraightSource(cloneData(), cloneIndex(), getType());
     }
diff --git a/lucene/core/src/java/org/apache/lucene/index/DocValues.java b/lucene/core/src/java/org/apache/lucene/index/DocValues.java
index 5d5d112..405a9dc 100644
--- a/lucene/core/src/java/org/apache/lucene/index/DocValues.java
+++ b/lucene/core/src/java/org/apache/lucene/index/DocValues.java
@@ -35,6 +35,7 @@ import org.apache.lucene.document.SortedBytesDocValuesField; // javadocs
 import org.apache.lucene.document.StraightBytesDocValuesField; // javadocs
 import org.apache.lucene.store.DataOutput;
 import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.CloseableThreadLocal;
 import org.apache.lucene.util.packed.PackedInts;
 
 /**
@@ -95,7 +96,6 @@ public abstract class DocValues implements Closeable {
 
   private volatile SourceCache cache = new SourceCache.DirectSourceCache();
   private final Object cacheLock = new Object();
-  
   /** Sole constructor. (For invocation by subclass 
    *  constructors, typically implicit.) */
   protected DocValues() {
@@ -129,12 +129,30 @@ public abstract class DocValues implements Closeable {
   public Source getSource() throws IOException {
     return cache.load(this);
   }
+  
+  /**
+   * Returns a disk resident {@link Source} instance through the current
+   * {@link SourceCache}. Direct Sources are cached per thread in the
+   * {@link SourceCache}. The obtained instance should not be shared with other
+   * threads.
+   */
+  public Source getDirectSource() throws IOException {
+    return this.cache.loadDirect(this);
+  }
+  
 
   /**
-   * Returns a disk resident {@link Source} instance. Direct Sources are not
-   * cached in the {@link SourceCache} and should not be shared between threads.
+   * Loads a new {@link Source direct source} instance from this {@link DocValues} field
+   * instance. Source instances returned from this method are not cached. It is
+   * the callers responsibility to maintain the instance and release its
+   * resources once the source is not needed anymore.
+   * <p>
+   * For managed {@link Source direct source} instances see {@link #getDirectSource()}.
+   * 
+   * @see #getDirectSource()
+   * @see #setCache(SourceCache)
    */
-  public abstract Source getDirectSource() throws IOException;
+  public abstract Source loadDirectSource() throws IOException;
 
   /**
    * Returns the {@link Type} of this {@link DocValues} instance
@@ -721,6 +739,15 @@ public abstract class DocValues implements Closeable {
      * This method will not return <code>null</code>
      */
     public abstract Source load(DocValues values) throws IOException;
+    
+    /**
+     * Atomically loads a {@link Source direct source} into the per-thread cache from the given
+     * {@link DocValues} and returns it iff no other {@link Source direct source} has already
+     * been cached. Otherwise the cached source is returned.
+     * <p>
+     * This method will not return <code>null</code>
+     */
+    public abstract Source loadDirect(DocValues values) throws IOException;
 
     /**
      * Atomically invalidates the cached {@link Source} 
@@ -744,7 +771,8 @@ public abstract class DocValues implements Closeable {
      */
     public static final class DirectSourceCache extends SourceCache {
       private Source ref;
-
+      private final CloseableThreadLocal<Source> directSourceCache = new CloseableThreadLocal<Source>();
+      
       /** Sole constructor. */
       public DirectSourceCache() {
       }
@@ -758,6 +786,19 @@ public abstract class DocValues implements Closeable {
 
       public synchronized void invalidate(DocValues values) {
         ref = null;
+        directSourceCache.close();
+      }
+
+      @Override
+      public synchronized Source loadDirect(DocValues values) throws IOException {
+        final Source source = directSourceCache.get();
+        if (source == null) {
+          final Source loadDirectSource = values.loadDirectSource();
+          directSourceCache.set(loadDirectSource);
+          return loadDirectSource;
+        } else {
+          return source;
+        }
       }
     }
   }
diff --git a/lucene/core/src/java/org/apache/lucene/index/MultiDocValues.java b/lucene/core/src/java/org/apache/lucene/index/MultiDocValues.java
index a079cd0..efbb9b6 100644
--- a/lucene/core/src/java/org/apache/lucene/index/MultiDocValues.java
+++ b/lucene/core/src/java/org/apache/lucene/index/MultiDocValues.java
@@ -209,7 +209,7 @@ class MultiDocValues extends DocValues {
     }
 
     @Override
-    public Source getDirectSource() throws IOException {
+    public Source loadDirectSource() throws IOException {
       return emptySource;
     }
   }
@@ -241,7 +241,7 @@ class MultiDocValues extends DocValues {
     }
 
     @Override
-    public Source getDirectSource() throws IOException {
+    public Source loadDirectSource() throws IOException {
       return emptyFixedSource;
     }
   }
@@ -594,7 +594,7 @@ class MultiDocValues extends DocValues {
   }
 
   @Override
-  public Source getDirectSource() throws IOException {
+  public Source loadDirectSource() throws IOException {
     return new MultiSource(slices, starts, true, type);
   }
   
diff --git a/lucene/memory/src/java/org/apache/lucene/index/memory/MemoryIndexNormDocValues.java b/lucene/memory/src/java/org/apache/lucene/index/memory/MemoryIndexNormDocValues.java
index 6a69512..2719350 100644
--- a/lucene/memory/src/java/org/apache/lucene/index/memory/MemoryIndexNormDocValues.java
+++ b/lucene/memory/src/java/org/apache/lucene/index/memory/MemoryIndexNormDocValues.java
@@ -38,7 +38,7 @@ class MemoryIndexNormDocValues extends DocValues {
   }
 
   @Override
-  public Source getDirectSource() throws IOException {
+  public Source loadDirectSource() throws IOException {
     return source;
   }
 
