One, foreword

Use of concurrent Java programming practice “it is not easy to write a correct program, it is harder to write normal concurrent program”, compared to the situation of the order, multi-threaded thread safety issues are subtle and surprising, because in the absence of proper synchronization multithreaded each operation sequence is unpredictable, This article is a brief introduction to synchronization strategy in multi-threaded situations.

Alibaba long-term recruitment of Java R&D engineers P6, P7, P8 and other uncapped levels, interested in the resume can be sent to me, indicate the department and work place: [email protected]_

What is thread safety

Thread-safety is a problem that occurs when multiple threads access a state variable and one or more threads write to the state variable without any synchronization, resulting in dirty data or other unforeseeable results. The primary synchronization strategy in Java is the use of the Synchronized keyword, which provides a reentrant exclusive lock.

What is the visibility problem of shared variables

To talk about visibility, we first need to introduce the Memory model in Java when multithreading shared variables.

The Java memory model specifies that all variables are stored in main memory, and that threads copy variables from main memory into their working memory when they use variables.

When a thread operates on a shared variable, the process is as follows:

  • The thread first copies the shared variable from main memory into its own working memory
  • The variables in working memory are then processed
  • Update variable values to main memory after processing

So what happens if threads A and B simultaneously process A shared variable? First of all, they will go to the three process, if the thread A copy Shared variables in the working memory, and have to update the data but it has not updated main memory, then thread B copy Shared variables in your working memory for processing, processing, thread A to get their processing result more updates to the main memory, Thread B does not process the result of thread A’s processing, that is, the value of the variable processed by thread A is not visible to thread B. This is the visibility problem of shared variables.

The reason the memory that makes up the shared variables is not visible is because the three-step process is not an atomic operation, and we see that using proper synchronization can solve this problem.

We know that ArrayList is thread-unsafe because its accessors have no synchronization policy, resulting in dirty data and unexpected results. Here’s how to fix it.

This is thread unsafepublic class ArrayList<E> 
{

    public E get(int index) {
        rangeCheck(index);

        return elementData(index);
    }

    public E set(int index, E element) {
        rangeCheck(index);

        E oldValue = elementData(index);
        elementData[index] = element;
        returnoldValue; }}Copy the code

4. Atomicity

4.1 introduction

Hypothesis check A perform operations performed on Bo Ao and site B, then from A watch, when Bo operation on thread B, then Bo all perform operation, or does not perform, we call Ao and Bo are atomic operation, when in the design of the counter is usually read the current value first, then + 1, and then update will be variable, It is a read – change – write process, which must be atomic operation.

	public class ThreadNotSafeCount {

		private  Long value;

		public Long getCount(a) {
			return value;
		}

		public void inc(a) { ++value; }}Copy the code

The code above is thread unsafe because there is no guarantee that ++value is an atomic operation. Method one is Synchronized as follows:

	public class ThreadSafeCount {

		private  Long value;

		public synchronized Long getCount(a) {
			return value;
		}

		public synchronized void inc(a) { ++value; }}Copy the code

Note that you cannot simply use volatile to modify the value for synchronization because the value depends on the current value.

Thread safety can be achieved with Synchronized, that is, visibility and synchronization. However, Synchronized is an exclusive lock, and threads that do not acquire the internal lock will be blocked. The answer is yes.

4.2 Atomic variable classes

Atomic variables are less cumbersome than locks. For example, AtomicLong represents a Long value and provides the get and set methods. The get and set methods have the same pronunciation as volatile because AtomicLong is the real Long variable that uses the volatile modifier inside. Atomic increment and decrement operations are also provided, so the counter can be changed to:


	public class ThreadSafeCount {

		private  AtomicLong value = new AtomicLong(0L);

		public  Long getCount(a) {
			return value.get();
		}

		public void inc(a) { value.incrementAndGet(); }}Copy the code

The advantage of atomic class operation compared with synchronized is that it does not cause thread suspension and rescheduling, because it uses cas’s non-blocking algorithm internally.

Commonly used atomic class variables for: AtomicLong, AtomicInteger, AtomicBoolean, AtomicReference.

Five CAS

CompareAndSet (CompareAndSet), CAS has three operands: memory location, old expected value, and new value. The new value replaces the memory location with the old expected value. If so, replace it with the new value I gave it. If not, return the value that tells me the current value of the memory location. This is an atomic instruction supplied by the processor. The AtomicLong increment described above is implemented in this way:

    public final long incrementAndGet(a) {
        for (;;) {
            longcurrent = get(); (1)long next = current + 1; (2)if(compareAndSet (current and next)) (3)returnnext; }}public final boolean compareAndSet(long expect, long update) {
        return unsafe.compareAndSwapLong(this, valueOffset, expect, update);
    }

Copy the code

IncrementAndGet incrementAndGet incrementAndGet incrementAndGet incrementAndGet incrementAndGet incrementAndGet incrementAndGet incrementAndGet incrementAndGet Thread B will execute 3 because current=1 and the actual value of the variable is 2, so if false, continue the loop. If no other thread increments the variable, thread B will update the variable to 3 and exit.

This uses an infinite loop for polling checks using CAS, which wastes CPU resources to a certain extent, but avoids thread context switching and scheduling compared to locking.

What is reentrant lock

If a thread is blocked when it attempts to acquire a lock held by another thread, will it block when it attempts to acquire a lock it has already acquired? If there is no need to block, then we say that the lock is a reentrant lock, that is, the lock can access the code locked by the lock as many times as the thread acquires the lock.

Let’s look at an example of what can happen if the lock is not reentrant.

public class Hello{
     public Synchronized void helloA(a){
        System.out.println("hello");
     }

     public Synchronized void helloB(a){
        System.out.println("hello B"); helloA(); }}Copy the code

If the helloB function is not reentrant, the call will be deadlocked because the thread is holding the lock and waiting for it, so helloB will never acquire the lock.

Actually inside is a reentrant lock, lock the synchronized keyword management methods, for example, the principle of reentrant lock is in internal thread maintains a label marking the lock is the threads, then the associated a counter, counter value is 0 at the beginning, that the lock is not occupied by any thread, when a thread to get the lock, The counter will be changed to 1. When other threads acquire the lock, they find that the owner of the lock is not themselves, so they block. However, when the thread that acquired the lock again finds that the owner of the lock is itself, the counter value will be +1. The thread identifier inside the lock is reset to null, and the blocking thread is woken up to acquire the lock.

7, Synchronized keywords

7.1 introduce Synchronized

A synchronized block is a mandatory built-in lock provided by Java. Each Java object can implicitly act as a lock for synchronization. These built-in locks are called internal locks or monitor locks, and executing code automatically acquires the internal lock before entering a synchronized block. The block is blocked when another thread accesses it. The thread that holds the internal lock will release the internal lock after either exiting the synchronized block normally or after an exception is thrown, and the blocked thread can then acquire the internal lock and enter the synchronized block.

7.2 Synchronized Synchronization instance

An internal lock is a kind of mutex lock. Specifically, one thread can acquire the lock at the same time. When one thread obtains the lock and does not release it, the other threads can only wait.

For the ArrayList described above, you can use synchronized to handle visibility issues.

usesynchronizedSynchronize the methodpublic class ArrayList<E>
{

    public synchronized  E get(int index) {
        rangeCheck(index);

        return elementData(index);
    }

    public synchronized E set(int index, E element) {
        rangeCheck(index);

        E oldValue = elementData(index);
        elementData[index] = element;
        returnoldValue; }}Copy the code

As shown in the figure, after thread A acquires the internal lock and enters the synchronization code block, thread B is also ready to enter the synchronization block, but A has not released the lock, so B enters the wait now. Using synchronization can ensure that the variable value during thread A acquires the lock until it releases the lock is visible to B after it acquires the lock. That is, when THREAD B starts executing the code synchronization block that A is executing, we can see all the variable values of A’s operation. Specifically, when thread B gets the value of B, it can guarantee that it gets the value of 2. In this case, the visibility is achieved because thread A enters the synchronized block to modify variable values, and then flusher the values to the main memory before exiting the synchronized block, whereas thread B emptying the local memory and retrieving variable values from the main memory before entering the synchronized block. Note, however, that all threads use the same lock.

Note that the Synchronized keyword causes on-the-spot context switching and thread scheduling.

Eight, ReentrantReadWriteLock

Synchronized can be used to achieve synchronization, but the disadvantage is that only one thread can access the shared variable at the same time. However, under normal circumstances, there is no need to synchronize the shared variable for multiple read operations. Multiple read threads cannot execute the shared variable at the same time in synchronized, and in most cases, the number of read operations is more than that of write operations. ReentrantReadWriteLock allows read and write separation. Multiple threads are running to read at the same time, but at most one write thread exists.

The above method can now be modified to:


public class ArrayList<E>
{
  private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();

  public E get(int index) {

		Lock readLock = readWriteLock.readLock();
		readLock.lock();
		try {
			return list.get(index);
		} finally{ readLock.unlock(); }}public E set(int index, E element) {

		Lock wirteLock = readWriteLock.writeLock();
		wirteLock.lock();
		try {
			return list.set(index, element);
		} finally{ wirteLock.unlock(); }}}Copy the code

For example, if the code obtains the readLock from readwritelock.readlock (), multiple threads can acquire the readLock at the same time. If the set method obtains the writeLock from readwritelock.writelock (), only one thread can acquire the writeLock. Other threads block while acquiring a write lock until the write lock is released. If a thread has acquired a read lock, then one thread must wait until the write lock is released. If one thread has acquired a write lock, then all the threads that acquired the read lock must wait until the write lock is released. Therefore, compared with synchronized, multiple readers exist at the same time, thus increasing the concurrency. Note that the user is required to invoke Lock and unlock operations

Volatile variables

To avoid invisibility problems, Java also provides a weak form of synchronization, using the volatile keyword. This keyword ensures that updates to a variable are visible to other threads. When a variable is declared volatile, the thread writes without caching the value in a register or elsewhere. When it reads, the thread retrieves the latest value from main memory, rather than using the current thread’s copy of the value.

Volatile provides visibility guarantees, but it cannot be used to construct compound atomicity operations, meaning that when a variable depends on another variable or when a variable value is updated, the new value depends on the current old value. The similarity with synchronized is shown in the figure below

As shown in the figure, thread A modiates volatile B, and thread B reads the change. All values visible to thread A before writing to volatile B are visible to thread B after b reads volatile B, and the values of variables A and B operated by thread B on A are visible. The memory semantics of volatile are similar to synchronized in that writing a volatile value is equivalent to exiting a synchronized block. Reading volatile values is equivalent to entering a synchronized block (local memory values are emptied and the latest values are fetched from main memory).

The following Integer is also thread-unsafe because no synchronization measures have been taken

	public class ThreadNotSafeInteger {

		private int value;

		public int get(a) {
			return value;
		}

		public void set(int value) {
			this.value = value; }}Copy the code

Use the synchronized keyword as follows:

	public class ThreadSafeInteger {

		private int value;

		public synchronized int get(a) {
			return value;
		}

		public synchronized  void set(int value) {
			this.value = value; }}Copy the code

This is equivalent to using volatile for synchronization as follows:

	public class ThreadSafeInteger {

		private volatile int value;

		public int get(a) {
			return value;
		}

		public void set(int value) {
			this.value = value; }}Copy the code

Synchronized and volatile are equivalent here, but not in all cases, and volatile is generally used only if all of the following conditions are met

  • Variable values are written independently of the current value of the variable, or can be guaranteed to be modified by only one thread.
  • Write variable values independent of the participation of other variables.
  • Variable values cannot be fettered for other reasons.

In addition, locking guarantees both visibility and atomicity, whereas volatile guarantees only visibility of variable values.

Note that the volatile keyword does not cause thread context switching or thread scheduling.

Optimistic and pessimistic locks

10.1 pessimistic locks

Pessimistic locking: The conservative attitude (pessimistic) of the index data being modified by the outside world, keeping the data locked during the entire data processing process. The realization of pessimistic locking often relies on the locking mechanism provided by the database. Database implementation is to data records before the operation, the record to add an exclusive lock, if the lock fails to obtain, it means that the data is being modified by other threads, then wait or throw an exception. If the lock is successful, the record is acquired, modified, and the exclusive lock is released after the transaction commits. Select * from table where… for update;

Pessimistic lock is a lock first, then access strategy, processing lock makes database generate additional costs, and increase the chances of deadlocks, and in the case of multiple threads to read only won’t produce line data inconsistency problem, it is not necessary to use the lock, will only increase the system load, reduce the concurrency, because when a transaction locking the actual orderdate, other read the record of the transaction can only wait.

10.2 optimistic locking

Optimistic locking is the opposite of pessimistic locking. It believes that data will not cause conflicts in general, so it will not add exclusive locks before accessing records. Instead, it will formally detect whether data conflicts are detected when data is submitted for update, and let users decide what to do according to the number of rows returned by update. Optimistic locking does not use the locking mechanism provided by the database; it is usually done by adding a version field to the table or using business state. Specific can refer to: www.atatech.org/articles/79…

Optimistic locks are not locked until commit time, so there are no locks or deadlocks.

Exclusive and shared locks

Locks are divided into exclusive locks and shared locks depending on whether they can be held by a single thread or multiple threads. An exclusive lock guarantees that only one thread can read or write at any time. A ReentrantLock is a mutex implemented in an exclusive manner. Read and write are mutually exclusive. For example, the ReadWriteLock read/write lock allows a resource to be read by multiple threads or written by one thread at the same time, but both cannot be performed at the same time.

An exclusive lock is a pessimistic lock that starts with a mutex each time a resource is accessed. This limits concurrency because read operations do not affect data consistency. An exclusive lock allows only one thread to read data at the same time, and other threads must wait for the current thread to release the lock before they can read.

The shared lock is an optimistic lock, which relaxes the lock conditions and allows multiple threads to read simultaneously.

Fair lock and unfair lock

According to the preemption mechanism of thread acquiring lock, lock can be divided into fair lock and unfair lock. Fair lock means that the order of thread acquiring lock is determined by the time of thread locking, that is, the thread with the earliest lock will be the first to acquire the lock, that is, the first-come-first-served FIFO order. Non-fair locks run break-ins, which are first come, not first served.

ReentrantLock provides fair and unfair lock implementations: Fair lock ReentrantLock pairLock = new ReentrantLock(true); ReentrantLock pairLock = new ReentrantLock(false); If the constructor passes no arguments, the default is an unfair lock.

Use unfair locks when there is no fairness requirement, because fair locks incur performance overhead. Hypothesis thread already holds A lock, then thread B requests the lock will be suspended, when A thread A lock is released, if the current thread C also needs to acquire the lock, if using A fair lock, depending on the thread scheduling policy thread B and C may be acquiring A lock, either don’t need any other interference at this moment, if you use A fair lock you need to hang C, Let B get the current lock.

The Beauty of Concurrent Programming in Java is available for pre-sale: item.jd.com/12450812.ht…