Author: Tangyuan

Personal blog: Javalover.cc

preface

Intermittent more than a month, also wrote a dozen original articles, feeling really different;

I wouldn’t say the technology is much better, but the thinking is different;

Haven’t started, thinking about too many things to learn, always feel unable to start;

But when you really decide to do it for a few days, you will find that the path is really a step by step;

If you are always standing still and looking around, it will not help you;

Ok, let’s start today’s topic, concurrent containers

Introduction to the

Earlier, we introduced synchronous containers. One of their major disadvantages is poor performance in high-concurrency environments.

For this, there are concurrent container classes designed specifically for high concurrency;

Since the concurrent container classes are under java.util.concurrent, we also tend to refer to concurrent containers as JUC containers for short;

There are also JUC atomic classes, JUC locks, JUC utility classes, and more (more on that later)

Today let’s take a quick look at concurrent containers in JUC

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

directory

  1. What is a concurrent container
  2. Why are there concurrent containers
  3. The difference between a concurrent container, a synchronous container, and a normal container

The body of the

1. What is concurrent container

Concurrent containers are classes designed specifically for high concurrency to replace synchronous containers with lower performance

Common concurrent container classes are as follows:

In this section we’ll focus on the first ConcurrentHashMap as an example to introduce concurrent containers

The others will be analyzed separately at a later time

2. Why are there concurrent containers

The same thing happened with synchronous containers:

Synchronous containers are designed to allow us to write multithreaded code without having to manually lock it, freeing us up to do more meaningful things (meaningful? Your hands? ;

The concurrent container is to improve the performance of synchronous container, which is equivalent to the upgraded version of synchronous container.

This is also why Java has been in decline, but not in decline (and the big guys are worried!!). ;

Then again, the bosses’ anxiety is overdone; I can’t believe Java is now level 16 and we’re still stuck at level 8.

3. Differences between concurrent containers, synchronous containers, and common containers

By normal containers, I mean container classes without synchronization and concurrency, such as HashMap

Three by contrast, just to make it a little clearer

The following uses HashMap, HashTable, and ConcurrentHashMap as examples

Performance analysis

Let’s analyze the performance differences among the three:

Note: the normal container is tested with a single thread, so we will not consider multithreading because it is not safe

Some friends may say, you are not fair, but there is no way ah, who let her multithreading is not safe.

If I had to choose between security and performance, I’d choose ConcurrentHashMap.

The relationship between the three of them is shown below

(Red means severe, orange means moderate, and green means smooth)

You can see:

  • When working with a normal container in a single thread, the code is executed serially and only one piece of data can be put or get into the container at a time

  • When working with a synchronous container in multiple threads, multiple threads can queue up to do so, and only one data can be put or get into the synchronous container at a time

  • When working with a concurrent container in multiple threads, it is possible for multiple threads to execute simultaneously, meaning that multiple threads can put or get data into a concurrent container at the same time (read, write, or write at the same time – it may block, using ConcurrentHashMap as reference).

Let’s use code to reproduce the effect shown above (slow – medium – fast)

  1. HashMap test method
public static void hashMapTest(a){
  Map<String, String> map = new HashMap<>();
  long start = System.nanoTime();
	// Create 100000 data single thread
  for (int i = 0; i < 100 _000; i++) {
		// Use the UUID as the key to ensure that the key is unique
    map.put(UUID.randomUUID().toString(), String.valueOf(i));
    map.get(UUID.randomUUID().toString());
  }
  long end = System.nanoTime();
  System.out.println("HashMap time:");
  System.out.println(end - start);
}
Copy the code
  1. HashTable test method
public static void hashTableTest(a){
  Map<String, String> map = new Hashtable<>();
  long start = System.nanoTime();
	// Create 10 threads - multiple threads
  for (int i = 0; i < 10; i++) {
    new Thread(()->{
      // Create 10000 data per thread
      for (int j = 0; j < 10000; j++) {
        // UUID ensures that the key is unique
        map.put(UUID.randomUUID().toString(), String.valueOf(j));
        map.get(UUID.randomUUID().toString());
      }
    }).start();
  }
	The main thread in IDEA has another monitor thread. The main thread has another monitor thread
  while (Thread.activeCount()>2){
    Thread.yield();
  }
  long end = System.nanoTime();
  System.out.println("HashTable time:");
  System.out.println(end - start);
}
Copy the code
  1. ConcurrentHashMap Test method
public static void concurrentHashMapTest(a){
  Map<String, String> map = new ConcurrentHashMap<>();
  long start = System.nanoTime();
  // Create 10 threads - multiple threads
  for (int i = 0; i < 10; i++) {
    new Thread(()->{
      // Create 10000 data per thread
      for (int j = 0; j < 10000; j++) {
        // UUID is used as the key to ensure uniqueness
        map.put(UUID.randomUUID().toString(), String.valueOf(j));
        map.get(UUID.randomUUID().toString());
      }
    }).start();
  }
	The main thread in IDEA has another monitor thread. The main thread has another monitor thread
  while (Thread.activeCount()>2){
    Thread.yield();
  }
  long end = System.nanoTime();
  System.out.println("ConcurrentHashMap Time:");
  System.out.println(end - start);
}
Copy the code
  1. The main method performs the above three tests separately
public static void main(String[] args) {
  hashMapTest();
  hashTableTest();
  while (Thread.activeCount()>2){
    Thread.yield();
  }
  concurrentHashMapTest();
}
Copy the code

Run it and you’ll see something like this (run it multiple times, the numbers may get better, but the pattern is basically the same)

A hashMap time-consuming:754699874(slow) hashTable time:609160132(middle) concurrentHashMap Time:261617133(fast)Copy the code

The result is the normal speed: normal < synchronous < concurrent

But it’s not absolute, because all the keys are unique, so it looks normal

What if we’re not normal? Like the extreme BT kind

Let’s insert the same data over and over again, with all the above put/get changed to the following code:

map.put("a"."a");
map.get("a");
Copy the code

And when you run it, you’ll find another result (if you’re interested, you can type it out)

However, if the conclusion is not conclusive, it is not of great significance;

Lock analysis

Normal containers are unlocked

All synchronized containers are locked at the method level, which means the entire container is locked. Let’s look at locking HashTable

public synchronized V put(K key, V value) {}
public synchronized V remove(Object key) {}
Copy the code

As you can see, because the lock is a built-in lock, it locks the entire container

So when we put, no other thread can put/get

And when we get, none of the other threads can put/get

So synchronizing containers is less efficient

For concurrent containers, use 1.7 ConcurrentHashMap as an example (1.7 is chosen because it is used in the previous section).

Its locking granularity is very small, it does not lock the whole container, but the segment lock;

Based on key.hash, different hash values are mapped to different segments (16 segments by default). When inserting data, the corresponding segment is locked based on the hash value, and other segments can still be read and written by other threads.

ConcurrentHashMap supports simultaneous writing of multiple threads (as long as the inserted key’s hashCode does not map to the same segment, there is no conflict, so it can be written simultaneously)

Reading is not locked, so of course simultaneous reading is supported

If the read operation has no lock, how does it ensure data consistency?

The answer is volatile, which applies to the Node Node and the value val and ensures that your get value is always up to date

The ConcurrentHashMap node is volatile. The val and NET nodes are volatile

static class Node<K.V> implements Map.Entry<K.V> {
  final int hash;
  final K key;
  volatile V val;
  volatile Node<K,V> next;
}
Copy the code

To sum up, ConcurrentHashMap allows multiple threads to read, write, and read and write simultaneously

conclusion

  1. What are concurrent containers: Concurrent containers are classes that are specifically designed for high concurrency to replace less efficient synchronous containers
  2. Why concurrent containers: To improve the performance of synchronous containers
  3. The difference between a concurrent container, a synchronous container, and a normal container:
    • Performance: high-medium-low
    • Lock: Small granularity – Large granularity – None
    • Scenario: High concurrency – Medium concurrency – Single thread

Reference content:

  • Java Concurrent Programming
  • Real Java High Concurrency
  • In-depth Understanding of the Java Virtual Machine

Afterword.

What I have introduced here is relatively superficial, but in fact there is a lot of knowledge about concurrent containers;

But because this is the early part of the concurrent series, there’s a lot of stuff that’s not covered, so it’s a little shallow;

Wait until you’ve covered enough ground in the concurrent series to go back in depth.

Write at the end:

May the right person be the right person for you.