Click “Technical blog post of Brother Flathead” above and select “Follow the public account”

Technical articles first time delivery!

In development, we often use HashMap to store k-V key-value pairs, but in the case of concurrent multi-threading, HashMap is not safe, because when a put element is put, if the expansion operation, known as rehash, is triggered, If the hash value of the two elements is the same, they may be represented in a linked list under the same array, resulting in a closed loop, resulting in an infinite loop during get. So HashMap is thread-unsafe.

Is there a secure Map container? Yes, there are currently three secure Map containers available in the JDK:

  • HashTable
  • Collections.synchronizedmap (synchronous wrappers provide method)
  • ConcurrentHashMap

Take a look at the first two containers, both of which take advantage of very coarse-grained synchronization and tend to have low performance under high concurrency. As a result, all concurrent operations must compete for the same lock. When a thread performs a synchronization operation, other threads can only wait, greatly reducing the efficiency of concurrent operations.

Taking a look at SynchronizedMap, the synchronization wrapper provided by Collections, we can look at the source code for SynchronizedMap:

private static class SynchronizedMap<K,V> implements Map<K,V>, Serializable { private static final long serialVersionUID = 1978198479659022715L; private final Map<K,V> m; // Backing Map final Object mutex; // Object on which to synchronize SynchronizedMap(Map<K,V> m) { this.m = Objects.requireNonNull(m); mutex = this; } SynchronizedMap(Map<K,V> m, Object mutex) { this.m = m; this.mutex = mutex; } public int size() { synchronized (mutex) {return m.size(); } } public boolean isEmpty() { synchronized (mutex) {return m.isEmpty(); } } public boolean containsKey(Object key) { synchronized (mutex) {return m.containsKey(key); } } public boolean containsValue(Object value) { synchronized (mutex) {return m.containsValue(value); } } public V get(Object key) { synchronized (mutex) {return m.get(key); } } public V put(K key, V value) { synchronized (mutex) {return m.put(key, value); } } public V remove(Object key) { synchronized (mutex) {return m.remove(key); } } public void putAll(Map<? extends K, ? extends V> map) { synchronized (mutex) {m.putAll(map); } } public void clear() { synchronized (mutex) {m.clear(); }}... }Copy the code

SynchronizedMap () does not lock synchronized, but uses “this” as a mutex, so SynchronizedMap is strictly the same as HashTable. There was no real improvement.

The third ConcurrentHashMap is also the main character of this article. Compared with the first two secure Map containers, it has great changes in design and thinking, and greatly improves the concurrent efficiency of Map. In terms of the implementation of ConcurrentHashMap itself, there are significant differences between versions, typically JDK1.7 and JDK1.8. The ConcurrentHashMap implementations will also be introduced in this article, with the main focus on JDK 1.8. I personally feel that JDK 1.7 has become a thing of the past and there is no need to delve into it.

Implementation of ConcurrentHashMap in JDK 1.7

In JDK 1.7 and earlier versions, ConcurrentHashMap proposed a solution to the problem of HashTable locking, which involves splitting a large hash table into several smaller hash tables. To improve hash table performance, lock small hash tables. ConcurrentHashMap in JDK1.7 introduces Segment objects, which break the hash table into segments, each of which can be treated as a fine-grained HashMap.

The Segment object inherits the ReentrantLock class because it becomes a lock that keeps the data safe. Maintains an internal hash table in the Segment object through a HashEntry array. Each HashEntry represents a K-V in the map, where a linked list is formed if a hash conflict occurs.

In JDK1.7, the overall structure of ConcurrentHashMap can be described as follows:

ConcurrentHashMap 1.7 Storage structure

One of our main concerns with ConcurrentHashMap is how to solve the security problem caused by the expansion of HashMap put. Take a look at how ConcurrentHashMap solves this problem in JDK1.7, starting with the put method:

public V put(K key, V value) { Segment<K,V> s; if (value == null) throw new NullPointerException(); Int hash = hash(key.hashcode ()); int hash = hash(key.hashcode ()); int j = (hash >>> segmentShift) & segmentMask; // unsafe. getObject (s = (Segment<K,V>) unsafe. getObject // nonvolatile; recheck (segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment s = ensureSegment(j); return s.put(key, hash, value, false); }Copy the code

In the put method, a second hash is used to reduce the number of possible hash conflicts. An Unsafe call calls the Segment directly based on its hash value, and the put method on the Segment object is used to add data to the container. Segment object put method source code is as follows:

Final V put(K key, int hash, V value, Boolean onlyIfAbsent) { Ensure that acquiring a lock scanAndLockForPut will go to find whether you have the same key Node ConcurrentHashMap. HashEntry < K, V > Node = tryLock ()? null : scanAndLockForPut(key, hash, value); V oldValue; try { ConcurrentHashMap.HashEntry<K,V>[] tab = table; int index = (tab.length - 1) & hash; ConcurrentHashMap.HashEntry<K,V> first = entryAt(tab, index); for (ConcurrentHashMap.HashEntry<K,V> e = first;;) {// Update the existing key if (e! = null) { K k; if ((k = e.key) == key || (e.hash == hash && key.equals(k))) { oldValue = e.value; if (! onlyIfAbsent) { e.value = value; ++modCount; } break; } e = e.next; } else { if (node ! = null) node.setNext(first); else node = new ConcurrentHashMap.HashEntry<K,V>(hash, key, value, first); int c = count + 1; If (c > threshold && tab.length < MAXIMUM_CAPACITY) rehash(node); else setEntryAt(tab, index, node); ++modCount; count = c; oldValue = null; break; } } } finally { unlock(); } return oldValue; }Copy the code

The Segment itself is a lock, so when adding data, the corresponding Segment block is locked, and other threads cannot manipulate the Segment. This ensures data security. In JDK1.7, ConcurrentHashMap is used to expand the HashEntry array of the Segment. Other threads cannot manipulate the segment’s hash table, which solves the closed-loop problem caused by the put data in the HashMap.

So much for ConcurrentHashMap in JDK1.7, we need to know that ConcurrentHashMap in JDK1.7 uses segwise locking to solve the problem of HashMap insecurity.

Implementation of ConcurrentHashMap in JDK1.8

In JDK1.8, ConcurrentHashMap has changed dramatically, as can be seen from the amount of code implemented, which is less than 2000 lines in 1.7 and over 6000 lines in 1.8. Without further ado, let’s see what has changed.

Starting with container security, ConcurrentHashMap in 1.8 abandons the staging technology in JDK1.7 and adopts CAS + synchronized to ensure concurrency security. However, the ConcurrentHashMap implementation keeps the Segment definition, which is only for serialization compatibility and is not structurally useful. Here is a knowledge point about CAS mechanism:

Mechanism of the CAS

The typical application of CAS is AtomicInteger. CAS is a type of atomic operation that ensures that a read/write operation is atomic. CAS compares the value in memory with the expected value and changes the value in memory only when the two values are equal. CAS provides thread safety in concurrent scenarios while ensuring performance. In Java, CAS is implemented in the Sun.misc. Unsafe class, which defines a large number of native methods.

public final native boolean compareAndSwapObject(Object o, long offset, Object expected, Object x); public final native boolean compareAndSwapInt(Object o, long offset, int expected, int x); public final native boolean compareAndSwapLong(Object o, long offset, long expected, long x);Copy the code

We can only see the definition, not the implementation. The implementation depends on the operating system, so we can forget about this and simply understand what the parameters in the method mean:

  • O: Target operation object
  • Offset: indicates the memory offset address of the target operand
  • -Penny: You expected me to.
  • X: Updates the value

Although the CAS mechanism is lock-free, secure and efficient, it also has some disadvantages, which are summarized as follows:

  • Loop checks can be long, but you can limit the number of loop checks
  • Atomic operations can only be performed on one shared variable
  • There is an ABA problem (ABA problem means that the value of the target variable changes from A to B and back to A between two CAS check operations, but CAS does not see the change. The value of the target variable remains A for CAS, so the CAS operation continues to update the value of the target variable.)

In terms of storage structure, ConcurrentHashMap in JDK1.8 abandonshashentry structure and adopts the form of Node array plus linked list (when the linked list length is larger than 8, it is converted into a red-black tree), which is very similar to HashMap structure. Node Node design is as follows:

static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; volatile V val; volatile Node<K,V> next; . Omit... }Copy the code

Like a HashMap, the Key field is final, indicating that the Key is immutable during its lifetime, and the val field is volatile, which ensures that the val field is visible.

The ConcurrentHashMap structure in JDK1.8 is shown as follows:

JDK1.8 ConcurrentHashMap Structure diagram

ConcurrentHashMap (); ConcurrentHashMap (); ConcurrentHashMap

public ConcurrentHashMap() {}Copy the code

Finding that nothing is done without this constructor, why design this way? This has the advantage of implementing lazy-load and avoiding initialization overhead, which is one of the many complaints about ConcurrentHashMap in JDK1.7.

Again, let’s take a look at the problem we’re concerned about, how to solve the problem of HashMap expansion insecurity, and read the source code for ConcurrentHashMap with this problem in mind. In this paper, we mainly discuss the two methods of adding (putVal) and expanding (transfer), and other methods will not be introduced one by one.

PutVal method

Instead of calling putVal directly, the ConcurrentHashMap element is added using the PUT method

public V put(K key, V value) { return putVal(key, value, false); }Copy the code

In other words, putVal is a specific new method. PutVal is an implementation of putVal. PutVal is annotated in the source code.

Final V putVal(K key, V value, Boolean onlyIfAbsent) { Direct return if (key = = null | | value = = null) throw new NullPointerException (); Int hash = spread(key.hashcode ()); Int binCount = 0; For (concurrenthashmap. node <K,V>[] TAB = table;) { ConcurrentHashMap.Node<K,V> f; int n, i, fh; / / lazy lazy - load, loading the containers, if the current TAB is empty, initialize TAB if container (TAB = = null | | (n = TAB. Length) = = 0) TAB = initTable (); Unsafe.getobjectvolatile (); // Unsafe.getobjectVolatile (); // Unsafe.getobjectVolatile (); Else if ((f = tabAt(TAB, I = (n-1) & hash)) == null) {//// If (casTabAt(TAB, I, null) new ConcurrentHashMap.Node<K,V>(hash, key, value, null))) break; // No lock when adding to empty bin} // If fh == -1, expansion is being performed. Else if ((fh = f.hash) == MOVED) // helpTransfer(TAB, f); Else {// If none of the preceding conditions is met, a hash conflict exists, and synchronized is used to lock. V oldVal = null; V oldVal = null; Synchronized (f) {if (tabAt(TAB, I) == f) {if (fh >= 0) { For (concurrenthashmap. Node<K,V> e = f; ++binCount) { K ek; / / here comes to the same key to put will cover the original value of the if (e.h ash = = hash && (= e.k (ek ey) = = key | | (ek! = null && key.equals(ek)))) { oldVal = e.val; if (! onlyIfAbsent) e.val = value; break; } ConcurrentHashMap.Node<K,V> pred = e; Next = new concurrenthashMap. Node<K,V>(hash, key, value, null); if ((e = e.ext) == null) {// Insert the end of the list. break; }}} else if (f instanceof ConcurrentHashMap. TreeBin) {/ / the Node is red and black tree Node ConcurrentHashMap. Node < V > K, p; binCount = 2; if ((p = ((ConcurrentHashMap.TreeBin<K,V>)f).putTreeVal(hash, key, value)) ! = null) { oldVal = p.val; if (! onlyIfAbsent) p.val = value; }}}} // After insertion, determine whether the list length is greater than 8, and convert to a red-black tree if (binCount! = 0) { if (binCount >= TREEIFY_THRESHOLD) treeifyBin(tab, i); // If the same key exists, return the original value if (oldVal! = null) return oldVal; break; AddCount (1L, binCount); return null; }Copy the code

There are more detailed comments in the source code, if you want to understand the detailed implementation, you can read the source line by line, here we will make a summary of putVal method, putVal method mainly does the following things:

  • In ConcurrentHashMap, the key val field is not allowed to be null. Therefore, the first step is to verify the key value. The key and val fields cannot be null. Otherwise a NullPointerException is returned, unlike a HashMap, which is allowed to be null.
  • Check whether the container is initialized. If the container is not initialized, call initTable to initialize it.
/** * Initializes table, using the size recorded in sizeCtl. */ private final Node<K,V>[] initTable() { Node<K,V>[] tab;  int sc; While (= (TAB table) = = null | | TAB. The length = = 0) {/ / negative initializing or expansion, waiting for the if (= sizeCtl (sc) < 0) / / spin waiting Thread. The yield (); // lost initialization race; Just spin // Perform CAS with sizeCtl set to -1, Else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {// CAS grabs the lock try {// Initializes the table with the specified length. Or the default value if 16 ((TAB = table) = = null | | TAB. The length = = 0) {/ / sc at the time of initialization user could be customized, if there is no custom, is the default int n = > 0 (sc)? Sc: DEFAULT_CAPACITY; Node<K,V>[] nt = (Node<K,V>[])new Node<? ,? >[n]; table = tab = nt; // Specify the size of the next capacity expansion, equivalent to 0.75 x N sc = n - (n >>> 2); } } finally { sizeCtl = sc; } break; } } return tab; }Copy the code

Table is essentially an array of nodes. The initialization process is also the initialization process of the Node array. The CAS policy is used in the initialization operation. The initialization process is as follows:

If the sizeCtl value is smaller than 0, then ConcurrentHashMap is being initialized. If other threads fail to initialize, ConcurrentHashMap can be replaced. Preempt the flag sizeCtl to -1 based on the CAS policy, indicating that ConcurrentHashMap is being initialized, then the table is constructed and the sizeCtl value is updated

  • The third step is to find the subscript position of the array based on the hash value after the double hash. If there is no node in this position, that is, there is no hash conflict, then add the data to the container using CAS unlocked mode and end the loop.
  • Step 4, if did not meet the third step, will determine whether the container is being expanded other threads for operation, if being expansion and other threads, are added to the operation, join the army of expansion (ConcurrentHashMap expansion operation USES is multithreaded way, then we’ll talk about), add capacity does not jump out of the loop, This ensures that while the container is expanding, no other threads will be adding data, which also ensures the security of the container.
  • Step 5: If hash conflict occurs, the linked list operation or red-black tree operation will be performed (if the linked list tree exceeds 8, the linked list will be changed to red-black tree operation). During the linked list or red-black tree operation, synchronized lock will be used to lock the head node, ensuring that only one thread can modify the linked list at the same time and preventing the linked list from being looped.
  • AddCount (1L, binCount), this operation will update the size, determine whether to expand, addCount method source code is as follows:
// X is passed 1, check is passed binCount from putVal, 0 if there is no hash collision, Conflict would be greater than 1 private final void addCount (long x, int check) {ConcurrentHashMap. CounterCell [] as; long b, s; If ((as = counterCells)! = null || ! U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) { ConcurrentHashMap.CounterCell a; long v; int m; boolean uncontended = true; if (as == null || (m = as.length - 1) < 0 || (a = as[ThreadLocalRandom.getProbe() & m]) == null || ! (uncontended = U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) { fullAddCount(x, uncontended); return; } if (check <= 1) return; s = sumCount(); If (check >= 0) {concurrenthashmap. Node<K,V>[] TAB, nt; int n, sc; The first condition is that map.size is larger than sizeCtl, which means that it needs to be expanded // 2. While (s >= (long)(sc = sizeCtl) && (TAB = table)! = null && (n = tab.length) < MAXIMUM_CAPACITY) { int rs = resizeStamp(n); If (sc < 0) {if ((sc >>>) RESIZE_STAMP_SHIFT)! = rs || sc == rs + 1 || sc == rs + MAX_RESIZERS || (nt = nextTable) == null || transferIndex <= 0) break; If (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) transfer(TAB, nt); } // If the capacity is not expanded, update the SC: move the identifier 16 bits to the left and then + 2. So it becomes a negative number. The high 16 bits are identifiers, Else if (U.compareAndSwapInt(this, SIZECTL, SC, (rs << RESIZE_STAMP_SHIFT) + 2)) Transfer (TAB, null); s = sumCount(); }}Copy the code

The addCount method does two things:

1. Add one to the map size

2. Check whether the capacity needs to be expanded or is being expanded. If capacity expansion is required, call the capacity expansion method. If capacity expansion is in progress, help it expand capacity.

Expansion transfer method

Expansion transfer method is a very awesome method, before looking at the specific transfer source code, we will first understand when the expansion operation will be triggered, if there is no accident, the following two cases may trigger expansion operation:

  • After calling the put method to add elements, the addCount method will be called to update the size and check whether expansion is needed. When the number of array elements reaches the threshold, the Transfer method will be triggered

  • The tryPresize operation is triggered. The tryPresize operation will trigger the expansion operation. There are two situations where the tryPresize operation will trigger:

    • The first case: When the number of linked list elements of a node reaches the threshold of 8, the linked list needs to be converted into a red-black tree. Before the structure conversion, the array length n is determined to be smaller than the threshold MIN_TREEIFY_CAPACITY. The default value is 64. If it is smaller, the tryPresize method is called to double the array length, and the Transfer method is triggered to reposition the node.
    • Second case: The tryPresize operation is triggered first during the putAll operation.

TryPresize = tryPresize

TryPresize method source code

Well, know when will trigger expansion, let’s take a look at the source of expansion transfer method, this is also a hard bone, very difficult to chew, I hope I can try to make it clear, the source of transfer method is as follows:

private final void transfer(ConcurrentHashMap.Node<K,V>[] tab, ConcurrentHashMap.Node<K,V>[] nextTab) { int n = tab.length, stride; If ((stride = (NCPU > 1)? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE) stride = MIN_TRANSFER_STRIDE; // subdivide range // nextTab is empty, Initializing a new array instantiates if (nextTab == null) {// The new array is twice the size of the original array ConcurrentHashMap.Node<K,V>[] nt = (ConcurrentHashMap.Node<K,V>[])new ConcurrentHashMap.Node<? ,? >[n << 1]; nextTab = nt; } catch (Throwable ex) { // try to cope with OOME sizeCtl = Integer.MAX_VALUE; return; } // Update the member variable nextTable = nextTab; TransferIndex = n; // update transferIndex = n; // advance: int nextn = nexttab.length; // advance: int nextn = nexttab.length; // Create a FWD node for placeholder. When another thread finds a FWD node in the slot, it skips the node. ConcurrentHashMap.ForwardingNode<K,V> fwd = new ConcurrentHashMap.ForwardingNode<K,V>(nextTab); // Advance specifies whether to continue decrement to the next bucket. If it is true, it can continue to decrement to the next bucket. Boolean finishing = false; Research case (" I ", "bound") for (int I = 0, bound = 0;;) { ConcurrentHashMap.Node<K,V> f; int fh; while (advance) { int nextIndex, nextBound; if (--i >= bound || finishing) advance = false; else if ((nextIndex = transferIndex) <= 0) { i = -1; advance = false; } else if (U.compareAndSwapInt (this, TRANSFERINDEX, nextIndex, nextBound = (nextIndex > stride ? nextIndex - stride : 0))) { bound = nextBound; i = nextIndex - 1; advance = false; } } if (i < 0 || i >= n || i + n >= nextn) { int sc; if (finishing) { nextTable = null; table = nextTab; sizeCtl = (n << 1) - (n >>> 1); return; } if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) { if ((sc - 2) ! = resizeStamp(n) << RESIZE_STAMP_SHIFT) return; finishing = advance = true; i = n; // recheck before commit } } else if ((f = tabAt(tab, i)) == null) advance = casTabAt(tab, i, null, fwd); else if ((fh = f.hash) == MOVED) advance = true; Already processed else {synchronized (f) {if (tabAt(TAB, I) == f) {if (tabAt(TAB, I) == f) { Concurrenthashmap. Node<K,V> ln, hn; concurrenthashmap. Node<K,V> ln, hn; concurrenthashmap. Node<K,V> ln, hn; if (fh >= 0) { int runBit = fh & n; ConcurrentHashMap.Node<K,V> lastRun = f; for (ConcurrentHashMap.Node<K,V> p = f.next; p ! = null; p = p.next) { int b = p.hash & n; if (b ! = runBit) { runBit = b; lastRun = p; } } if (runBit == 0) { ln = lastRun; hn = null; } else { hn = lastRun; ln = null; } for (ConcurrentHashMap.Node<K,V> p = f; p ! = lastRun; p = p.next) { int ph = p.hash; K pk = p.key; V pv = p.val; If ((ph&n) == 0) ln = new concurrenthashMap. Node<K,V>(ph, pk, pv, ln); else hn = new ConcurrentHashMap.Node<K,V>(ph, pk, pv, hn); } setTabAt(nextTab, i, ln); // Insert setTabAt(nextTab, I + n, hn) at nextTable I + n; // Insert ForwardingNode at table I to indicate that the node has already been setTabAt(TAB, I, FWD); advance = true; } else if (f instanceof ConcurrentHashMap. TreeBin) {/ / if TreeBin, according to the red-black tree for processing, processing logic in accordance with the above / / the logic of the red-black tree with the same node, . The final will be divided into high and low ConcurrentHashMap TreeBin < V > K, t = (ConcurrentHashMap. TreeBin > < K, V) f; ConcurrentHashMap.TreeNode<K,V> lo = null, loTail = null; ConcurrentHashMap.TreeNode<K,V> hi = null, hiTail = null; int lc = 0, hc = 0; for (ConcurrentHashMap.Node<K,V> e = t.first; e ! = null; e = e.next) { int h = e.hash; ConcurrentHashMap.TreeNode<K,V> p = new ConcurrentHashMap.TreeNode<K,V> (h, e.key, e.val, null, null); if ((h & n) == 0) { if ((p.prev = loTail) == null) lo = p; else loTail.next = p; loTail = p; ++lc; } else { if ((p.prev = hiTail) == null) hi = p; else hiTail.next = p; hiTail = p; ++hc; }} // If the number of nodes in the tree is less than or equal to 6, then convert to a linked list, otherwise, create a new tree ln = (lc <= UNTREEIFY_THRESHOLD)? untreeify(lo) : (hc ! = 0)? new ConcurrentHashMap.TreeBin<K,V>(lo) : t; hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) : (lc ! = 0)? new ConcurrentHashMap.TreeBin<K,V>(hi) : t; setTabAt(nextTab, i, ln); setTabAt(nextTab, i + n, hn); setTabAt(tab, i, fwd); advance = true; } } } } } }Copy the code

Want to know the specific implementation details, please read the source line by line, if you do not understand, welcome message exchange, like putVal method, we also summarize the transfer method, transfer roughly do the following events:

  • Step 1: Calculate the number of buckets that each thread (CPU) can process at a time. Calculate the number of buckets (table array number) that each thread (CPU) needs to process according to the Map length. By default, each thread processes 16 buckets at a time.

  • Step 2: Initialize nextTab. If the new table nextTab is empty, initialize nextTab. The default value is twice that of the original table

  • Step 3: Introduce ForwardingNode, advance, and finishing variables to assist expansion. ForwardingNode indicates that the node has been processed and does not need to be processed. Advance indicates whether the thread can be moved to the next bucket (true: Finishing indicates whether expansion is completed (true: expansion is completed, false: expansion is not completed). The specific logic is not mentioned

  • Step 4: Skip some other details and go directly to data migration. Synchronized lock will be added in the process of data transfer to lock the head node and synchronize the operation to prevent data insertion into the linked list when putVal is used

  • Step 5: For data migration, if the node on the bucket is a linked list or red-black tree, the node data will be divided into low and high levels. The calculation rule is to perform bit operation (&) on the hash value of the node and the length of the table container before capacity expansion. If the result is 0, If the result is not 0, the data will be placed in the high position of the new table (the current table is the I position, the position in the new table is I + the length of the current table container).

  • Step 6: If the bucket is mounted to a red-black tree, not only do you need to separate the low-order and high-order nodes, but you also need to determine whether the low-order and high-order nodes will be stored in the new table as a linked list or red-black tree.

I hope I can make it clear. I will not talk about other methods of ConcurrentHashMap here. ConcurrentHashMap is extensive and profound, and it will take some time to thoroughly study it. This is my insight into the implementation of ConcurrentHashMap. I hope this article will help you in your study or work. Thank you for your support.

Recommended reading:
A three-minute guide to redis high availability architecture
Java shallow copy, deep copy, how much do you know?