java multithreading - lock

This is the fourth part of the multi-threading series. Please pay attention to the following for others:

Java multithreading - how do threads come from?

java multithreading - memory model

java multithreading - volatile

If you have read the previous articles about threads, you will have a clear understanding of the implementation principles of threads. With theoretical support, you will have better guidance for practice. Then this article will focus on the practice of threads, and the implementation of threads. A brief introduction to several applications.

The main content of this article:

  1. Classification of thread safety
  2. How to implement thread synchronization
  3. lock optimization

 

thread safety classification

Thread safety is not a binary world that is both true and false. If you sort by the "security degree" of thread safety, java can be divided into the following categories

Immutable . If the data type is modified as final type, it can be guaranteed to be immutable (except for the reference object, the final object attribute is not guaranteed, only the memory address is guaranteed). Immutable objects must be thread-safe, such as the String class, which is a typical immutable object. When calling its subString(), replace() and concat() methods, it will not affect its original value, only Returns a newly constructed String object.

Absolutely thread safe . This definition is extremely restrictive, and for a class to achieve, regardless of the runtime environment, the caller does not need any additional synchronization measures.

Relatively thread safe . This is what we usually call thread safety. It ensures that individual operations on objects are thread-safe, and no additional guarantee work is required. However, for continuous calls in a specific order, the caller may need to use additional synchronization methods to ensure that the call is made. correctness.

Thread compatible . This means that the object itself is not thread-safe, and the caller can use synchronization methods correctly to ensure that the object can be used safely in a concurrent environment. Our commonly used non-thread-safe classes belong to this category.

Thread opposition . This means that code cannot be used concurrently in a multi-threaded environment regardless of whether the caller adopts synchronization measures. We should avoid this.

 

The above may not distinguish between absolute security and relative security. We use an example to distinguish:

public class VectorTest {



    private Vector<Integer> vector = new Vector<Integer>();



    public void remove() {

        new Thread() {

            @Override

            public void run() {

                for (int i = 0; i < vector.size(); i++) {

                    vector.remove(i);

                }

            }

        }.start();

    }



    public void print() {

        new Thread() {

            @Override

            public void run() {

                for (int i = 0; i < vector.size(); i++) {

                    System.out.println(vector.get(i));

                }

            }

        }.start();

    }



    public void add(int data) {

        vector.add(data);

    }



    public static void main(String[] args) {

        VectorTest test = new VectorTest();

        for (int j=0;j<100;j++){

            for (int i = 0; i < 10; i++) {

                test.add(i);

            }

            test.remove();

            test.print();

        }



    }

}

The above code will report an error: ArrayIndexOutOfBoundsException. This exception occurs in the print method. When the remove thread deletes an element, the print method just executes the vector.get() method, and this exception occurs at this time.

We know that vector is thread-safe, and its get(), remove(), size(), add() methods are synchronized by synchronize, but in the case of multi-threading, if you do not do additional synchronization on the method caller down, still not thread-safe. This is what we call relative thread safety, it can not guarantee that when the caller does not need any additional synchronization measures.

 

How to implement thread-safe synchronization

Mutual exclusion synchronization is a common concurrency correctness guarantee method. Synchronization means that when multiple threads access shared data concurrently, the shared data is guaranteed to be used by only one thread at the same time. The common means of mutual exclusion synchronization in java are synchronize and ReentrantLock .

Presumably these two locking methods are known to those who have an understanding of multithreading. We will not discuss the specific usage. Let's talk about the differences and specific scenarios of the two.

We have also mentioned synchronize in the previous text. It is a heavy lock. Since the jvm thread is mapped to the native thread of the operating system, when blocking or waking up the thread, it needs to be converted from the user mode to the kernel. This cost will sometimes cost The time exceeds the code execution time, so the JVM will use spin locks and other methods to execute short synchronization codes for some codes to avoid frequent entry into the core state.

synchronize is a built-in lock provided by jvm and is recommended by jvm. The code it writes is relatively simple and compact. Only when the built-in lock cannot meet the needs, use ReentrantLock.

 

ReentrantLock

So what advanced features can ReentrantLock provide? Let's look at an example;

public void synA() {

    synchronized (lockA) {

        synchronized (lockB) {

            //doSomeThing....

        }

    }

}



public void synB() {

    synchronized (lockB) {

        synchronized (lockA) {

            //doSomeThing....

        }

    }

}

 

The above synchronized code is prone to deadlock when multiple threads call synA and synB respectively. To avoid it, you can only enforce the same order of all calls at the time of writing. In ReentrantLock, polling locks can be used to avoid this problem.

public void tryLockA() {

    long stopTime = System.currentTimeMillis() + 10000l

    while (true) {

        if (lockA.tryLock()) {

            try {

                if (lockB.tryLock()) {

                    try {

                        // doSomeThing.....

                        return;

                    } finally {

                        lockB.unlock();

                    }

                }

            } finally {

                lockA.unlock();

            }

        }



        if (System.currentTimeMillis() > stopTime) {

            return;

        }

    }

}

 

In the above trylock method, if the required lock cannot be obtained, it can be obtained by polling, so that the program can regain control and release the lock that has been obtained. In addition, trylock also provides a timed overloading method, which is convenient for you to obtain the lock within a certain time. If the result cannot be given within the specified time, the program will end with zero.

In addition to providing pollable and timed locks, ReentrantLock can also provide interruptible lock acquisition operations so that shackles can be used in cancelable operations. It also provides lock acquisition operations, fair queues, and non-block-structured locks. These functions greatly enrich the customizability of lock operations.

 

Of course, if you can't use these advanced functions of ReentrantLock, it is recommended to use synchronized. In terms of performance, synchronized can be balanced with ReentrantLock after java6, and according to official reports, the performance of this aspect will be strengthened in the future, because it belongs to the built-in property of JVM and can perform some optimizations, such as the lock object of thread closed lock. Lock elimination optimization, and increasing lock granularity to eliminate lock synchronization, etc. These are difficult to achieve in ReentrantLock.

 

lock optimization

We have also learned above that when multi-threading competes for resources, it will block and wait for other lines that have not competed, and blocking and wake-up require kernel scheduling, which is too expensive for limited CPUs, so JVM will A lot of effort is spent on lock optimization to improve execution efficiency.

Let's look at common lock optimizations.

spin lock

In the state of shared data lock, there are many ways to only hold it for a short period of time, and it is not worthwhile to suspend and resume the thread for such a short period of time. Then the jvm will let the thread waiting for the lock wait for a while, but will not give up the corresponding execution time. In this way, we can see whether the waiting thread is released soon, so as to reduce the pressure of thread scheduling. If the lock is occupied for a short time, this effect is very good, if the time is too long, it will waste the resources of the cycle, and it will lead to waste of resources.

Adaptive Spinlock

The spin lock cannot be processed according to the length of time the lock is occupied, and it will be introduced later. The self-selected time of the self-adaptive spin lock is no longer fixed, but is determined by the previous self-selected time and state of the same lock. decided. It will become smarter.

lock removal

Lock elimination means that the JVM just-in-time compiler eliminates locks that require synchronization on some codes, but are detected as impossible to have shared data competition. Lock elimination detection is mainly based on data support from escape analysis. If it is judged that in a piece of code, all data on the heap will not escape, treat them as data on the stack, and consider them to be thread-private, and synchronization No need to proceed.

Chain coarsening

When writing code, it is always recommended to keep the scope of the synchronized block as small as possible. If a series of operations are repeatedly shackled and unlocked on an object, even appearing in the loop body, even if there is no thread competition, it will lead to unnecessary performance loss. Then for this kind of code, the jvm will expand the granularity of its lock, and only use one synchronization operation for this part of the code.

Lightweight lock

Lightweight locks reduce the performance consumption of traditional heavyweight locks using operating system mutual exclusion without multi-thread competition. The object header in jvm is divided into two parts of information. The first part is used to store the running data of the object itself, called "Mark Word", which is the key to realize lightweight lock. The other part is used to store the pointer of the object type data of the execution method area. When the code enters the synchronization block, if the synchronization object is not locked, the pointer to the lock record in "Mark world" is marked as "01".

If the Mark Word is updated successfully and the thread owns the lock of the object, the pointer of the execution lock bit is marked as "00". If the update fails and the Mark word of the current object does not point to the stack frame of the current thread, it means that the lock object already preempted by another thread. If there are more than two threads contending for the same lock, the lightweight lock will no longer work, the lock is marked "10" and bloated to the heavyweight lock.

Lightweight locks are based on the fact that most of the locks do not compete during the synchronization cycle, so this reduces the performance consumption of mutual exclusion. Of course, if there is a lock competition, in addition to mutual exclusion, the overhead of using mutex will be avoided, and there will be additional operations to synchronously modify the flag bit.

Bias lock

The biased lock is to eliminate the entire synchronization in the absence of competition, even the CAS update operation is not done, it will bias the first thread to obtain it, if in the next execution process, the lock is not used by other Thread acquisition, the thread holding the biased lock will never need to synchronize, and when another thread tries to acquire the lock, the biased mode is declared to end.

 

-----------------------------------------------------------------------------

If you want to see more interesting and original technical articles, scan and follow the official account.

Focus on personal growth and game development, and promote the growth and progress of the domestic game community.

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=324431130&siteId=291194637