David John Wheeler famously said that ** “any problem in computer science can be solved by adding another layer of indirection.” ** If one layer is not enough, another layer can be added. The last half of the sentence is my (* ̄)  ̄), although a little joke, but also can explain some problems. It is true that computer science solves enormous problems through layer upon layer of abstraction and encapsulation.
To recap: In the beginning, programmers typed in binary instructions to manipulate hardware, which was inefficient and time-consuming; Then came the operating system, with files, processes and threads, address space abstraction of disk, CPU and memory, unified and simplified hardware access; Machine languages are not user-friendly, so there is assembly language, intermediate language (such as C), high-level language (such as Java) packaging, the final implementation has to be converted to machine language; Naked high-level languages are still used unwell and feel inefficient in development, so various frameworks (such as Spring and Hibernate)……
With this layer upon layer of abstraction, it’s easy to implement a feature like periodic file writing in just a few lines of code. However, too many abstraction layers will lead to some puzzling problems for users on the top layer. Let’s use a practical case of pseudo-sharing to illustrate
public class FalseSharing { private static AtomicLong time = new AtomicLong(0); public static void main(String... args) throws InterruptedException { int testNum = 50; for (int i = 0 ; i< testNum; Thread Thread = new Thread(new Job()); thread.start(); thread.join(); } System.out.println(time.get()/1000/testNum + " us,avg"); } static class Job implements Runnable{ @Override public void run() { int number = 8; int iterationNumber = 20000; CountDownLatch countDownLatch = new CountDownLatch(number); Obj[] objArray = new Obj[number]; for (int i = 0; i < number; i++) { objArray[i] = new Obj(); } long start = System.nanoTime(); for (int i = 0; i < number; i++){ int ii = i; Thread thread = new Thread(new Runnable() { int iterationNumberInner = iterationNumber; @Override public void run() { while (iterationNumberInner-->0){ objArray[ii].aLong+=1L; } countDownLatch.countDown(); }}); thread.start(); } try { countDownLatch.await(); } catch (InterruptedException e) { e.printStackTrace(); } long end = System.nanoTime(); time.getAndAdd(end-start); } } @Contended private static final class Obj{ private volatile long aLong = 8L; //8Bytes // private volatile long a=2L,b=2L,c=2L,d=2L,e=2L,f=2L,g=2L; / / * * * * *}}Copy the code
The entire code is here, and to avoid the Java JIT (which is also a layer of abstraction), we force the use of interpreted mode by adding -xint to each execution. On my machine (4core,8processor, core-i7), I run this code directly and get the result 1 is 4594 us, avG level. Based on the result 1, I uncomment the line //***** and get the result 2 is 3916 us, AVG level. Running a parameter based on result 1 plus -xx: -restrictContEnded makes @ContEnded work to get a result of 33466 us, AVg.
At this time the top user will be inexplicable, how many more fields running time is reduced instead? Why is it even shorter with @contended? At this level of abstraction in Java code, there’s no problem at all, so what’s the problem?
We know that each core in a CPU has its own Cache, L1 is private, L2, L3 and other lower levels may be private, or may be shared by different cores. These different levels of caching (in the order of a few ns at a time) are used to bridge the gap between fast CPU speeds (typically a few tenths of a ns per cycle) and slow memory accesses (in the order of tens of ns per cycle), in CacheLine Size: The basic unit is N Bytes (core-i7 is 64). According to the principle of locality, N Bytes around the access variable are copied to the Cache at a time. If an object is not enough Bytes, it may share a Cache line with several objects. If one thread flushes the Cacheline, it will invalidate the Cache of other threads, which will slow down access to lower levels of Cache or even memory.
So to go back to the question, adding a few fields can increase the space taken up by the object to some extent and reduce the chances of sharing the CacheLine, so the access time is reduced. The @contended method allows each object to have one CacheLine, which directly helps us avoid pseudo sharing, so the access time is reduced. To solve this problem, it is not only possible to understand the Java level of abstraction (syntax, JDK APIS, etc.), but also the operating system and even CPU chip principles.
For example, the JVM provides us with a layer of abstraction for manual memory management in C/C++, which frees us up. We enjoy it, but if we don’t understand this layer of abstraction and encapsulation, we will be dumbsided in OOM.
In conclusion, it is true that any problem in computer science can be solved by adding a layer of indirection, but it is precisely because of layer upon layer of abstraction and packaging that it is difficult to locate the problem, and you do not know which layer the problem is at. Therefore, in order to improve the technical level, we should not only know what it is (see the packaging of the top layer) but also know why it is (see the packaging of the bottom layer). If we can understand or understand some of each layer, problems can be located by intuition to a large extent. Even if it is not intuitive, we can also use various means to debug. The abstraction at the top level is often left to bugs.
Access to the original text from MageekChiu.