Immutable classes and meta patterns - Concurrent Programming (Java)

Posted by magicdanw on Fri, 01 Oct 2021 05:14:16 +0200

1. Use of immutable classes

There are the following codes

import lombok.extern.slf4j.Slf4j;

import java.text.ParseException;
import java.text.SimpleDateFormat;

@Slf4j(topic = "c.DateFormatMutable")
public class DateFormatMutable {
    public static void main(String[] args) {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                try {
                    log.debug("{}",sdf.parse("2021-10-01"));
                } catch (ParseException e) {
                    log.error("{}", e);
                }
            }).start();
        }
    }
}
// test result
Exception in thread "Thread-4" Exception in thread "Thread-3" Exception in thread "Thread-1" Exception in thread "Thread-2" java.lang.NumberFormatException: multiple points

resolvent:

  1. Locking: it can be realized, but it has performance problems
  2. Use immutable class: DateTimeFormatter

The code is as follows:

import lombok.extern.slf4j.Slf4j;

import java.time.format.DateTimeFormatter;

@Slf4j(topic = "c.DateFormatMutable")
public class DateFormatImmutable {
    public static void main(String[] args) {
        DateTimeFormatter sdf = DateTimeFormatter.ofPattern("yyyy-MM-dd");
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                log.debug("{}",sdf.parse("2021-10-01"));
            }).start();
        }
    }
}
// test result
[INFO] --- exec-maven-plugin:3.0.0:exec (default-cli) @ concurrent ---
2021-10-01 10:26:04.363 DEBUG [Thread-8] c.DateFormatMutable - {},ISO resolved to 2021-10-01
2021-10-01 10:26:04.363 DEBUG [Thread-3] c.DateFormatMutable - {},ISO resolved to 2021-10-01
2021-10-01 10:26:04.363 DEBUG [Thread-4] c.DateFormatMutable - {},ISO resolved to 2021-10-01
2021-10-01 10:26:04.363 DEBUG [Thread-2] c.DateFormatMutable - {},ISO resolved to 2021-10-01
2021-10-01 10:26:04.363 DEBUG [Thread-10] c.DateFormatMutable - {},ISO resolved to 2021-10-01
2021-10-01 10:26:04.363 DEBUG [Thread-6] c.DateFormatMutable - {},ISO resolved to 2021-10-01
2021-10-01 10:26:04.363 DEBUG [Thread-1] c.DateFormatMutable - {},ISO resolved to 2021-10-01
2021-10-01 10:26:04.363 DEBUG [Thread-9] c.DateFormatMutable - {},ISO resolved to 2021-10-01
2021-10-01 10:26:04.363 DEBUG [Thread-5] c.DateFormatMutable - {},ISO resolved to 2021-10-01
2021-10-01 10:26:04.363 DEBUG [Thread-7] c.DateFormatMutable - {},ISO resolved to 2021-10-01

Simply look at the source code of the changed class

* @implSpec
 * This class is immutable and thread-safe.
 *
 * @since 1.8
  • This class is immutable and thread safe
  • New classes introduced in jdk1.8

2. Design of immutable classes

Another familiar String class is immutable. Take it as an example to illustrate immutable elements

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];

    /** Cache the hash code for the string */
    private int hash; // Default to 0
	...
    public String(char value[], int offset, int count) {
        if (offset < 0) {
            throw new StringIndexOutOfBoundsException(offset);
        }
        if (count <= 0) {
            if (count < 0) {
                throw new StringIndexOutOfBoundsException(count);
            }
            if (offset <= value.length) {
                this.value = "".value;
                return;
            }
        }
        // Note: offset or count might be near -1>>>1.
        if (offset > value.length - count) {
            throw new StringIndexOutOfBoundsException(offset + count);
        }
        this.value = Arrays.copyOfRange(value, offset, offset+count);
    }
    ...
    public String substring(int beginIndex) {
        if (beginIndex < 0) {
            throw new StringIndexOutOfBoundsException(beginIndex);
        }
        int subLen = value.length - beginIndex;
        if (subLen < 0) {
            throw new StringIndexOutOfBoundsException(subLen);
        }
        return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
    }
    ...
}
  • Use of final

    • The attribute in the class is decorated with final to ensure that the attribute is read-only and cannot be modified
    • Class is decorated with final to ensure that the methods in the class cannot be overridden to prevent subclasses from inadvertently destroying immutability
  • defensive copy

There are some methods related to modification, such as subString. How is it implemented? See the source code. It is found that the String constructor is called internally to create a new String. When constructing a new String, the corresponding construction method will generate a new char[] value to copy the content.

This means of avoiding sharing by creating a replica object is called a protective copy.

3. Meta model - definition and embodiment

3.1 definition

When an immutable class tries to modify an object, the current object will create a new object to ensure thread safety. However, this brings a problem that too many objects will consume resources and low performance. How to solve it? Immutable classes are usually solved by associating design pattern - shared meta pattern classes.

Definition: a flyweight is an object that minimizes memory usage by sharing as much data as possible with other similar objects.

Applicable: when a limited number of objects of the same class need to be reused

Classification: structural pattern

3.2 embodiment

3.2.1 packaging

In JDK, wrapper classes such as Boolean, Byte, short, Integer, Long, and Character provide valueOf() methods. For example, the valueOf() method of Long caches Long objects between - 128 and 127. Objects will be reused in this range. New Long objects will be created only when the range is greater than this range. The source code is as follows:

 public static Long valueOf(long l) {
        final int offset = 128;
        if (l >= -128 && l <= 127) { // will cache
            return LongCache.cache[(int)l + offset];
        }
        return new Long(l);
    }
private static class LongCache {
        private LongCache(){}

        static final Long cache[] = new Long[-(-128) + 127 + 1];

        static {
            for(int i = 0; i < cache.length; i++)
                cache[i] = new Long(i - 128);
        }
    }

tips

  • The range of byte, short and long caches is - 128 ~ 127
  • Character cache range: 0 ~ 127
  • The default cache range of Integer is - 128 ~ 127, and the minimum value is immutable; The single maximum value can be changed by adjusting the virtual machine parameter - Djava.lang.Integer.IntegerCache.high
  • Boolean caches TRUE and FALSE

3.2.2 String string pool

Consult the relevant documents by yourself. There is no detailed explanation here. It will be discussed when learning the JVM.

3.2.3,BigDecimal BigInteger

Consult the relevant documents by yourself. There is no detailed explanation here. It will be discussed when learning the JVM.

4. Shared meta mode - immutable thread safety

We say that immutable classes are thread safe, but why did we use AtomicReference to include BigDecimal thread safe in our previous bank withdrawal case? Look at the code

import java.math.BigDecimal;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BiConsumer;

/**
 * CAS Lock free implementation
 */
public class AccountDecimalCAS implements AccountDecimal {

    private final AtomicReference<BigDecimal> balance;

    public AccountDecimalCAS(BigDecimal balance) {
        this.balance = new AtomicReference<>(balance);
    }

    @Override
    public BigDecimal getBalance() {
        return balance.get();
    }

    @Override
    public void withdraw(BigDecimal amount) {
        balance.getAndUpdate(m -> m.subtract(amount));
    }

//    @Override
//    public void withdraw(BigDecimal amount) {
//        while (true) {
//            BigDecimal prev = balance.get();
//            BigDecimal next = prev.subtract(amount);
//            if (balance.compareAndSet(prev, next)) {
//                break;
//            }
//        }
//    }

}

Observe the annotated part, in which BigDecimal's substract method is atomic and the source code

public BigDecimal subtract(BigDecimal subtrahend) {
        if (this.intCompact != INFLATED) {
            if ((subtrahend.intCompact != INFLATED)) {
                return add(this.intCompact, this.scale, -subtrahend.intCompact, subtrahend.scale);
            } else {
                return add(this.intCompact, this.scale, subtrahend.intVal.negate(), subtrahend.scale);
            }
        } else {
            if ((subtrahend.intCompact != INFLATED)) {
                // Pair of subtrahend values given before pair of
                // values from this BigDecimal to avoid need for
                // method overloading on the specialized add method
                return add(-subtrahend.intCompact, subtrahend.scale, this.intVal, this.scale);
            } else {
                return add(this.intVal, this.scale, subtrahend.intVal.negate(), subtrahend.scale);
            }
        }
    }
    
private static BigDecimal add(final long xs, int scale1, final long ys, int scale2) {
        long sdiff = (long) scale1 - scale2;
        if (sdiff == 0) {
            return add(xs, ys, scale1);
        } else if (sdiff < 0) {
            int raise = checkScale(xs,-sdiff);
            long scaledX = longMultiplyPowerTen(xs, raise);
            if (scaledX != INFLATED) {
                return add(scaledX, ys, scale2);
            } else {
                BigInteger bigsum = bigMultiplyPowerTen(xs,raise).add(ys);
                return ((xs^ys)>=0) ? // same sign test
                    new BigDecimal(bigsum, INFLATED, scale2, 0)
                    : valueOf(bigsum, scale2, 0);
            }
        } else {
            int raise = checkScale(ys,sdiff);
            long scaledY = longMultiplyPowerTen(ys, raise);
            if (scaledY != INFLATED) {
                return add(xs, scaledY, scale1);
            } else {
                BigInteger bigsum = bigMultiplyPowerTen(ys,raise).add(xs);
                return ((xs^ys)>=0) ?
                    new BigDecimal(bigsum, INFLATED, scale1, 0)
                    : valueOf(bigsum, scale1, 0);
            }
        }
    }
 

At present, any other single method selected in while is also atomic, but the combination of methods is not atomic operation, so AtomicReference is required to ensure the atomicity of the whole method and realize thread safety.

  • conclusion

    • A single method of an immutable class is atomic and thread safe
    • However, multiple method combinations are not atomic and require additional ways to ensure thread safety

5. Meta mode - custom connection pool

After learning immutable classes and understanding the meta pattern, we use our knowledge to implement a simple user-defined connection pool.

  • Scenario: an online mall application has thousands of QPS. If the database connection is re created and closed every time, the performance will be greatly affected. At this time, create a batch of connections in advance and put them into the connection pool. After a request arrives, obtain a connection from the connection pool; After use, return it to the connection pool. This not only saves time and resources, realizes reuse, can respond to client requests in time, and will not cause too much burden on the database.

  • The code is as follows

    import lombok.extern.slf4j.Slf4j;
    
    import java.sql.Connection;
    import java.util.concurrent.atomic.AtomicIntegerArray;
    
    /**
     * Custom thread connection pool
     */
    @Slf4j(topic = "c.Pool")
    public final class Pool {
        /** Connection pool size */
        private final int poolSize;
        /** Join array */
        private final Connection[] conns;
        /** Mark whether the connection is occupied, 0-unoccupied, 1-occupied */
        private final AtomicIntegerArray busies;
    
        /**
         * Construction method: initialize connection pool
         * @param poolSize      Initial connection pool size
         */
        public Pool(int poolSize) {
            this.poolSize = poolSize;
            conns = new Connection[poolSize];
            busies = new AtomicIntegerArray(new int[poolSize]);
            for (int i = 0; i < poolSize; i++) {
                conns[i] = new MockConnection("conn" + i);
            }
        }
    
        /**
         * Get connection from connection pool
         * @return      connect
         */
        public Connection getConnection() {
            // Check for free connections
            while (true) {
                for (int i = 0; i < poolSize; i++) {
                    // There are idle connections and return connections
                    if (busies.get(i) == 0) {
                        // Idle, modify flag, return connection
                        if (busies.compareAndSet(i, 0, 1)) {
                            log.debug("get {}", conns[i]);
                            return conns[i];
                        }
                    }
                }
                // No idle connections waiting
                synchronized (this) {
                    try {
                        log.debug("wait...");
                        this.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    
        /**
         * Return connection
         * @param conn      Connection to return
         */
        public void close(Connection conn) {
            for (int i = 0; i < poolSize; i++) {
                if (conns[i] == conn) {
                    // There will be no competition for returned connections
                    busies.set(i, 0);
                    synchronized (this) {
                        log.debug("close {}", conn);
                        this.notifyAll();
                    }
                    break;
                }
            }
        }
    }
    
    /**
     * connect
    */
    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public class MockConnection implements Connection {
        private String name;
    	...
    }
    
    
    import java.sql.Connection;
    import java.util.concurrent.TimeUnit;
    
    /**
     * Test connection pool
     */
    public class PoolTest {
        public static void main(String[] args) {
            Pool p = new Pool(2);
            for (int i = 0; i < 5; i++) {
                new Thread(() -> {
                    Connection connection = p.getConnection();
                    try {
                        // Time consuming for analog connection
                        TimeUnit.SECONDS.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } finally {
                        p.close(connection);
                    }
    
                }).start();
            }
        }
    }
    
    // test result
    2021-10-01 11:33:32.443 DEBUG [Thread-3] c.Pool - wait...
    2021-10-01 11:33:32.443 DEBUG [Thread-1] c.Pool - get MockConnection(name=conn0)
    2021-10-01 11:33:32.443 DEBUG [Thread-2] c.Pool - get MockConnection(name=conn1)
    2021-10-01 11:33:32.445 DEBUG [Thread-5] c.Pool - wait...
    2021-10-01 11:33:32.445 DEBUG [Thread-4] c.Pool - wait...
    2021-10-01 11:33:33.445 DEBUG [Thread-2] c.Pool - close MockConnection(name=conn1)
    2021-10-01 11:33:33.445 DEBUG [Thread-4] c.Pool - get MockConnection(name=conn0)
    2021-10-01 11:33:33.445 DEBUG [Thread-5] c.Pool - get MockConnection(name=conn1)
    2021-10-01 11:33:33.445 DEBUG [Thread-3] c.Pool - wait...
    2021-10-01 11:33:33.445 DEBUG [Thread-1] c.Pool - close MockConnection(name=conn0)
    2021-10-01 11:33:33.445 DEBUG [Thread-3] c.Pool - wait...
    2021-10-01 11:33:34.445 DEBUG [Thread-4] c.Pool - close MockConnection(name=conn0)
    2021-10-01 11:33:34.445 DEBUG [Thread-3] c.Pool - get MockConnection(name=conn0)
    2021-10-01 11:33:34.445 DEBUG [Thread-5] c.Pool - close MockConnection(name=conn1)
    2021-10-01 11:33:35.445 DEBUG [Thread-3] c.Pool - close MockConnection(name=conn0)
    

Question:

  • Dynamic increase and decrease of connection pool
  • Connection keep alive (availability test)
  • Wait for timeout processing
  • Distributed hash
  • ...

In practical application, we should use the mature connection pool to implement. Relational databases, such as C3P0, druid, etc. for more general object connection pool, we can consider apache commons pool, etc. when we learn about the framework in the future, we can see how the mature connection is implemented (source code) when we have time.

6. final principle

6.1 principle of setting final variable

After understanding the volatile principle, the implementation of final is simple compared with simple examples and bytecode

public class FinalTest {
    final int a = 20;
}



// class version 52.0 (52)
// access flags 0x21
public class com/gaogzhen/final01/FinalTest {

  // compiled from: FinalTest.java

  // access flags 0x10
  final I a = 20

  // access flags 0x1
  public <init>()V
   L0
    LINENUMBER 3 L0
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
   L1
    LINENUMBER 4 L1
    ALOAD 0
    BIPUSH 20
    PUTFIELD com/gaogzhen/final01/FinalTest.a : I
    	<-- Write barrier
    RETURN
   L2
    LOCALVARIABLE this Lcom/gaogzhen/final01/FinalTest; L0 L2 0
    MAXSTACK = 2
    MAXLOCALS = 1
}

It is found that the assignment of the final variable will also be completed through the putfield instruction. Similarly, a write barrier will be added after this instruction to ensure that other threads will not read its value as 0.

6.2 principle of obtaining final variable

final read optimization

  • Smaller value: copy a copy to the stack memory to read
  • Larger value: constant pool
  • Read heap memory without final, low performance

7. Stateless

When learning in the web stage and designing a Servlet, in order to ensure its thread safety, there will be such suggestions. Do not set member variables for the Servlet. This class without any member variables is thread safe.

Because the data saved by member variables can also be called state information, no member variables are called stateless.

QQ:806797785

Warehouse address: https://gitee.com/gaogzhen/concurrent

Topics: Concurrent Programming