Author: Tangyuan

Personal blog: Javalover.cc

preface

Officials hello, I am tangyuan, today to bring you is “Java concurrent – synchronous container”, I hope to help, thank you

If there is any problem with the article, you are welcome to criticize and correct it. Thank you

Introduction to the

Synchronous containers fall into two main categories: ordinary classes such as Vector and inner classes created through Collections’ factory methods

While many people are biased against synchronous containers for their poor performance, it’s not all bad. Here’s an excerpt from Alibaba’s development manual:

In high concurrency, synchronous calls should consider the performance cost of locking. If you can use lock-free data structures, don’t use locks; If you can lock blocks, don’t lock the whole method body. If you can use object locks, don’t use class locks.

As you can see, locking performance is only considered for high concurrency, so synchronous containers can be useful in small, comprehensive systems (concurrent containers can also be considered, as discussed in a later section).

P.S. This is not a whitewash

directory

Here we can analyze it in three steps:

  1. What is a synchronous container
  2. Why have synchronous containers
  3. Advantages and disadvantages of synchronous containers
  4. Use scenarios for synchronous containers

The body of the

1. What is a synchronous container

Definition: to synchronize container classes so that when we use containers concurrently, we don’t have to manually synchronize them because internal synchronization is already automatic

Example: A Vector is a synchronous container class that synchronizes all its internal methods by locking them. (Some overloaded methods are unlocked, but the methods that are called are still locked.)

Source: Vector. Add

// Lock the add method with synchronized
public synchronized boolean add(E e) {
  modCount++;
  ensureCapacityHelper(elementCount + 1);
  elementData[elementCount++] = e;
  return true;
}
Copy the code

Synchronous containers fall into two main categories:

  1. Common classes: Vector, Stack, HashTable
  2. The inner class: the Collections to create inner classes, such as the Collections. SynchronizedList, Collections. SynchronizedSet, etc

Is there a difference between the two?

Of course there is, when we started (Java1.0) there was only the first synchronous container (Vector, etc.)

But because Vector is so awkward, it wants to make everything its own (Vector becomes its own via toArray,HashTable becomes its own via putAll).

Source: Vector constructor

public Vector(Collection<? extends E> c) {
	// Here we convert the passed collection to our own through toArray
  elementData = c.toArray();
  elementCount = elementData.length;
  // c.toArray might (incorrectly) not return Object[] (see 6260652)
  if(elementData.getClass() ! = Object[].class) elementData = Arrays.copyOf(elementData, elementCount, Object[].class); }Copy the code

So there is a second synchronous container class (the inner container class created by the factory method), which is more clever. It simply wraps the original container (with this.list = list pointing directly to the container to be synchronized) and partially locks it, thus generating thread-safe classes without too much effort;

Source: the Collections. SynchronizedList constructor

SynchronizedList(List<E> list) {
  super(list);
  // This is only a reference to the passed list, not to own, subsequent related operations are still based on the original list collection
  this.list = list;
}

Copy the code

The differences between them are as follows:

The difference between two synchronous containers Ordinary class The inner class
The object of the lock Cannot be specified, only this Can be specified, default is this
The scope of the lock Method body (including iteration) Code block (excluding iteration)
Scope of application Narrow – individual containers Wide – all containers

Here we focus on the object of the lock:

  • The normal class locks the current object this (on the method, this by default);
  • The inner class locks the mutex attribute, which defaults to this, but you can specify the object to lock through the constructor (or factory method)

Source: the Collections. SynchronizedCollection constructor

final Collection<E> c;  // Backing Collection
// This is the lock object
final Object mutex;     // Object on which to synchronize

SynchronizedCollection(Collection<E> c) {
  this.c = Objects.requireNonNull(c);
// Initialize to this
  mutex = this;
}

SynchronizedCollection(Collection<E> c, Object mutex) {
  this.c = Objects.requireNonNull(c);
  this.mutex = Objects.requireNonNull(mutex);
}
Copy the code

One thing to note here is that the iterators of the inner class are not synchronized (the iterators of the Vector are synchronized) and need to be manually locked to synchronize

Source: vector.itr. Next Iterating method (locked)

public E next(a) {
  synchronized (Vector.this) {
    checkForComodification();
    int i = cursor;
    if (i >= elementCount)
      throw new NoSuchElementException();
    cursor = i + 1;
    returnelementData(lastRet = i); }}Copy the code

Source: the Collections. SynchronizedCollection. Iterator iterator (unlocked)

public Iterator<E> iterator(a) {
  // The iterators of the class are implemented directly (e.g. ArrayList, where the iterators must be unlocked)
  return c.iterator(); // Must be manually synched by user!
}

Copy the code

2. Why have synchronous containers

Because normal container classes (such as ArrayList) are not thread-safe, we would need to manually lock them to be safe if they were used concurrently, which would be troublesome;

So we have the synchronous container, which automatically locks for us

Let’s compare this with code

Thread-unsafe class: ArrayList

public class SyncCollectionDemo {
    
    private List<Integer> listNoSync;

    public SyncCollectionDemo(a) {
        this.listNoSync = new ArrayList<>();
    }

    public void addNoSync(int temp){
        listNoSync.add(temp);
    }

    public static void main(String[] args) throws InterruptedException {
        SyncCollectionDemo demo = new SyncCollectionDemo();
				// Create 10 threads
        for (int i = 0; i < 10; i++) {
					// Each thread performs 100 add operations
          new Thread(()->{
                for (int j = 0; j < 1000; j++) { demo.addNoSync(j); } }).start(); }}}Copy the code

The above code seems to be ok, but if there is a problem, it should be the order of insertion is disorderly.

But actually running it will find that it is possible to error that the array is out of bounds, as shown below:

There are two reasons:

  1. Because the arrayList. add operation is unlocked, multiple threads can execute the add operation at the same time
  2. During the Add operation, if the capacity of the list is insufficient, the capacity will be expanded. However, the capacity of multiple threads will be expanded at the same time

Arraylist.grow expands

// Capacity expansion method
private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
				// You can see that the capacity is increased by half each time
  			int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // minCapacity is usually close to size, so this is a win:
        elementData = Arrays.copyOf(elementData, newCapacity);
    }
Copy the code

As you can see, capacity expansion is based on previous capacity, so if multiple threads are expanding at the same time, the base of capacity expansion is not accurate, resulting in problems

Thread-safe classes: Collections. SynchronizedList

/** * <p> * Synchronous container class: Why have it * </p> **@author: JavaLover
 * @time: 2021/5/3 * /
public class SyncCollectionDemo {

    private List<Integer> listSync;

    public SyncCollectionDemo(a) {
      	// Wrap an empty ArrayList
        this.listSync = Collections.synchronizedList(new ArrayList<>());
    }

    public void addSync(int j){
      	Synchronized (mutex) {return c.dd (e); synchronized (mutex) {return c.dd (e); }
        listSync.add(j);
    }

    public static void main(String[] args) throws InterruptedException {
        SyncCollectionDemo demo = new SyncCollectionDemo();

        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                for (int j = 0; j < 100; j++) {
                    demo.addSync(j);
                }
            }).start();
        }

        TimeUnit.SECONDS.sleep(1);
      	/ / output 1000System.out.println(demo.listSync.size()); }}Copy the code

The output is correct, because ArrayList is now wrapped as a thread-safe class by Collections

That’s why synchronous containers exist: they make threading safer when programming concurrently

3. Advantages and disadvantages of synchronous containers

Generally speaking, it is to say the advantages first, then say the disadvantages

But let’s talk about the pros first

Advantages:

  • In concurrent programming, separate operations are thread-safe, such as separate Add operations

The cons (yes, the pros) :

  • Poor performance, basically all methods are locked, the perfect interpretation of “would rather kill a thousand wrong, can not let go of a”

  • Compound operations, which are still unsafe, such as the putIfAbsent operation (if not added)

  • Rapid failure mechanism, this mechanism will be an error prompt ConcurrentModificationException, generally occurs when a thread in the traversal container, other threads to modify the length of the container

Why is number three a weakness?

Because it is only a suggestion that there is a concurrent change exception, there is no guarantee that every concurrent change will explode that exception

The premise for this exception is as follows:

Source: Vector. Itr. CheckForComodification check container modification

final void checkForComodification(a) {
  // modCount: the number of times the container length changes, and expectedModCount: the expected number of times the container length changes
  if(modCount ! = expectedModCount)throw new ConcurrentModificationException();
}
Copy the code

When does concurrent modification not throw an exception? There are two:

  1. Unlocked traversal: For the second type of synchronous container (the Collections inner class), thread B traversals without exception, assuming that thread A has modified modCount but not synchronized to thread B (but the problem is already there, just not yet)

  2. Thread sequence-dependent cases: For all synchronous containers, assuming that thread B has already traversed the container before thread A starts traversing the changes, no exceptions will occur

Code will not post, you can directly write several threads to try, run several times, should be able to see the effect (but the first case is also based on theoretical analysis, the actual code I did not run out)

According to Alibaba’s development specification: do not remove/add elements in the foreach loop. The remove element must be Iterator. If the operation is performed concurrently, the Iterator must be locked.

Remove is different from iterator. remove

  • Iterator.remove: The expectedModCount=modCount is synchronized

  • List. remove: Only modCount is modified because the expectedModCount is an iterator property and not a list property (but can also be accessed indirectly)

Arraylist. remove Removes elements

public E remove(int index) {
        rangeCheck(index);
				// 1. ModCount is modified
        modCount++;
        E oldValue = elementData(index);

        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--size] = null; // clear to let GC do its work

        return oldValue;
    }
Copy the code

Arraylist.itr. Remove iterator removes elements

public void remove(a) {
            if (lastRet < 0)
                throw new IllegalStateException();
            checkForComodification();

            try {
              	// 1. Call list.romove above to modify modCount
                ArrayList.this.remove(lastRet);
                cursor = lastRet;
                lastRet = -1;
              	// 2. Here you synchronize the expectedModCount again
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException ex) {
                throw newConcurrentModificationException(); }}Copy the code

Because of these disadvantages of synchronous containers, concurrent containers are born (more on that next time)

4. Synchronize the usage scenarios of containers

Multi-purpose in concurrent programming, but the concurrency value is not a lot of scenarios, such as some simple personal blog system, specific how many concurrent quantity is big, this also points a lot of cases, is not to say how many requests per second over, as high concurrency, combine the throughput, system response time, and many other factors to consider together)

Specifically, there are the following scenarios:

  • Write more and read less, and the performance difference between synchronous and concurrent containers is similar (concurrent containers can concurrently read)
  • Custom compound operations, such as getLast (forget putIfAbsent because concurrent containers provide this compound by default)
  • , etc.

conclusion

  1. What is a synchronous container: synchronizing container classes so that when we use containers concurrently, we don’t have to manually synchronize them because they are already synchronized internally
  2. Why synchronous containers are necessary: Normal container classes (such as ArrayList) are thread-unsafe and require manual locking to be safe if they are used concurrently. So we have the synchronous container, which automatically locks for us
  3. Advantages and disadvantages of synchronous containers:
advantages disadvantages
Synchronization of the container Independent operation, thread safety Compound operation, still not safe
Performance is poor
Fast fail mechanism, only suitable for bug debugging
  1. Use scenarios for synchronous containers

It is used in scenarios where the amount of concurrency is not very large, such as personal blogs, background systems, etc

Specifically, there are the following scenarios:

  • Write more than read less: There is not much difference between synchronous and concurrent containers at this point

  • Custom compound operations: Compound operations such as getLast can be easily concatenated because synchronous containers are locked by a single operation (remember external locking)

  • , etc.

Reference content:

  • Java Concurrent Programming
  • Real Java High Concurrency

Afterword.

And finally, thank you for watching. Thank you

Original is not easy, look forward to the three even yo