Immutability of java Concurrent Programming sharing model

Posted by bynary on Fri, 04 Feb 2022 09:25:28 +0100

preface

Start with (6) and discuss some immutable classes. The article is based on the book "the art of Java Concurrent Programming" and the video of dark horse Dark horse multithreading Take notes.

1. Date conversion

For the date class SimpleDateFormat, since SimpleDateFormat is not thread safe, exceptions may occur in the use of SimpleDateFormat in the case of multithreading. The following is an example:

  private static void test1() {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        //Define 10 threads for parsing
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                try {
                    log.debug("{}", sdf.parse("2021-2-4"));
                } catch (Exception e) {
                    log.error("{}", e);
                }
            }).start();
        }
    }
java.lang.NumberFormatException: For input string: "202120212021E"
java.lang.NumberFormatException: For input string: ""
java.lang.NumberFormatException: multiple points
java.lang.NumberFormatException: multiple points


Multithreading is not safe. Of course, we can use synchronized to synchronize threads, but there is a problem that the efficiency of the program will be reduced

 private static void test2() {
	  SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
	     for (int i = 0; i < 10; i++) {
	         new Thread(() -> {
	             synchronized (sdf) {
	                 try {
	                     log.debug("{}", sdf.parse("1951-04-21"));
	                 } catch (Exception e) {
	                     log.error("{}", e);
	                 }
	             }
	         }).start();
	     }
	 }

JDK 8 provides a new date formatting class, DateTimeFormatter. This class is thread safe internally and its internal state cannot be modified. Therefore, we use DateTimeFormatter to replace SimpleDateFormat. Here is the code:

 private static void test3() {
        DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd");
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                //LocalDate.from method to create a localdate instance
                LocalDate date = dtf.parse("2018-10-01", LocalDate::from);
                log.debug("{}", date);
            }).start();
        }
    }

Through the above example, we can draw a conclusion: the internal design of the class is immutable, which is also a thread safe method



2. Immutable design

1. Design of string class: protective copy

Let's take the String class as an example to explain how String is immutable. Just find two methods


1,subString

It can be seen from the following method that the last returned String is a new String, which is created through new String. Note that the new String is stored on the heap when it comes out, so it can be seen from this method that the original String is not modified, but a new String is created. It can also be seen from the construction method that a new array is created internally to store the String value, so the original String has not changed. Even in the case of multithreading, thread safety will only be reflected in the creation of multiple String objects without modifying the original.

 public String substring(int beginIndex) {
        if (beginIndex < 0) {
            throw new StringIndexOutOfBoundsException(beginIndex);
        }
        int subLen = value.length - beginIndex;
        if (subLen < 0) {
            throw new StringIndexOutOfBoundsException(subLen);
        }
        //The key here is that you can see that new creates a new string
        return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
    }


 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;
            }
        }
       
        if (offset > value.length - count) {
            throw new StringIndexOutOfBoundsException(offset + count);
        }
        //The copyOfRange method is used here. See the following method
        this.value = Arrays.copyOfRange(value, offset, offset+count);
    }

 public static char[] copyOfRange(char[] original, int from, int to) {
        int newLength = to - from;
        if (newLength < 0)
            throw new IllegalArgumentException(from + " > " + to);
        //A new array is created internally to store the old content
        char[] copy = new char[newLength];
        System.arraycopy(original, from, copy, 0,
                         Math.min(original.length - from, newLength));
        return copy;
    }

2,concat

You can see that the new String method is also used internally to create a new String. The principle is not explained

 public String concat(String str) {
	  int otherLen = str.length();
	    if (otherLen == 0) {
	        return this;
	    }
	    int len = value.length;
	    char buf[] = Arrays.copyOf(value, len + otherLen);
	    str.getChars(buf, len);
	    return new String(buf, true);
	}



2. Design of final class

It is found that the class and all attributes in the class are final. Some basic knowledge of final can be seen here On final

  • The attribute 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 this class cannot be overwritten and prevent subclasses from inadvertently destroying immutability

For this knowledge, first give a video link: final principle , the final class is also introduced in the art of Java Concurrent Programming. A separate article will be written to introduce the memory semantics of final.

About the writing of the final variable: in short, a memory barrier will be added to prevent other threads from reading the variable before the assignment is completed.

On the writing of final variables: look at the following code and analyze it from the bytecode level

public class TestFinal {
    //Two static members become tired
    static int A = 10;
    static int B = Short.MAX_VALUE+1;
    
    //Two ordinary final become tired
    final int a = 20;
    final int b = Integer.MAX_VALUE;

    final void test1() {
        final int c = 30;
        new Thread(()->{
            System.out.println(c);
        }).start();

        final int d = 30;
        class Task implements Runnable {

            @Override
            public void run() {
                System.out.println(d);
            }
        }
        new Thread(new Task()).start();
    }

}

class UseFinal1 {
    public void test() {
        System.out.println(TestFinal.A);   //1
        System.out.println(TestFinal.B);   //2
        System.out.println(new TestFinal().a);   //3
        System.out.println(new TestFinal().b);   //4
        new TestFinal().test1();
    }
}

class UseFinal2 {
    public void test() {
        System.out.println(TestFinal.A);
    }
}

The following are the operations on 1 and 2 at the bytecode level

  L0
    LINENUMBER 33 L0
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    //This instance actually copies a copy of final to the stack, and then obtains the value in the stack
    //If final is not added, this instruction is GETSTATIC
    BIPUSH 10
    INVOKEVIRTUAL java/io/PrintStream.println (I)V
  L1
    LINENUMBER 34 L1
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    //Read the contents of the constant pool. After JDK8, it is in the heap
    LDC 32768
    INVOKEVIRTUAL java/io/PrintStream.println (I)V

In fact, one of the characteristics reflected in the above bytecode is that the variable modified by final is determined at the beginning and cannot be modified later. When you want to read, you can see that the ordinary value is copied to the stack for reading, while the maximum value is moved to the heap.



3. Yuan sharing mode

Definition: When you need to reuse a limited number of objects of the same class, you can use the meta pattern

In fact, it is to use the already created object instead of using the new object.


1. Embodiment

  • In JDK, wrapper classes such as Boolean, Byte, Short, Integer, Long and Character provide valueOf methods

    • Note: in the valueOf method, not all values in the range are reused, but only in a certain range. We can look at the source code and use short valueOf () as an example, you can see that it can only be reused between [- 128 ~ 127], and beyond this object, it cannot be reused.

    • The range of byte, short and long caches is - 128 - 127

    • The range of Character cache is 0 - 127

    • The default range of Integer is - 128 ~ 127. The minimum value cannot be changed, but the maximum value can be changed by adjusting the virtual machine parameter - "Djava.lang.Integer.IntegerCache.high"

    • Boolean caches TRUE and FALSE

    • As for why it is - 128 - 127, it may be that it is used more times in this interval. According to the thinking of writing items, some commonly used items are cached

  • String string pool

  • BigDecimal BigInteger (new is also used internally to create a new one to protect the old one)

However: thread safety of immutable classes means that a single function is thread safe, and it is not thread safe to combine or call multiple methods. For example, when testing BigDecimal, an atomic reference class is used to protect it.



2. Customize a connection pool

1. Parameter design and construction method

    //1. Specify the connection pool size
    private final int poolSize;

    //2. Array of connected objects
    private Connection[] connections;

    //3. Connection status array 0 idle 1 busy
    private AtomicIntegerArray status;

	//4. The constructor initializes the attribute
    public Pool(int poolSize){
        this.poolSize = poolSize;
        this.connections = new Connection[poolSize];
        this.status = new AtomicIntegerArray(new int[poolSize]);
        for(int i = 0; i < poolSize; i++){
            connections[i] = new MorkConnection("connect-" + (i + 1));
        }
    }



2. Connection method

Using the wait - notifyAll method, when a thread cannot get a connection, it uses wait to wait. In fact, it is better to use park - unpark here, because using wait - notifyAll may occur when notifyAll is performed before wait

//5. Borrow connection
    public Connection borrow(){
        while(true){
            for(int i = 0; i < poolSize; i++){
                //Got free connection
                if(status.get(i) == 0){
                	//Take out a connection and set the state of the corresponding subscript to 1
                    if(status.compareAndSet(i, 0, 1)){
                        log.debug("borrow:{}", connections[i]);
                        return connections[i];
                    }
                }
            }
            //If there is no idle connection, the current thread enters wait
            synchronized (this){
                try {
                    log.debug("wait...");
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }



3. Return the connection

CAS operation is not required when returning the connection, because if connections[i] == connection is judged to be true, it means that the connection of the current thread corresponds to a subscript, and other threads have other connections, which are different, so there will be no two threads entering here to set I to 0;

//6. Return the connection
public void free(Connection connection){
    for(int i = 0; i < poolSize; i++){
        if(connections[i] == connection){
            status.set(i, 0);
            synchronized (this){
                log.debug("free:{}", connection);
                this.notifyAll();
            }
            break;
        }
    }
}



4. All codes

@Slf4j
class Pool{
    //1. Specify the connection pool size
    private final int poolSize;

    //2. Array of connected objects
    private Connection[] connections;

    //3. Connection status array 0 idle 1 busy
    private AtomicIntegerArray status;

    //4. The constructor initializes the attribute
    public Pool(int poolSize){
        this.poolSize = poolSize;
        this.connections = new Connection[poolSize];
        this.status = new AtomicIntegerArray(new int[poolSize]);
        for(int i = 0; i < poolSize; i++){
            connections[i] = new MorkConnection("connect-" + (i + 1));
        }
    }

    //5. Borrow connection
    public Connection borrow(){
        while(true){
            for(int i = 0; i < poolSize; i++){
                //Got free connection
                if(status.get(i) == 0){
                    if(status.compareAndSet(i, 0, 1)){
                        log.debug("borrow:{}", connections[i]);
                        return connections[i];
                    }
                }
            }
            //If there is no idle connection, the current thread enters wait
            synchronized (this){
                try {
                    log.debug("wait...");
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    //6. Return the connection
    public void free(Connection connection){
        for(int i = 0; i < poolSize; i++){
            if(connections[i] == connection){
                status.set(i, 0);
                synchronized (this){
                    log.debug("free:{}", connection);
                    this.notifyAll();
                }
                break;
            }
        }
    }
}

//Connection, we only need to use it as a connection, and the methods inside do not need to be rewritten
class MorkConnection implements Connection{
    private String name;

    public MorkConnection(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "MorkConnection{" +
                "name='" + name + '\'' +
                '}';
    }

    //Omit a lot of rewriting methods
}



5. Test results

Poolsize pool = new Poolsize(2);
    for(int i = 0; i < 5; i ++){
        new Thread(()->{
            Connection connection = pool.borrow();
            try {
                Thread.sleep(new Random().nextInt(1000));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                pool.free(connection);
            }
        }).start();
    }
//1.  
15:29:13.226 [Thread-1] DEBUG cn.itcast.n7.Poolsize - wait...
15:29:13.226 [Thread-2] DEBUG cn.itcast.n7.Poolsize - borrow:MorkConnection{name='connect-2'}
15:29:13.231 [Thread-4] DEBUG cn.itcast.n7.Poolsize - wait...
15:29:13.231 [Thread-3] DEBUG cn.itcast.n7.Poolsize - wait...
15:29:13.226 [Thread-0] DEBUG cn.itcast.n7.Poolsize - borrow:MorkConnection{name='connect-1'}
The above shows that thread 2 and thread 0 have obtained the lock, and then the other three threads have not obtained it and are waiting

//2. 
//Thread 0 releases the lock
15:29:13.552 [Thread-0] DEBUG cn.itcast.n7.Poolsize - free:MorkConnection{name='connect-1'}
//Thread 3 gets lock get lock
15:29:13.552 [Thread-3] DEBUG cn.itcast.n7.Poolsize - borrow:MorkConnection{name='connect-1'}
15:29:13.552 [Thread-4] DEBUG cn.itcast.n7.Poolsize - wait...
15:29:13.552 [Thread-1] DEBUG cn.itcast.n7.Poolsize - wait...
//At this time, thread 4 and thread 1 are still waiting

//3. 
//Thread 2 released the lock
15:29:13.929 [Thread-2] DEBUG cn.itcast.n7.Poolsize - free:MorkConnection{name='connect-2'}
15:29:13.929 [Thread-4] DEBUG cn.itcast.n7.Poolsize - wait...
//Thread 1 acquired the lock
15:29:13.929 [Thread-1] DEBUG cn.itcast.n7.Poolsize - borrow:MorkConnection{name='connect-2'}
//Thread 4 is still waiting

//4. 
//Thread 1 release lock
15:29:14.348 [Thread-1] DEBUG cn.itcast.n7.Poolsize - free:MorkConnection{name='connect-2'}
//Thread 4 gets lock
15:29:14.348 [Thread-4] DEBUG cn.itcast.n7.Poolsize - borrow:MorkConnection{name='connect-2'}
//Finally, thread 3 releases the lock
15:29:14.355 [Thread-3] DEBUG cn.itcast.n7.Poolsize - free:MorkConnection{name='connect-1'}
//Thread 4 release lock
15:29:14.797 [Thread-4] DEBUG cn.itcast.n7.Poolsize - free:MorkConnection{name='connect-2'}

Process finished with exit code 0





If there is any error, please point it out!!!

Topics: Java Back-end Multithreading