Interview questions are getting tougher these days. Every junior programmer has to ask the JVM, interviews build rockets, and job screwing is the norm. No way, thinking to do some interview questions for you, tangled or give you a good talk about the specific principle of it, so the “deep understanding of Java virtual machine” and some official unofficial documents are studied, feel hair is not enough.

Hardware efficiency and consistency

Concurrency problems in physical computers have a lot in common with Java virtual machines.

In order to solve the speed contradiction between processor and memory, cache is introduced.

The introduction of caching brings with it a problem: cache consistency. In a multiplex processor system, each processor has its own cache, and they share the same main memory. When the computing tasks of multiple processors all involve the same main memory area, the cache data of each processor may be inconsistent.

To solve the problem of consistency, it is necessary for each processor to follow some protocol when accessing the cache, and to operate according to the protocol when reading and writing. We will mention the term “memory model” several times, which can be understood as a process abstraction for reading and writing access to a particular memory or cache under a particular operating protocol. Different physical machines can have different memory models. The Java virtual machine also has its own memory model.

In addition to increasing the cache, the processor optimizes out-of-order execution of the code. Instruction reordering in the corresponding JVM.

Java memory model

The Java Virtual Machine Specification attempts to define a Java Memory Model (JMM) to screen out the differences in Memory access between different hardware and operating systems, so that Java programs can achieve consistent Memory access effects on various platforms.

Previously (C, for example) directly used the memory model of physical hardware and the operating system, resulting in a set of programs with different errors on different platforms.

Main memory vs. working memory

The main goal of the Java memory model is to define the access rules for variables in a program, the low-level details of storing variables into and out of memory in the virtual machine.

Variables here do not include local variables and parameters, because the thread is private, will not be shared, naturally there is no race problem.

The Java memory model specifies that all variables are stored in main memoryEach thread also has its own working memory (analogous to a cache), which holds a main memory copy of variables used by the thread.All operations by the thread on variables (reading and assigning values) must be done in working memoryYou cannot read or write variables directly from main memory (including volatile variables). Different threads cannot directly access variables in each other’s working memory, and the transfer of variable values between threads needs to be completed through the main memory.

Intermemory operation

Implementation details of how a variable is copied from main memory to working memory, synchronized from working memory back to main memory, and so on. The following eight operations are defined in the Java memory model to accomplish this, and virtual machine implementations must ensure that each of these operations is atomic.

  • Lock (Main memory)
  • Unlock (Main memory)
  • Read (main memory)
  • Load working memory
  • Use (working memory)
  • Assign working memory
  • Store (Working memory)
  • Write (Main memory)

Note: The Java memory model only requires that these two operations (read, load, Store, write) be executed sequentially, not consecutively.

This means that other instructions can be inserted between read and write, for example

Read A, read B, load B, load A

Atomicity, visibility and order

The Java memory model is built around how atomicity, visibility, and orderliness are handled during concurrency

atomic

An operation either happens at all or does not happen at all

  • Basic data reads and writes are atomic (32-bit machines beware of longs and doubles)
  • Operations between synchronized blocks are also atomic

visibility

When one thread changes a value, other threads know about it immediately

  • Like normal variables, volatile variables are mediated by main memory, but reads and writes between volatile and main memory occur immediately
  • In addition to volatile, synchronized (which is synchronized to main memory before unlock), and final (as long as no this escapes)

order

In this thread, all operations are ordered.

Observed in another thread, all operations are out of order (refers to “instruction reorder” phenomenon and “working memory and main memory synchronization delay” phenomenon)

  • Volatile itself contains the semantics of instruction reordering
  • Synchronized allows only one thread to lock a variable at a time

Principle of antecedent

There is an antecedent principle in the Java memory model, which is an important means of determining whether data is contentitious and threads are safe.

Let’s take an example of what is the antecedent principle

/ / A thread
i = 1;
/ / thread B
j = i;
/ / C thread
j = 2;
Copy the code

If A occurs first in B and C does not enter, then j must have the value of 1. If C comes in, it’s still only A that happens first to B, then j could be 2 or it could be 1

There are some natural antecedent principles in the Java memory model, of which the following two are introduced;

  • Monitor Lock Rule: An UNLOCK operation occurs first when a subsequent Lock operation is performed on the same Lock. It must be emphasized here that the same lock, and “behind” refers to the chronological order.
  • Volatile Variable Rule: Writes to a volatile Variable occur first before reads to that Variable occur later, again in chronological order.

Volatile variable rule

For volatile reads and writes, if there is a write before read in the actual execution time

(For example, thread A’s assign operation precedes thread B’s use operation.)

, there is a prior occurrence of writes before they are read, thus ensuring that any variables that are visible to volatile writes are also visible to subsequent volatile reads. If a common variable is changed, it does not guarantee visibility, even if it is written before reading, if it is not on the same thread.

private int value = 0;
public void setValue(int value) {
    this.value = value;
}
public int getValue(a) {
    return value;
}
Copy the code
  • Let’s say we have threads A and B, and in chronological order, A calls setValue(2) first and B calls getValue() then, B gets an indeterminate value.
  • The value can be volatile, and since setter methods do not depend on the original value, it will be thread-safe.

Thread safety

Thread-safe sorting in Java:

  • immutable
  • Absolute thread safety
  • Relative thread safety
  • The thread is compatible with
  • Thread opposite

This escape

Throws its own this reference and is copied (accessed) by other threads before the constructor construction is fully completed (that is, the instance initialization phase is not complete).

class ThisEscape {
    int i;
    static ThisEscape obj;
    public ThisEscape(a) { // Due to instruction reordering, it is not certain which of these two items should proceed first
        i = 1;
        obj = this; }}// If thread A hasn't had time to assign A value to I, thread B uses the obj. Causes a null pointer. Or in other cases, the object is incomplete.
Copy the code

What happens when This escapes?

(1) Explicitly throw this as above

(2) In the constructor of the inner class using the external class: the inner class access the external class is no condition, and no cost, so that when the external class is not completed initialization, the inner class will try to get initialized variables

  • Start the thread in the constructor: The thread task that is started is the inner class. If XXX. This accesses the external class instance in the inner class, it will access variables that have not been initialized

  • The event is registered in the constructor, because listening for events in the constructor has a callback function (which may access the instance variable of the operation), and event listening is generally asynchronous. Callbacks to uninitialized variables can occur before the initialization is complete.

immutable

  • finalDecoration: As long as an object is constructed correctly (i.e., does not occurthisQuote escape)
  • If multiple threads share a basic datatype, use it only when redefining itfinalKeyword decoration ensures that it is immutable. If it is an object, it needs to be immutable by itself (e.g.StringSo whatever you do, it doesn’t change its original value. Is it determined byfinalModified)

Absolute thread safety

Most classes that label themselves as thread-safe in the Java API are not thread-safe.

The Vector class, for example, has methods like add() and get() modified by synchronized, but that doesn’t mean it doesn’t require atomic operations.

public void method(Vector<Double> vec) {
    synchronized(vec) {
        vec.set(...). ; vec.get(...) ; }}Copy the code

Relative thread safety

Thread-safe refers to the common sense in which a single operation on an object is thread-safe. For example, Vector.

The thread is compatible with

Refers to what we usually call thread insecurity. The object itself is not secure, but can be safely used in a concurrent environment by properly using synchronization on the calling side.

Thread opposite

Means that no matter what the operation is, it cannot be used concurrently in a multithreaded environment. Because Java is inherently multithreaded, threading opposition is rare.

Thread-safe implementation

The mutex synchronization

Synchronization refers to ensuring that shared data is used by only one thread at a time (or some, when using semaphores) when accessed concurrently by multiple threads.

Mutex is a means of synchronization: critical sections, mutex, and semaphores are common implementations of mutex.

Mutual exclusion synchronization: mutual exclusion is the cause, synchronization is the result. Mutual exclusion is the method, synchronization is the destination.

We take Reentrant with a few more advanced features than synchronized:

  • We can think of synchronized as a subset of Reentrant, but after JDK6 optimization, they perform about the same, while synchronized is easy to use
  • Fair locks: They both use unfair locks (Reentrant can change to fair locks, but performance degrades)
  • Locking binds multiple conditions: Reentrant can bind multiple Condition objects, whereas synchronized cannot.
  • Wait interruptible: When the thread holding the lock does not release the lock for a long time, the waiting thread can give up waiting to do something else. (Synchronized blocks all the time, and blocking or waking up a thread requires an OS transition from user state to core state, which can be costly.)

Nonblocking synchronization

The problem with mutex synchronization is the performance cost of thread blocking and waking up. This is called blocking synchronization and is a pessimistic development strategy because locking is done regardless of contention.

Non-blocking synchronization: An optimistic evolution strategy that involves doing things first regardless of the risk (such as the CAS algorithm), but it requires the development of a hardware instruction set (to atomise multi-step operations) so that some seemingly multi-step operations can be done in one step. It does not solve the ABA problem, however, and it may be faster to switch to traditional mutex synchronization.

Asynchronous scheme

Synchronization is not necessarily related to thread safety. If a method does not inherently involve sharing data, it naturally does not require any synchronization measures to ensure its correctness.

  • Reentrant code
  • Thread-local storage (e.g., using the ThreadLocal class)

Lock the optimization

Lock optimization techniques implemented by virtual machine development teams from JDK5 to JDK6

Spin-locking and adaptive spin

The biggest performance impact of mutex synchronization is the implementation of blocking, which requires switching between user and kernel modes (consuming more than CPU resources).

If we have multiple processors or processor cores on the machine that allow two threads to execute in parallel, we ask one of the threads to wait for a while without giving up the processor’s execution time, which in this case is a busy loop (spin). Default busy cycle 10 times, you can also set.

If the time of the cycle is no longer fixed, but determined by itself, that’s adaptive spin.

Lock elimination

Eliminates locks that are detected to be unable to compete for shared data. You may think you’re not locking, but the compiler locks it. Such as:

public String add(String s1, String s2, String s3) {
    return s1 + s2 + s3;
}
// Javac after conversion
public String add(String s1, String s2, String s3) {
    StringBuffer sb = new StringBuffer();
    sb.append(s1);
    sb.append(s2);
    sb.append(s3);
    return sb.toString();
}
Copy the code

Lock coarsening

If a series of operations repeatedly lock the same object, even if the lock occurs in the body of the loop, frequent synchronized operations can lead to unnecessary performance losses, even if threads are not competing.

Such as the append() method above. The virtual machine will extend the lock until the first append() operation and until after the last append() operation.

Lightweight lock

Biased locking

In-depth Understanding of the Java Virtual Machine

False sharing

What is pseudo-sharing

In order to solve the problem of the speed difference between the main memory and the CPU in the computer system, one or more level of Cache is added between the CPU and the main memory. It is stored in rows inside the Cache.

When the CPU accesses a variable, it first checks the cache to see if it has the variable. If it does not, it retrieves the variable directly from main memory.

The CPU usually reads data in a contiguous block, called a Cache Line. There can be multiple variables in a cache line. So it’s usually faster to access contiguously stored data than it is to access randomly stored data, and it’s usually faster to access array structures than it is to access chain structures, because arrays are usually allocated contiguously in memory. (PS. The JVM standard does not state that “arrays must be allocated in contiguous space.”)

The size of the cache row is typically 64 bytes, which means that even if only 1 byte of data is being processed, the CPU will read at least 64 consecutive bytes of data in which the data resides.

Cache invalidation: According to the simple understanding of the MESI protocol used by mainstream cpus to ensure cache validity, if the cache row being used by one core is modified by another core, the cache row is invalidated and the cache needs to be re-read.

False Sharing: Frequent cache failures occur if threads with multiple cores are operating on different variable data in the same cache row, even though at the code level the two threads are operating on data that has no relationship at all. This kind of unreasonable resource competition is called False Sharing, which will seriously affect the efficiency of concurrent execution of machines.

How to avoid fake sharing

Prior to Java8, you could avoid this problem by populating a variable’s Cache Line with a fill field when creating the variable. Such as:

public static void FilledLong(a) {
    public long value = 0L;
    public long a1, a2, a3, a4, a5, a6;
}
// If the Cache Line is 64 bytes and the long is 8 bytes, then there are 56 bytes, and the class FilledLong takes up 8 bytes, which is 64 bytes.
Copy the code

JDK8 provides sun.misc.Contended annotations to solve this problem.

@sun.misc.Contended
public static void FilledLong(a) {
    public long value = 0L;
}
Of course, this annotation can also be used to modify fields.
@sun.misc.Contended
long a;
// By default, this annotation only applies to Java core classes, such as those under rt packages.
// If you want to use this annotation in a user path, you need to add the JVM parameter: -xx :RestrictContended, and the default padding size is 128 bytes.
// Custom width: -xx :ContendedPaddingWidth
Copy the code

In the end, I wish you all success as soon as possible, get satisfactory offer, fast promotion and salary increase, and walk on the peak of life. If you can please give me a triple support for me yo, we will see you next time