Java concurrent programming practice (1)

Thread advantage

  • Reduce program development and maintenance costs
  • Improve resource utilization and system throughput
  • Improve the responsiveness of the user interface
  • Unleash the power of multiprocessing
  • Simplified handling of asynchronous events

risk

  • security issues
  • activity problem
  • performance issues

thread safety

Thread safety means that in a multi-threaded environment, the execution of the program can be guaranteed to be reliable and correct. A class always behaves correctly when multiple threads access it. Then the class is said to be thread-safe.

atomicity

Race Conditions and Compound Operations

A race condition occurs when the correctness of a calculation depends on the timing of alternate execution of multiple threads. The following is a typical race condition in lazy initialization. When multiple threads access the getInstance method at the same time, there is a situation where two threads satisfy the a==null condition at the same time, then two Instances will actually be created, making the program deviation from correctness.

class Instance{
    private static Instance a;
    static Instance getInstance(){
        if(a == null) a = new Instance();
        return a;
    }
}

In the above code, a = new Instance() is a composite operation, which generally includes three steps:
1. new Instance();
2. Point the reference of a to the created object
3. Synchronize the value of the reference of a to the main
Conversely , if the operation is indivisible between thread schedules, then the operation is atomic.

locking mechanism

Built-in lock

Java provides a built-in locking mechanism to support atomicity: synchronized block. It consists of two parts: 1. Object reference as lock. 2. As a block of code protected by this lock.
A method modified with the keyword synchronized is a synchronized code block that spans the entire method body, where the lock of the synchronized code block is the object of the method call. The static synchronized method uses the Class object as the lock.

reentrant

When a thread holds a lock, other threads cannot execute the code block that needs to acquire the lock, but a synchronized code block that needs the lock in the same thread can be executed. This is what reentrancy means.
Reentrancy means that the force of the operation to acquire the lock is "thread", not "call".
Reentrancy is implemented by associating an acquisition count and an owner thread for each lock. When the count value is 0, the lock is considered to be not held by any thread. When a thread requests a lock that is not held, the JVM will note the holder of the lock and set the acquisition count to 1. If the same thread acquires the lock again, the counter will be incremented, and when the thread exits the synchronized block, the counter will be decremented accordingly. When the count value is 0, the lock will be released.

object sharing

visibility

The following program may produce several results, 1. Output 42. 2. Output 0. 3. Unable to terminate. Because the code does not use sufficient synchronization mechanisms, there is no guarantee that the ready and number values ​​written by the main thread will be visible to the thread.

    public class NoVisibility{
        private static boolean ready;
        private static int number;
        public static void main(String[] args){
            new Thread(){
                public void run(){
                    while(!ready)Thread.yield();
                    System.out.println(number);
                }
            }.start();
            number = 40;
            ready = true;
        }
    }

Locking and Visibility

Built-in locks allow the user to ensure that one thread sees the results of another thread's execution in a predictable way. Java also provides a slightly weaker synchronization mechanism, volatile variables, to ensure that other threads are notified of white-bright update operations. When a variable is declared as volatile, both the compiler and the runtime (Runtime) will notice that the variable is shared, so operations on the variable will not be reordered with other memory operations. Volatile variables are not cached in registers or invisible to other processors, so reading a volatile variable always returns the most recently written value.

While volatile variables are convenient, there are some limitations. Volatile variables are usually used as a sign of completion of an operation, an interrupt, or status. But be very careful when using, for example, when performing count++ operations, volatile does not guarantee atomicity (atomic variables provide "read-write-modify" atomic operations)

release and escape

Publishing an object means making the object available for use in code outside of the current scope. For example, save a reference to the object somewhere other code can access it, or return the reference in a non-private method, or pass the reference to a method in another class. Publishing internal state can break encapsulation and make it difficult for programs to position invariant conditions. When an object that should not be released is released, this situation is called escape.

//发布了knowSecrets,并间接发布了Set集合中的Secret对象
public static Set<Secret> knowSecrets;
public void initialize(){
    knownSecrets = new HashSet<Secret>();
}

When you publish an instance of the inner class, you also implicitly publish the outer class, because the instance of the inner class contains an implicit reference to the ThisEscape instance.

public class ThisEscape{
    public ThisEscape(EventSource source){
        source.registerListener(
            new EventListener(){
                public void onEvent(Event e){
                    doSomething(e);
                }
        });
    }
}

Safe object construction: A special example of escape is given in ThisEscape, where the this reference escapes in the constructor. When the inner EventListener instance is published, the outer wrapped ThisEscape instance also escapes. An object is in a predictable and consistent state if and only if its constructor returns. So when you publish an object from an object's constructor, you're just publishing an object that hasn't been constructed yet. This is true even if the statement to release the object is on the last line of the constructor. If the this reference escapes during construction, the object is considered incorrectly constructed.

Do not escape this reference during construction

Common mistakes: 1. Start a thread in the constructor, pay attention to start, there is nothing wrong with creating a thread in the constructor, but it is best not to start it immediately. 2. Calling an overridable instance method in the constructor will also cause the this reference to escape during the construction process.

If you want to register an event listener or start a thread in the constructor, you can use a private constructor and a public factory method to avoid incorrect construction, like this:

public class SafeListener{
    private final EventListener listener;
    private SafeListener(){
        listener = new EventListener(){
            public void onEvent(Event e){
                doSomething(e);
            }
        };
    }
    public static SafeListener newInstance(EventSource source){
        SafeListener safe = new SafeListener();//已经构造完成
        source.registerListener(safe.listener);//正确发布,其它线程在构造完成之前无法访问this
        return sace;
    }
}

thread closure

If the data is located within a single thread, synchronization is not required. This technique is called thread confinement, and it is one of the easiest ways to achieve thread safety. Thread confinement is heavily used in Swing. Swing's visual components and data model objects are not thread-safe. Swing achieves thread-safety by enclosing them in Swing's event dispatch thread.

Ad-hoc thread closure

Ad-hoc thread closure means that the responsibility for maintaining thread closure is entirely undertaken by the program implementation. Ad-hoc thread confinement is very fragile because no language feature, such as visibility modifiers or local variables, can confine an object to the target thread. Due to the fragility of Ad-hoc thread confinement technology, use it as little as possible in the program. If possible, it should use stronger thread confinement technology, such as stack confinement or ThreadLocal class

stack closure

A special case of stack closure thread closure, in which objects can only be accessed through local variables. Just as encapsulation makes it easier for code to maintain invariant conditions, synchronization variables make it easier for objects to be enclosed in threads. One of the properties of local variables is that they are enclosed in the thread of execution. They are on the stack of the executing thread, which cannot be accessed by other threads.

ThreadLocal类

A more formal way to maintain thread confinement is to use ThreadLocal, a class that associates a value in a thread with an object that holds the value. ThreadLocal provides access interfaces or methods such as get and set. These methods have an independent copy for each thread that uses the variable, so get always returns the latest value set by the current executing thread when calling set.

Immutability

Another way to meet synchronization needs is to use Immutable Objects

basic building blocks

Sync container class

Synchronization container classes include Vector and Hashtable. Synchronized wrappers are created by factory methods such as Collections.synchronizedXxx. These classes achieve thread safety by encapsulating their state and synchronizing each common method so that only one thread at a time can access the container's state.

Problems with synchronizing container classes

Synchronized container classes are thread-safe, but in some cases additional client-side locking may be required to protect conforming operations. Common compound operations on containers include:
1. Iterate (repeatedly visit elements until all elements in the container are convenient)
2. Jump (find the next element of the current element according to the specified order)
3. Conditional operations, such as add if none
In a synchronous container class, these conforming operations are still thread-safe without client locking, but they may exhibit unexpected behavior when other threads modify the container concurrently.

The two methods defined in Vector are given below: getLast and deleteLast, both of which perform a "check first" operation. Each method first gets the size of the array and then uses the result to get or remove the last element.

public static Object getLast(Vector list){
    int lastIndex = list.size()-1;
    return list.get(lastIndex);
}
public static void deleteLast(Vector list){
    int lastIndex = list.size()-1;
    list.remove(lastIndex);
}

These methods are actually problematic in a multithreaded environment. If thread A calls getLast on a Vector containing 10 elements, and thread B calls deleteLast on the same Vector at the same time, getLast will throw ArrayIndexOutOfBoundsException under the alternating execution of these operations. Between the call to size and the call to getLast, the Vector gets smaller, so the index value obtained when calling size is no longer valid.

Since the synchronization class must comply with the synchronization policy, that is, support client locking, some new operations may be created. As long as we know which lock should be used, these operations are atomic operations like some other operations of the container. The synchronization container class protects each of its methods with its own lock. By acquiring the lock on the container class, we can make getLast and deleteLast atomic and ensure that the size of the Vector does not change between calls to size and get

public static Object getLast(Vector list){
    synchronized(list){
        int lastIndex = list.size()-1;
        return list.get(lastIndex);
    }
}

public static Object deleteLast(Vector list){
    synchronized(list){
        int lastIndex = list.size()-1;
        list.remove(lastIndex);
    }
}

Also, the length of the Vector may change between calls to size and the corresponding get, a risk that still arises when iterating over the elements in the Vector

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

The correctness of this iterative operation depends on luck that no thread will modify the Vector between calls to size and get. Although an ArrayIndexOutOfBoundsException may be thrown in the above code, this does not mean that Vector is not thread safe. The state of the Vector is still valid, and the exception thrown is consistent with its specification. We can lock, but this will cause other threads to be unable to access the Vector during the iteration, thus reducing concurrency.

synchronized(vector){
    for(int i=0;i<vector.size();i++){
        doSomething(vector.get(i));
    }
}

Iterator with ConcurrentModificationException

Although Vector is an old container class. However, many modern container classes do not eliminate the problem of composite operations. The standard way to iterate over a container class, whether in direct iteration or in the for-each loop syntax introduced in Java 5.0, is to use an Iterator. However, if other threads are concurrently modifying the container, the timely use of iterators cannot avoid locking the container during the iteration. Iterators for synchronous container classes are not designed with concurrent modification in mind, and they exhibit "fail-in-time" behavior. That's it, when they find that the container has been modified during the iteration, a ConcurrentModificationException is thrown.

concurrent container

Java 5.0 provides a variety of concurrent container classes to improve the performance of synchronous containers. Synchronous containers serialize all access to container state to achieve their thread safety. The price of this approach is a severe reduction in concurrency. When multiple threads compete for container locks, throughput will be severely reduced.

Concurrent containers, on the other hand, are designed for concurrent access by multiple threads. In Java 5.0, ConcurrentHashMap was added to replace the synchronized and hash-based Map, and CopyOnWriteArrayList was added to replace the synchronized List when convenient manipulation was the main operation.

ConcurrentHashMap

Like HashMap, ConcurrentHashMap is also a hash-based Map, but it uses a completely different locking strategy to provide higher concurrency and scalability. Instead of synchronizing each method on the same lock and allowing only one thread to access the container at a time, ConcurrentHashMap uses a finer-grained locking mechanism to achieve greater sharing, which is called For segment lock. Under this mechanism, any number of threads can access Map concurrently, threads performing read operations and threads performing write operations can access Map concurrently, and a certain number of write threads can concurrently modify Map, which can achieve Higher throughput with very little performance penalty in a single-threaded environment.

ConcurrentHashMap and other concurrent containers enhance the synchronous container class: they provide iterators that do not throw ConcurrentModificationException, so there is no need to lock the container during iteration. But the iterator returned by ConcurrentHashMap implements weak consistency rather than "fail in time". Weakly consistent iterators can tolerate concurrent modifications, facilitate existing elements when the iterator is created, and can (at the time not guarantee) reflect modifications to the container after the iterator is constructed.

Despite these improvements, there are still some constant trade-offs. For some methods that require computation on the entire Map, such as size and isEmpty, the semantics of these methods are slightly reduced to reflect the concurrency nature of the container. Since the result returned by size may be nearly out of date at the time of calculation, it is really just a sniping value, allowing size to return an approximation rather than an exact value. While this may seem disturbing, methods such as size and isEmpty are in fact a union rather than an exact value. While this may seem disturbing, methods like size and isEmpty are of little use in a concurrent environment because their return values ​​are always changing. Therefore, the need for these operations is weakened in exchange for performance optimizations for other more important operations, including get, put, containsKey, and remove, etc.

Locking the Map to provide exclusive access is not implemented in ConcurrentHashMap. In HashTable and synchronizedMap, acquiring the lock on the Map prevents other threads from accessing the Map. There are some uncommon cases where this functionality is needed, such as adding some maps atomically, or iterating over a map several times and keeping the elements in the same order in the meantime, however, overall this tradeoff is justified because concurrent containers The content will continue to change.

Compared with Hshtable and synchronizedMap, ConcurrentHashMap has more advantages and fewer disadvantages, so in most cases, using ConcurrentHashMap instead of synchronized Map can further improve the scalability of the code. ConcurrentHashMap should be abandoned only when the application needs to lock the Map for exclusive access.

Additional atomic Map operations

Since ConcurrentHaspMap cannot be locked for exclusive access, we cannot use client-side locking to create new atomic operations such as "add if none", "remove if equal", and "replace if equal", etc. , have been implemented as atomic operations and declared in the ConcurrentMap interface.

 public interface ConcurrentMap<K,V> extends Map<K,V>
     V putIfAbsent(K key,V value);
     boolean remove(K key,V value);
     boolean replace(K key,V oldValue,V newValue);
     V replace(K key,V newValue);

CopyOnWriteArrayList

CopyOnWriteArrayList user replaces synchronized List, in some cases it provides better concurrency performance, don't cut the container during iteration without locking or copying.

Blocking queues and the producer-consumer pattern

Blocking and Interrupting Methods

A thread may block or suspend execution for a number of reasons: waiting for an I/O operation to complete, waiting to acquire a lock, waiting to wake up from the Thread.sleep method, or waiting for another thread to compute a result. When a thread blocks, it is usually suspended and in some kind of blocking state (Blocked, WAITING or TIMED_WAITING)

Synchronization tool class

atresia

Latches are a class of synchronization tools that delay a thread's progress until it reaches a termination state. A latch acts like a door: until the latch reaches the end state, the door is closed and no thread can pass, and when the end state is reached, the door opens and allows all threads to pass. Latches can be used to ensure that certain activities do not continue until other activities have completed, for example:

  • Make sure that a computation does not proceed until all the resources it needs have been initialized. Binary latches can be used to indicate that "the resource R has been initialized", and all operations that require R must now wait on this latch.
  • Make sure that a service does not start until all other services it depends on have started. Every service has an associated binary lock. When the service S is started, it will first wait on the lock of other services that S depends on, and after all dependent services are started, the lock S will be released, so that other services that depend on S can continue to execute.
  • Waiting for all participants of an operation to be ready to proceed.

    CountDownLatch is a flexible latching implementation that can be used in each of the above situations, which enables one or more threads to wait for a set of events to occur. The latched state includes a counter initialized to an integer representing the number of events to wait for. The countDown method decrements the counter, indicating that an event has occurred, and the await method waits for the counter to reach a large ridge, indicating that all the waiting time has occurred.

public class TestHarness{
    public long timeTasks(int nThreads,final Runnable task){
        final CountDownLatch startGate = new CountDownLatch(1);//起始门设置为1,等待所有线程创建完毕
        final CountDownLatch endGate = new CountDownLatch(nThreads)//结束门设置为n,等待所有线程执行完毕
        for(int i = 0; i<nThreads;i++){
            Thread t = new Thread(){
                public void run(){
                    startGate.await();
                    try{
                        task.run();
                    }finally{
                        endGate.countDown();
                    }
                }
            };
            t.start();
        }
        long start = System.nanoTime();
        startGate.countDown();
        endGate.await();
        long end = System.nanoTime();
        return end-start;
    }
}

In the above way, we can ensure that all threads start at the same time, and wait for the last thread to finish executing.

FutureTask

FutureTask can also be used as a latch. (FutureTask implements Future semantics), representing an abstract computation that produces results. The calculation represented by FutureTask is implemented by Callable, which is equivalent to a Runnable that can generate results, and can be in the following three states: waiting to run, running and running complete.

signal

Guess you like

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