JAVA Concurrent Programming: multithreading safety and performance issues

Posted by beanwebb on Thu, 16 Dec 2021 02:23:17 +0100

1. Thread safety

1.1 thread safety definition

● when multiple threads access an object, if the scheduling and alternate execution of these threads in the runtime environment are not considered, and no additional synchronization or any other coordination operation is required at the caller, the behavior of calling this object can or get the correct result, then this object is thread safe.
● in other words, when we use multithreading to access the properties and methods of an object, and when programming this business logic, we do not need to do additional processing (that is, we can do the same as single thread programming), and the program can run normally (without errors due to multithreading), which can be called thread safety.
● on the contrary, if it is necessary to consider the scheduling and alternation of these threads at runtime (for example, set() cannot be called during the call from get() to), or additional synchronization is required (such as using the synchronized keyword, etc.), then the thread is unsafe

1.2 thread insecurity

● from thread safety, we can call it thread unsafe if we need additional operations such as locking when we use multithreading to access an object.

1.3 why not make all classes thread safe?

● it has an impact on the running speed: if we want to make all classes thread safe, we must lock the operation of the object. At this time, when multiple threads do these operations, they cannot do them at the same time. There will also be additional overhead.
● in terms of design, it will also increase the design cost, increase the amount of code, and require a lot of manpower to optimize thread safety development.
● if a class will not be applied in multithreading, there is no need to design concurrent processing and over design.

2 how to avoid thread insecurity?

2.1 case description

  1. Unsafe index++
public class MultiThreadError implements Runnable {
    private static MultiThreadError multiThreadError = new MultiThreadError();
    private int index = 0;
    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            index++;
        }
    }
    
    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(multiThreadError);
        Thread thread2 = new Thread(multiThreadError);
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println(multiThreadError.index);
    }
}
11852

● note that the above results are not necessarily
● let's take a look at the situation that index + + occurs when two threads execute at the same time. The arrow is the execution order

● due to thread scheduling, thread 1 and thread 2 may have the above execution sequence, that is, both of our threads will add less index when executing index + +.

2.2 common problems: deadlock, livelock, starvation

  1. Deadlock Case
public class ThreadDeadlock {
    private static Object object1 = new Object();
    private static Object object2 = new Object();
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            System.out.println(" in 1 run");
            synchronized (object1) {
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (object2) {
                    System.out.println("1");
                }
            }
        });
        Thread thread1 = new Thread(() -> {
            System.out.println(" in 2 run");
            synchronized (object2) {
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (object1) {
                    System.out.println("2");
                }
            }
        });
        thread.start();
        thread1.start();
    }
}
in 1 run
 in 2 run

● the program will not stop, and it will be stuck in the second synchronized block of each run method

2.3 security of object publishing and initialization

● release: enable an object to be used by code outside the current scope.
● escape: an erroneous release.
a. Method returns a private object (private is meant to prevent external access)

public class ReleaseEffusion {
    private Map<String, String> states;
    public ReleaseEffusion() {
        this.states = new HashMap<>();
        this.states.put("1", "Monday");
        this.states.put("2", "Tuesday");
        this.states.put("3", "Wednesday");
        this.states.put("4", "Thursday");
        this.states.put("5", "Friday");
        this.states.put("6", "Saturday");
        this.states.put("7", "Sunday");
    }
    /**
     * Suppose weekly service is provided...
     * @return map
     */
    public Map<String,String> getStates() {
        return this.states;
    }
    public static void main(String[] args) {
        ReleaseEffusion releaseEffusion = new ReleaseEffusion();
        Map<String, String> states = releaseEffusion.getStates();
        System.out.println(states.get("1"));
        states.remove("1");
        System.out.println(states.get("1"));
    }
}

a. Provide the object to the outside world before completing initialization, such as:
■ assign a value to this after initialization in the constructor
■ implicit escape - register listener events
■ running thread in constructor

/**
 * Publish objects before initialization is complete
 */
public class ReleaseEffusionInit {
    private static Point point;
    public static void main(String[] args) throws InterruptedException {
        PointMaker pointMaker = new PointMaker();
        pointMaker.start();
        Thread.sleep(10);
        if (null != point) {
            System.out.println(point);
        }
        TimeUnit.SECONDS.sleep(1);
        if (null != point) {
            System.out.println(point);
        }
    }
    private static class Point{
        private final int x, y;
        public Point(int x, int y) throws InterruptedException {
            this.x = x;
            ReleaseEffusionInit.point = this;
            TimeUnit.SECONDS.sleep(1);
            this.y = y;
        }
        @Override
        public String toString() {
            return "Point{x=" + x + ", y=" + y + '}';
        }
    }
    private static class PointMaker extends Thread {
        @Override
        public void run() {
            try {
                new Point(1, 1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
Point{x=1, y=0}
Point{x=1, y=1}
Process finished with exit code 0 
/**
 * Listener mode
 */
public class ReleaseEffusionListener {
    public static void main(String[] args) {
        Source source = new Source();
        new Thread(() -> {
            try {
                TimeUnit.MILLISECONDS.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            source.eventCome(new Event() {
            });
        }).start();
        new ReleaseEffusionListener(source);
    }
    int count;
    public ReleaseEffusionListener(Source source) {
        source.registerListener(event -> {
            System.out.println("\n I get the number:" + count);
        });
        for (int i = 0; i < 10000; i++) {
            System.out.print(i);
        }
        count = 100;
    }
    private static class Source {
        private EventListener listener;
        void registerListener(EventListener eventListener) {
            this.listener = eventListener;
        }
        void eventCome(Event e) {
            if (null != listener) {
                listener.onEvent(e);
            } else {
                System.out.println("Not initialized");
            }
        }
    }
    private interface EventListener {
        void onEvent(Event e);
    }
    interface Event { }
}
0123456789.......
I got the number: 0
28532854285528...9999
Process finished with exit code 0
/**
 * Constructor start thread
 * @author yiren
 */
public class ReleaseEffusionConstructorStartThread {
    private Map<String, String> states;
    public ReleaseEffusionConstructorStartThread() {
        new Thread(() -> {
            this.states = new HashMap<>();
            this.states.put("1", "Monday");
            this.states.put("2", "Tuesday");
            try {
                Thread.sleep(300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            this.states.put("3", "Wednesday");
            this.states.put("4", "Thursday");
            this.states.put("5", "Friday");
            this.states.put("6", "Saturday");
            this.states.put("7", "Sunday");
        }).start();
    }
    /**
     * Suppose weekly service is provided...
     *
     * @return map
     */
    public Map<String, String> getStates() {
        return this.states;
    }
    public static void main(String[] args) {
        ReleaseEffusionConstructorStartThread releaseEffusion = new ReleaseEffusionConstructorStartThread();
        Map<String, String> states = releaseEffusion.getStates();
        System.out.println(states.get("1"));
        states.remove("1");
        System.out.println(states.get("1"));
        System.out.println(states.get("3"));
    }
}
Exception in thread "main" java.lang.NullPointerException
	at com.imyiren.concurrency.thread.safe.ReleaseEffusionConstructorStartThread.main(ReleaseEffusionConstructorStartThread.java:43)
Process finished with exit code 1

● how to solve escape
a. Return copy

// The above code plus this method is OK
    public Map<String, String> getStatesCopy() {
        return new HashMap<>(this.states);
    }

a. Factory mode repair listener

/**
 * Listener mode uses factory mode to fix it
 */
public class ReleaseEffusionListenerFix {
    public static void main(String[] args) {
        Source source = new Source();
        new Thread(() -> {
            try {
                TimeUnit.MILLISECONDS.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            source.eventCome(new Event() {
            });
        }).start();
        ReleaseEffusionListenerFix.getInstance(source);
    }
    private int count;
    private EventListener listener;
    public static ReleaseEffusionListenerFix getInstance(Source source) {
        ReleaseEffusionListenerFix releaseEffusionListenerFix = new ReleaseEffusionListenerFix(source);
        source.registerListener(releaseEffusionListenerFix.listener);
        return releaseEffusionListenerFix;
    }
    private ReleaseEffusionListenerFix(Source source) {
        listener = event -> System.out.println("\n I get the number:" + count);
        for (int i = 0; i < 10000; i++) {
            System.out.print(i);
        }
        count = 100;
    }
    private static class Source {
        private EventListener listener;
        void registerListener(EventListener eventListener) {
            this.listener = eventListener;
        }
        void eventCome(Event e) {
            if (null != listener) {
                listener.onEvent(e);
            } else {
                System.out.println("Not initialized");
            }
        }
    }
    private interface EventListener {
        void onEvent(Event e);
    }
    interface Event { }
}

2.3 some situations where thread safety issues need to be considered

● access shared variables or resources, such as attributes, static variables, cache, database, etc
● if sequential operations are required, even if each step is thread safe, there may be safety problems. For example, read first and then modify, check first and then execute
● there is a binding relationship between different data, such as ip and port number
● when using classes provided by others or third parties. Verify that the other party declares thread safety. Such as HashMap and ConcurrentHashMap.

3. Performance problems of multithreading

3.1 performance problems

● the most obvious experience is slow! For example, the front end invokes an excuse and returns the result for a long time or times out directly.

3.2 causes of performance problems

  1. Thread scheduling: context switching
    ○ what is the context?
    ■ it is to switch up and down the thread state or data to be saved (for example, where the thread is executed and what are the registers involved in the operation) to ensure the resumption of thread execution.
    ○ cache overhead
    ■ when a thread is operating in the CPU, some data needs to be put into the CPU cache. If the context is switched, the CPU cache of the current thread will become invalid. Then the CPU needs to cache the new thread data again. Therefore, the CPU starts slowly when starting a new thread, which is because most of the CPU's previous cache has failed.
    ○ what causes frequent context switching?
    ■ multiple threads compete for locks and IO read / write
  2. Multi thread collaboration: memory synchronization
    ○ when our program is running, the compiler and CPU will optimize the program, such as instruction reordering to make greater use of the cache. However, if multithreading is writing, we will use some means to prohibit instruction reordering to ensure thread safety. In addition, when multiple threads are running, JMM indicates that the thread will have a private memory area. If multiple threads want to ensure the latest data, they will synchronize the latest data in main memory, which will also bring performance overhead.

Topics: Java security