java fail-fast-vs-fail-safe

what is fail-fast

First, let's look at the explanation of fail-fast in Wikipedia:

In systems design, a fail-fast system is one which immediately reports at its interface any condition that is likely to indicate a failure. Fail-fast systems are usually designed to stop normal operation rather than attempt to continue a possibly flawed process. Such designs often check the system’s state at several points in an operation, so any failures can be detected early. The responsibility of a fail-fast module is detecting errors, then letting the next-highest level of the system handle them.

Roughly it means: In system design, a fail-fast system A system that immediately reports any condition that might indicate a failure. Fast-fail systems are usually designed to stop normal operation rather than attempt to continue a potentially defective process. This design typically checks the status of the system at multiple points in operation so any failures can be detected early. The responsibility of the fail-fast module is to detect errors and then let the next highest level of the system handle the errors.

In fact, this is a concept. To put it bluntly, it is to consider the abnormal situation first when designing the system. Once an abnormality occurs, it will be stopped and reported directly.

To give a simple fail-fast example:

public int divide(int divisor,int dividend){
    if(divisor == 0){
        throw new RuntimeException("divisor can't be null");
    }
    return dividend/divisor;
}

The above code is a method for dividing two integers. In the divide method, we do a simple check on the divisor. If the value is 0, an exception is thrown directly and the cause of the exception is clearly indicated. This is actually the practical application of the fail-fast concept.

The advantage of this is that some error conditions can be identified in advance. On the one hand, it can avoid the execution of other complicated codes. On the other hand, after this abnormal situation is identified, it can also be dealt with individually.

How about it, now you know fail-fast, in fact it is not mysterious, you may often use it in your daily code.

Since fail-fast is a better mechanism, why does the title of the article say that fail-fast will have pitfalls?

The reason is that the fail-fast mechanism is used in the design of the Java collection class. Once it is used improperly, the code designed by the fail-fast mechanism will be triggered, and unexpected situations will occur.

fail-fast in collection classes

The fail-fast mechanism in Java we usually refer to refers to an error detection mechanism for Java collections by default. When multiple threads perform structural changes on some collections, a fail-fast mechanism may occur, and a ConcurrentModificationException will be thrown at this time (replaced by CME later).

CMException, thrown when a method detects concurrent modification of an object, but does not allow such modification.

Many times, it is precisely because CMException is thrown in the code that many programmers will be confused. Obviously, their code is not executed in a multi-threaded environment. Why do they throw such concurrency-related exceptions? Under what circumstances will this situation be thrown? Let's take a closer look.

Abnormal recurrence

In Java, if the remove/add operation is performed on certain collection elements in the foreach loop, the fail-fast mechanism will be triggered and a CMException will be thrown.

Such as the following code:

List<String> userNames = new ArrayList<String>() {
   
   {
    add("Hollis");
    add("hollis");
    add("HollisChuang");
    add("H");
}};

for (String userName : userNames) {
    if (userName.equals("Hollis")) {
        userNames.remove(userName);
    }
}

System.out.println(userNames);

The above code uses the enhanced for loop to traverse the elements and tries to delete the Hollis string elements. Running the above code will throw the following exception:

Exception in thread "main" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
at java.util.ArrayList$Itr.next(ArrayList.java:859)
at com.hollis.ForEach.main(ForEach.java:22)

Similarly, readers can try to use the add method to add elements in the enhanced for loop, and the result will also throw this exception.

Before going into the principle, let's try to desugar the syntax of foreach, and see how foreach is implemented.

We use the [jad][1] tool to decompile the compiled class and get the following code:

public static void main(String[] args) {
    // 使用ImmutableList初始化一个List
    List<String> userNames = new ArrayList<String>() {
   
   {
        add("Hollis");
        add("hollis");
        add("HollisChuang");
        add("H");
    }};

    Iterator iterator = userNames.iterator();
    do
    {
        if(!iterator.hasNext())
            break;
        String userName = (String)iterator.next();
        if(userName.equals("Hollis"))
            userNames.remove(userName);
    } while(true);
    System.out.println(userNames);
}

It can be found that foreach is actually implemented by relying on while loop and Iterator.

exception principle

Through the exception stack of the above code, we can trace that the code that actually throws the exception is:

java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)

This method is called in the iterator.next() method. Let's look at the implementation of this method:

final void checkForComodification() {
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}

As above, modCount and expectedModCount are compared in this method, and if the two are not equal, CMException is thrown.

So, what are modCount and expectedModCount? What causes their values ​​to be unequal?

modCount is a member variable in ArrayList. It represents the number of times the collection was actually modified.

List<String> userNames = new ArrayList<String>() {
   
   {
    add("Hollis");
    add("hollis");
    add("HollisChuang");
    add("H");
}};

This variable exists when the collection is initialized using the above code. The initial value is 0.

expectedModCount is an internal class in ArrayList - a member variable in Itr.

Iterator iterator = userNames.iterator();

The above code can get an Itr class, which implements the Iterator interface.

expectedModCount indicates the number of times this iterator expects the collection to be modified. Its value is initialized as Itr is created. The value will only change if the collection is manipulated through the iterator.

Then, let's look at userNames.remove(userName); what is done in the method, and why the values ​​of expectedModCount and modCount are different.

By flipping through the code, we can also find that the core logic of the remove method is as follows:

private void fastRemove(int index) {
    modCount++;
    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
}

As you can see, it only modifies modCount and does not do anything to expectedModCount.

Simply draw a picture to describe the above scenario:

![][2]

To briefly summarize, the reason why CMException is thrown is because our code uses an enhanced for loop, and in the enhanced for loop, the collection traversal is performed through iterator, but the add/remove of elements is directly used Collection class own method. As a result, when the iterator traverses, it will find that an element has been deleted/added without knowing it, and an exception will be thrown to remind the user that concurrent modification may have occurred!

Therefore, when using Java collection classes, if CMException occurs, give priority to fail-fast related situations. In fact, there is no real concurrency here, but Iterator uses the fail-fast protection mechanism. If a modification is not performed by yourself, an exception will be thrown.

About how to solve this problem, we introduced it in "Why Alibaba Forbids the Remove/Add Operation of Elements in the Foreach Loop", so I won't go into details here.

fail-safe

In order to avoid triggering the fail-fast mechanism and causing exceptions, we can use some collection classes provided in Java that adopt the fail-safe mechanism.

Such a collection container is not directly accessed on the collection content when traversing, but first copies the original collection content and traverses on the copied collection.

The containers under the java.util.concurrent package are all fail-safe, and can be used and modified concurrently under multiple threads. At the same time, add/remove can also be performed in foreach.

Let's take a simple analysis of the fail-safe collection class CopyOnWriteArrayList.

public static void main(String[] args) {
    List<String> userNames = new CopyOnWriteArrayList<String>() {
   
   {
        add("Hollis");
        add("hollis");
        add("HollisChuang");
        add("H");
    }};

    userNames.iterator();

    for (String userName : userNames) {
        if (userName.equals("Hollis")) {
            userNames.remove(userName);
        }
    }

    System.out.println(userNames);
}

In the above code, if CopyOnWriteArrayList is used instead of ArrayList, no exception will occur.

All modifications to the fail-safe collection are first copied to a copy, and then performed on the copy collection, not directly modifying the original collection. And these modification methods, such as add/remove, control concurrency through locking.

Therefore, the iterator in CopyOnWriteArrayList does not need to perform fail-fast concurrency detection during the iteration process. (Because the main purpose of fail-fast is to identify concurrency, and then notify users through exceptions)

However, although the advantage of copying the content is that it avoids ConcurrentModificationException, iterators cannot access the modified content. Such as the following code:

public static void main(String[] args) {
    List<String> userNames = new CopyOnWriteArrayList<String>() {
   
   {
        add("Hollis");
        add("hollis");
        add("HollisChuang");
        add("H");
    }};

    Iterator it = userNames.iterator();

    for (String userName : userNames) {
        if (userName.equals("Hollis")) {
            userNames.remove(userName);
        }
    }

    System.out.println(userNames);

    while(it.hasNext()){
        System.out.println(it.next());
    }
}

After we get the Iterator of CopyOnWriteArrayList, we directly delete the values ​​in the original array through the for loop, and finally output the Iterator at the end. The results are as follows:

[hollis, HollisChuang, H]
Hollis
hollis
HollisChuang
H

What the iterator traverses is the copy of the collection obtained at the moment of traversal, and the iterator does not know the modification of the original collection during the traversal.

Copy-On-Write

After learning about CopyOnWriteArrayList, I don’t know if you will have such a question: its add/remove and other methods have been locked, why do you need to copy a copy and modify it? Unnecessary? It is also a thread-safe collection. What is the difference between this thing and Vector?

Copy-On-Write, referred to as COW, is an optimization strategy used in programming. The basic idea is that everyone is sharing the same content from the beginning. When someone wants to modify this content, they will actually copy the content out to form a new content and then modify it. This is a kind of delay and laziness Strategy.

The 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 to create a new container, and then add elements to the new container. After adding the elements, Then point the reference of the original container to the new container.

The write methods such as add/remove in CopyOnWriteArrayList need to be locked. The purpose is to avoid copying out N copies, resulting in concurrent writing.

However, the read method in CopyOnWriteArrayList is not locked.

public E get(int index) {
    return get(getArray(), index);
}

The advantage of this is that we can read the CopyOnWrite container concurrently. Of course, the data read here may not be the latest. Because the idea of ​​copy-on-write is to achieve the final consistency of data through the strategy of delayed update, not strong consistency.

**So the CopyOnWrite container is an idea of ​​reading and writing separation, reading and writing different containers. **But Vector uses the same container when reading and writing, read and write are mutually exclusive, and can only do one thing at the same time.

Guess you like

Origin blog.csdn.net/zy_dreamer/article/details/132307346