Said that the interview series has been completed, the results found that or really sweet, well, I found that MY Java foundation did not write, so this is a sequel, the first sequel please keep.

What’s the difference between processes and threads?

A process is an execution of a program. It is an independent unit of the system for resource allocation and scheduling. Its role is to enable concurrent execution of programs to improve resource utilization and throughput.

Due process is the basic unit of the resource allocation and scheduling, because the process creation, destruction, switch to produce a lot of time and space of overhead, the number of process cannot too much, and the thread is smaller than the process can run independently of the basic unit, he is a process of an entity that can reduce the costs of time and space programs concurrently, Enables the operating system to have better concurrency.

Threads own few system resources, only resources that are essential to the run time, such as program counters, registers, and stacks, while processes own the heap and stack.

Do you know the synchronized principle?

Synchronized is an atomic built-in lock provided by Java. These built-in and invisible locks are also known as monitor locks. Synchronized adds the monitorenter and Monitorexit bytecode instructions to and from the synchronized code block after compilation. It relies on the operating system’s underlying mutex implementation. His main role is to implement atomicity operations and solve the memory visibility problems of shared variables.

Monitorenter attempts to acquire the lock of the object. If the object is not locked or the lock has been acquired, the lock counter is +1. At this point, other threads competing for the lock will enter the waiting queue.

When monitorexit is executed, the counter is set to -1. When the counter is set to 0, the lock is released and threads in the waiting queue continue to compete for the lock.

Synchronized is the exclusive lock. When one thread acquies the lock, other threads must wait for it to release it before they can acquire it, and since Java threads are one-to-one counterparts to OS native threads, the transition from user mode to kernel mode when a thread is blocked or woken up can be very performance consuming.

In memory semantics, the locking process clears the shared variables in working memory and then reads them from main memory, while the lock release process writes the shared variables in working memory back to main memory.

Actually, most of the time I think it’s a monitorenter, but let’s be a little more specific for clarity.

If you dig deeper into the source code, synchronized actually has two queues, waitSet and entryList.

  1. When multiple threads enter a synchronized block of code, they first enter entryList
  2. When a thread has acquired the monitor lock, it is assigned to the current thread and the counter is +1
  3. If a thread calls wait, it releases the lock, sets the current thread to null, counters -1, and enters a waitSet waiting to be awakened. If a thread calls notify or notifyAll, it enters an entryList contention lock
  4. If the thread finishes executing, the lock is also released, the counter is -1, and the current thread is set to NULL

What about the lock optimization mechanism?

Since JDK1.6, synchronized itself has been improving its lock mechanics, and in some cases it’s not a serious lock anymore. Optimization mechanisms include adaptive locking, spin locking, lock elimination, lock coarsening, lightweight locking, and bias locking.

The status of the lock from low to high is no lock -> biased lock -> lightweight lock -> heavyweight lock. The process of promotion is from low to high, and degradation is possible under certain conditions.

Spin-locking: Since most of the time locks are held for very short periods of time and shared variables are locked for very short periods of time, there is no need to suspend threads, and context switching back and forth between user and kernel mode seriously affects performance. The concept of spin is to let the thread perform a busy loop, which can be interpreted as doing nothing to prevent the thread from switching from user mode to kernel mode. Spin-locking can be enabled by setting -xx :+UseSpining. The default number of spins is 10, which can be set using -xx :PreBlockSpin.

Adaptive lock: An adaptive lock is an adaptive spin lock. The spin time is not fixed, but determined by the previous spin time on the same lock and the state of the lock holder.

Lock elimination: Lock elimination occurs when the JVM detects that some synchronized block of code has no data race at all, that is, no need to lock.

Lock coarsening: Lock coarsening is when multiple operations lock the same object, extending the synchronization of the lock beyond the entire sequence of operations.

Biased locking: When threads access synchronized blocks access to lock in the object lock records in the head and the stack frame store thread ID, biased locking this thread enters the synchronized block again after all don’t need the CAS to lock and unlock, biased locking will bias the first thread gets the lock, if no other threads subsequent won this lock, thread holding the lock will never need to synchronize, Conversely, when other threads compete for a biased lock, the thread holding the biased lock releases the biased lock. You can use the setting -xx :+UseBiasedLocking to open the bias lock.

Lightweight lock: When code enters a synchronized block, the JVM will use CAS to attempt to acquire the lock. If the update is successful, the JVM will mark the status bit in the object header as a lightweight lock. If the update fails, the current thread will attempt to spin to acquire the lock.

The whole lock upgrade process is very complex, I try to remove some useless links, simple to describe the mechanism of the entire upgrade.

In simple terms, bias locking is done by comparing the bias of the object headers to the thread ID without even requiring CAS, while lightweight locking is done by using CAS to modify the record and spin of the object headers, and heavyweight locking is done by blocking all but the thread that owns the lock.

What exactly does an object header contain?

In our usual Hotspot virtual machine, the in-memory layout of objects actually consists of three parts:

  1. Object head
  2. The instance data
  3. Alignment filling

While the object header contains two parts, the contents of the Mark Word change with the lock flag bit, so just say the storage structure.

  1. The data required to run the object itself, also known as Mark Word, is the key for lightweight and biased locking. The details include the hashcode of the object, generational age, lightweight lock pointer, heavyweight lock pointer, GC tag, bias lock thread ID, bias lock timestamp.
  2. Stores a pointer to a type, that is, a pointer to the class metadata that determines which class an object is an instance of.

In the case of an array, it also contains the length of the array

For locking, how about ReentrantLock? What’s the difference between him and synchronized?

Compared to synchronized, ReentrantLock requires explicit lock acquisition and release, and the efficiency difference between ReentrantLock and synchronized is about the same as that of JDK7 and JDK8. The main differences are as follows:

  1. Wait is interruptible. When the thread holding the lock does not release it for a long time, the waiting thread can choose to abandon the wait and work on another task.
  2. Fair locks: Both synchronized and ReentrantLock are non-fair locks by default, but ReentrantLock can be changed by passing arguments to the constructor. It’s just that using fair locking causes performance to deteriorate dramatically.
  3. Bind multiple conditions: ReentrantLock can bind multiple Condition objects at the same time.

Already based on AQS (AbstractQueuedSynchronizer abstract queue synchronizer) implementation. Stop. I know the problem. Let me explain the PRINCIPLE of AQS.

AQS maintains a state bit, which is changed by CAS(CompareAndSwap) when attempting to lock the thread. If the value is successfully set to 1 and the current thread ID is assigned, the lock is successful. Once the lock is obtained, other threads will be blocked and enter the blocking queue to spin. When the thread that acquired the lock releases the lock, it wakes up the thread in the blocking queue. When it releases the lock, it sets the state back to 0 and the current thread ID to null.

How does CAS work?

The CAS is called CompareAndSwap. It compares and swaps with the processor’s instructions to keep the operations atomic. It contains three operands:

  1. The memory address of the variable. V stands for
  2. The old expected value, A is
  3. The new value to be set, B for

When CAS is executed, B is used to update V only if V is equal to A, otherwise the update operation will not be performed.

So what are the disadvantages of CAS?

There are three main disadvantages of CAS:

ABA problem: The ABA problem refers to the CAS update process, when the value read is A, and then ready to assign the value is still A, but it is possible that the value of A is changed to B, and then changed back to A. This CAS update vulnerability is called ABA. The ABA problem does not affect the final effect of concurrency in most scenarios.

Java has an AtomicStampedReference to solve this problem by adding two fields, the expected flag and the updated flag. The update checks not only the value but also whether the current flag is equal to the expected flag, and only updates if all are equal.

Long loop time and high overhead: If the spin CAS method is not successful for a long time, it will bring a lot of overhead to the CPU.

Atomicity is guaranteed only for one shared variable: atomicity is guaranteed for only one shared variable, but not for multiple, which can be handled with an AtomicReference or synchronized.

Ok, so how does HashMap work?

A HashMap, which consists primarily of arrays and linked lists, is not thread-safe. The core points are how put inserts data, how GET queries data, and how it expands. The main difference between JDK1.7 and 1.8 is the modification of the head and tail plugins. Head plugins tend to cause HashMap lists to loop forever, and the addition of red-black trees after 1.8 improves performance.

Put Indicates the process of inserting data

Inserting elements into a map is done first by hashing the key and then by hashing the array length -1 ((n-1)& hashing), both of which are powers of 2 and so are equivalent to modulo, but bitwise is much more efficient. Found in the array position after, if there is no element in the array direct deposit, judge whether the key is the same, conversely the key the same cover, otherwise it will be inserted into the list of the tail, if more than eight the length of the list, will be transformed into red and black tree, finally determine whether array length more than the default load factor is the length of * 12, more than the expansion.

Get Query data

Query data is relatively simple, first calculate the hash value, and then go to the array query, red black tree to red black tree search, the list to traverse the list query can be.

Resize Expansion process

The process of scaling up is to recalculate the hash of the key and copy the data to the new array.

So how do you use Map in a multithreaded environment? ConcurrentHashmap?

Multithreaded environment can use Collections. SynchronizedMap synchronization lock mode, you can also use the HashTable, but the way the synchronization performance is not up to standard, obviously and ConurrentHashMap more suitable for high concurrency scenario.

ConcurrentHashmap has been changed in JDK1.7 and 1.8, 1.7 uses Segment+HashEntry, 1.8 uses CAS+synchronized+Node instead of Segment. Red-black trees have also been added to avoid performance problems caused by long lists.

Section 1.7 the lock

Structurally, ConcurrentHashMap uses a Segment locking mechanism, which contains an array of segments. Segments inherit from ReentrantLock, and segments contain an array of HashEntries. HashEntry itself is a linked list structure with the ability to hold keys, values and Pointers to the next node.

Each Segment is a HashMap. The default length of the Segment is 16, which means that 16 threads can be written concurrently.

Put the process

In fact, the entire process is very similar to HashMap, except that the specific Segment is located first, and then the ReentrantLock is used to operate. I have simplified the following process, because it is basically the same as HashMap.

  1. Compute the hash, locate the segment, and initialize the segment if it is empty
  2. ReentrantLock is used to lock. If the lock fails to be acquired, spin is attempted. If the spin exceeds the number of times, the acquisition is blocked to ensure that the lock is successfully acquired
  3. If you go through a HashEntry, just like a HashMap, you just replace the keys and the hash in the array, and if they don’t exist, you insert them into the list, and the list does the same thing

Get the process

Get is simple. The key is hashed to the segment, and then traversed to the specific element. Note that value is volatile, so get does not require a lock.

1.8 the CAS + synchronized

1.8 Discard segmented locks and use CAS+synchronized. Similarly, HashEntry is changed to Node and red-black tree implementation is added. Mainly look at the put process.

Put the process

  1. First, compute the hash, iterate over the node array, and initialize node with CAS+ spin if node is empty
  2. If the current array position is empty, write data directly through CAS spin
  3. If hash==MOVED, it indicates that capacity expansion needs to be performed
  4. If neither of these is met, synchronized writes the data, which also determines the list and the red-black tree. The list is written in the same way as HashMap, which overwrites the key hash, and the other way around, which tail interpolates, which converts the list to a red-black tree if it is longer than 8

Get the query

Get is very simple, compute the hash from the key, if the key hash is the same, return it, if it’s a red-black tree, if it’s a red-black tree, then go through the linked list.

Do you know the principle of volatile?

Volatile is a lighter option than synchronized’s approach to solving the memory visibility problem of shared variables, without the overhead of context switching. Using volatile to declare variables ensures that the updated value is immediately visible to other threads. Volatile solves the problem of memory visibility by using memory barriers to ensure that no reorders of instructions occur.

We know that threads read shared variables from main memory into working memory and then write the results back to main memory, but this can cause visibility problems. For example, suppose we have a dual-core CPU architecture with two levels of caching, including L1 and L2 levels of caching.

  1. Thread A first gets the value of the variable X. Since both levels of cache are initially empty, thread A reads X directly from main memory. Assuming that X is initially 0, thread A reads both values of X to 1 and writes them back to main memory. The situation of cache and main memory is shown below.

  1. Thread B also reads the value of the variable X. Since the L2 cache already has X=1, thread B reads the value directly from the L2 cache. Thread B then changes X to 2 and writes it back to L2 and main memory. The X value at this time is shown in the following figure.

    If thread A wants to retrieve the value of the variable X, it will not be visible to the memory because it already has X =1 in the L1 cache. The value of B changed to 2 will not be visible to thread A.

If X is volatile and thread A reads X again, the CPU will force thread A to reload the latest value from main memory to its working memory according to the cache consistency protocol, rather than using the value from the cache directly.

As for the memory barrier issue, volatile fixes will add different memory barriers to ensure that visibility issues are properly implemented. The barriers here are based on what is provided in the book, but the actual memory barriers are different depending on the CPU architecture and the reordering strategy. For example, on x86 platforms, there is only one memory barrier, StoreLoad.

  1. The StoreStore barrier ensures that normal writes on it are not reordered by volatile writes
  2. The StoreLoad barrier ensures that volatile reads and subsequent volatile reads will not be reordered
  3. LoadLoad barrier, which prohibits volatile reads from being reordered with subsequent normal reads
  4. LoadStore barrier, which disallows volatile reads and subsequent ordinary write reordering

So what do you understand about the JMM memory model? Why is the JMM needed?

With the development of CPU and memory speed difference, resulting in the CPU speed is much faster than memory, so now the CPU added cache, cache can be generally divided into L1, L2, L3 level cache. Based on the example above, we know that this causes problems with cache consistency, so adding cache consistency protocol also causes problems with memory visibility, and compiler and CPU reordering causes problems with atomicity and order. The JMM memory model is a set of normative constraints on multi-threaded operations. Because it is impossible for Employee Chen’s code to be compatible with all cpus, we can shield the memory access differences between different hardware and operating systems through JMM, so as to ensure the consistent memory access effect of Java programs on different platforms, and also ensure the correct execution of programs in efficient concurrency.

Atomicity: The Java memory model guarantees atomic operations with read, Load, assign, use, Store, write, and lock and unlock, which correspond directly to the synchronized keyword’s Monitorenter and Monitorexit bytecode instructions.

Visibility: As mentioned in the above answer, Java guarantees visibility by thinking of it as volatile, synchronized, and final.

Orderness: Java guarantees orderness through volatile and synchronized due to reordering of processors and compilers.

Happen – before the rules

Although instruction reordering improves concurrent performance, the Java virtual machine will impose some rules on instruction reordering, and it is not possible to change the execution position of all instructions at will. The main points are as follows:

  1. For each operation in a single thread, happen-before any subsequent operations in that thread
  2. Volatile writes happen-before and subsequent reads to the variable
  3. Synchronized unlocks happen-before subsequent locks on the lock
  4. A write to a final variable happens -before the read of an object in the final field, and happens -before the subsequent read of a final variable
  5. The transitive rule, A precedes B, B precedes C, then A must occur before C

What is working memory and main memory?

Main memory can be thought of as physical memory, and in the Java memory model is actually part of the virtual machine memory. The working memory is the CPU cache. It can be a register or a L1, L2, L3 cache. It is possible.

How ThreadLocal works?

A ThreadLocal is a thread-local variable that creates a copy in each thread, so accessing internal copy variables between threads is a good idea, isolating the threads from each other, and swapping space for time in contrast to synchronized.

ThreadLocal has a static inner class, ThreadLocalMap, which in turn contains an array of entries. The Entry itself is a weak reference, and its key is a weak reference to the ThreadLocal. Entry stores key value pairs.

The purpose of a weak reference is to prevent memory leaks. If it is a strong reference, the ThreadLocal object will not be reclaimed until the thread terminates, and the weak reference will be reclaimed during the next GC.

If the key and ThreadLocal objects are recycled, there will be an entry with a null key but a value that will never be accessed until the thread is finished.

However, as long as ThreadLocal is used properly and the Entry object is removed by calling the remove method after use, this problem should not actually occur.

What are the reference types? What’s the difference?

Reference types are divided into four types: strong, weak, and empty:

  1. A strong reference refers to A common assignment in code, such as A A = new A(). Objects with strong references are never collected by GC.
  2. A SoftReference can be described as a SoftReference, which is an object that is useful but not required. The system reclaims such referenced objects before a memory overflow occurs.
  3. WeakReference can be described by WeakReference, whose strength is a little lower than soft reference. The object with WeakReference will be reclaimed in the next GC, regardless of whether the memory is sufficient.
  4. A virtual reference, also known as a PhantomReference, is the weakest reference relationship and can be described as PhantomReference. It must be used with the ReferenceQueue. Similarly, a virtual reference is recycled when GC occurs. Virtual references can be used to manage out-of-heap memory.

Do you know how thread pools work?

First, thread pools have several core parameter concepts:

  1. Maximum number of threads maximumPoolSize

  2. Number of core threads corePoolSize

  3. Active time keepAliveTime

  4. Block the workQueue

  5. Reject policy RejectedExecutionHandler

When a new task is submitted to the thread pool, the execution process is as follows:

  1. When we submit a task, the thread pool creates a number of task threads based on the corePoolSize to execute the task
  2. When the number of tasks exceeds the corePoolSize, subsequent tasks will be queued in the blocking queue
  3. When the blocking queue is full, it will continue to create (maximumPoolSize-corePoolSize) a number of threads to execute the task. If the task is completed, MaximumPoolSize -corePoolSize Additional threads are created to wait for keepAliveTime before being automatically destroyed
  4. If maximumPoolSize is reached and the blocking queue is still full, it will be processed according to different denial policies

What are the rejection strategies?

There are four main rejection strategies:

  1. AbortPolicy: Discard the task directly and throw an exception. This is the default policy
  2. CallerRunsPolicy: Only the caller’s thread is used to process the task
  3. DiscardOldestPolicy: Discards the latest task in the waiting queue and executes the current task
  4. DiscardPolicy: Discards the task without throwing an exception

In addition, the audience has been opened, to be honest, even without talking, you can also learn a lot in the group. Add my wechat, remarks into the group, I pull you!