basic building blocks

Sync container class

Synchronization container classes include Vector and Hashtable, as well as some classes with similar functions added after JDK1.2. These synchronization wrapper classes are created by factory methods such as Collections.synchronizedXxxd. These classes achieve thread safety by encapsulating their state and synchronizing each public method so that only one thread can access the container's state at a time.

Problems with synchronizing container classes

   Synchronized container classes are all thread-safe, but in some cases additional client-side locking may be required to protect compound operations. Common compound operations on containers are: iterations, jumps, and conditional operations (such as "add if none"). In a synchronous container, these composite operations are still thread-safe without client locking, but they may exhibit unexpected behavior when other threads are concurrently modifying the container. At this time, a locking mechanism is required.

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

When the above code iterates over a synchronized container, in order to prevent other threads from changing the Vector during the iteration, the code needs to be synchronized.

If you don't want to lock the container during iteration, another approach is to "clone" the container and iterate over the copy. Since the copy is enclosed in a thread, other threads do not change it during the iteration. However, there is a significant performance overhead in cloning the container.

concurrent container

The synchronized containers above serialize all accesses to the container state to achieve their thread safety. The price of this approach is a severe reduction in concurrency, and throughput will be severely reduced when multiple threads compete for the container's lock.

Concurrent containers are designed for concurrent access by multiple threads. Java 5.0 adds a number of concurrent container classes to improve the performance of synchronous containers.

ConcurrentHashMap

Like HashMap, ConcurrentHashMap is also a hash-based Map, but it uses a completely different locking strategy to provide higher concurrency and scalability. ConcurrentHashMap uses a more fine-grained segmented locking mechanism instead of synchronizing every method on the same lock. In this mechanism, any number of reading threads can access the Map concurrently, the reading thread can access the Map concurrently with the writing thread, and a certain number of writing threads can concurrently modify the Map. The result of ConcurrentHashMap is that it will bring higher throughput in a concurrent environment with very little performance loss 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 during iteration. The returned iterator has "weak consistency", not "immediate failure".

There is no implementation in ConcurrentHashMap that provides exclusive access to Map locking. Acquiring a lock on a Map in HashMap and synchronizedMap prevents other threads from accessing the Map. Compared with HashMap and synchronizedMap, using ConcurrentHashMap instead of synchronized Map can further improve scalability. ConcurrentHashMap should be abandoned only when the application needs to lock Map for exclusive access.

Just as ConcurrentHashMap was used to replace synchronous Map, Java6 introduced ConcurrentSkipListMap and ConcurrentSkipList-Set as concurrent replacements for SortedMap and SortedSet respectively.

CopyOnWriteArrayList

CopyOnWriteArrayList is used in place of a synchronized List, which provides better concurrency performance in some cases and does not require locking or copying of the container during iteration (similarly, CopyOnWriteArraySet is used instead of a synchronized Set).

A CopyOnWrite container is a copy-on-write container. The popular understanding is that when we add elements to a container, we do not directly add to the current container, but first copy the current container, copy a new container, and then add elements to the new container. After adding elements, Then point the reference of the original container to the new container. The advantage of this is that we can read the CopyOnWrite container concurrently without locking, because the current container will not add any elements. Therefore, the CopyOnWrite container is also a kind of separation of reading and writing, reading and writing different containers.

The thread safety of a copy-on-write container is that as long as a de facto immutable object is properly published, no further synchronization is required when accessing the object. Obviously, the underlying array is copied every time it is modified, which requires some overhead, especially if the array is large. Copy-on-write containers should only be used when there are far more iterations than modifications

Queue and BlockingQueue (blocking queue)

Queue is used to temporarily hold a set of pending elements. It provides several implementations, including: ConcurrentLinkedQueue, which is a traditional FIFO queue; and PriorityQueue, which is a (non-concurrent) priority queue. Operations on the Queue will not block, and if the queue is empty, a null value will be obtained. Although List can be used to simulate the behavior of Queue—in fact, Queue is implemented through LinkedList, but Queue can remove the random access request of List, thereby achieving more efficient concurrency.

BlockingQueue extends Queue, adding blockable insert and get operations. If the queue is empty, an fetch operation will block until an available element is available in the queue; if the queue is full, an insert operation will block until there is free space in the queue.

Producer-Consumer Model

Blocking queues support the producer-consumer pattern. This pattern separates the processes of "finding what needs to be done" and "doing it", and puts the work into a "to-do" list for processing later, rather than immediately after finding it. The producer-consumer pattern simplifies the development process because it removes code dependencies between producer and consumer classes.

Double-ended queue -- Deque and BlockingDeque

Deque and BlockingDeque extend Queue and BlockingQueue respectively. Deque is a double-ended queue that implements efficient insertion and removal at the head and tail of the queue. Specific implementations include ArrayDeque and ArrayBlockingDeque.

Working password mode

Just as blocking queues are suitable for "producer-consumer" mode, deques are suitable for another mode - "work secret". In the producer-consumer pattern, all consumers share a work queue, while in work cipher, each consumer has its own deque. If a consumer completes all work in its own deque, it can secretly get work from the end of other consumers' deques. The work stealing mode is more scalable than the general producer-consumer mode because worker threads do not compete on a single shared task queue.

Synchronization tool class

atresia

A latch is a tool synchronization class that delays a thread's progress until it reaches a terminated state. The function of the lock is equivalent to a door. Until the lock reaches the end state, the door is always closed, and no thread can pass; when the lock ends, the door will be opened and all threads can pass. When the latch reaches the end state and opens the door, it will not change its state, i.e. the door will not close again.

Locked application scenarios:

  • Ensure that a computation does not proceed until all the resources it needs have been initialized;
  • Make sure that a service does not start until all other services it depends on have been started;
  • Wait until all participants of an operation are ready before continuing.

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 a positive number representing the number of events to wait for. The countDown method decrements the counter, indicating that a time has occurred. The await method waits for the counter to be 0, which means that all events have occurred. If the counter is not 0, await will block until the counter is 0, or the waiting thread is interrupted, or the wait times out.

public class TestHarness{
    public long timeTasks(int nThreads, final Runnable task) throws InterruptedException{
        final CountDownLanch startGate = new CountDownLanch(1);
        final CountDownLanch endGate = new CountDownLanch(nThreads);
        
        for(int i=0; i<nThreads; i++){
            Thread t = new Thread(){
                public void run(){
                    try{
                        startGate.await();    //n个线程都会阻塞在这里,知道startGate门打开
                        try{
                            task.run();
                        }finally{
                            engGate.countDown();    //每个线程都在执行结束时将endGate门的计数器减一
                        }
                    }catch (InterruptedException ignored){}
                }
            };
            t.start();    //启动线程,但会阻塞在startGate门
        }

        //使用nanoTime()提供准确的计时
        long start = System.nanoTime();

        startGate.countDown();    //将StartGate门打开,n个线程同时执行
        endGate.await();    //阻塞直到n个线程都执行完成,计数器变为0

        long end = System.nanoTime();
        return end-start;
    }
}

Locking is used in the TestHarness class above to ensure that n threads start executing at the same time. This is useful for testing how long it takes n threads to execute a task concurrently. If latching does not apply, the thread started first must lead the thread started later.

Semaphore

Counting semaphores are used to control the number of operations that access a specific resource at the same time, or specify the number of specific operations at the same time. Semaphores are used to solve synchronization problems rather than deadlock problems.

A set of virtual licenses (premits) are managed in Semaphore, and the initial number of licenses can be specified by the constructor. A license can be acquired first when an operation is performed (as long as there are licenses remaining), and released after use. Will be blocked if there is no permission.

public class BoundedHashSet<T>{
    private final Set<T> set;
    private final Semaphore sem;
    
    public BoundedHashSet(int bound){
        this.set = Collection.synchronizedSet(New HashSet<T>());
        sem = new Semaphore(bound);
    }

    public boolean add(T o) throws InterruptedException{
        sem.acquire();    //获取许可
        boolean wasAdded = false;
        try{
            wasAdded = set.add(o);
            return wasAdded
        }finally{
            if(!wasAdded)    //如果add没有添加元素,立即释放许可
                sem.release();    
        }
    }

    public boolean remove(Object o){
        boolean wasRemoved = set.remove(o);
        if(wasRemoved)
            sem.release(); 
        return wasRemoved;
    }
}

fence

Fences are similar to latches in that they block a group of threads until an event occurs. The key difference between a fence and a latch is that all threads must reach the fence position at the same time in order to continue execution. Latches are used to wait for events, and fences are used to wait for other threads.

Common fences come in two forms: CyclicBarrier and Exchanger.

Guess you like

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