java thread startup, security issues (atomicity, visibility)

Two ways to start threads

psvm(){
	/*Method 1*/
	Thread t1 = new Thread(){
	            int num;
	            @Override
	            public void run() {
	            //process
	            }
	};
	t1.start();  //It can only be started once
	t1.start();	 //report errors!!!
	Thread t2 = new Thread(){
	            int num;
	            @Override
	            public void run() {
	            //process  num++;
	            }
	};
	t2.start();  
	/*Method 2*/ 
	Runnable r1 = new Runnable() {
				int num;
	            @Override
	            public void run() {
	                //process num++;;
	            }
	};
	        new Thread(r1).start(); //Multiple threads can be started multiple times
	        new Thread(r1).start();	//New comes out with a new thread, which can be started again
}

Method 1 thread t1 can only be started once. If you want to restart, you need to create a new thread t2, and each time you start a new thread, the operation of num is local,
In method 2, the Runable object is encapsulated into multiple threads to start, and the operation on num is global

  • Thread
    • Class can only inherit single, which is not flexible enough
    • Thread can only be started once at a time, and the object can only be used in one thread
  • Runnable
    • Multiple interfaces can be implemented, which is flexible
    • Independent of the startup method, an object is encapsulated into multiple Thread objects for startup

Anonymous inner classes and common writing

The above is the anonymous inner class writing method
General method:

class ThreadAddNum extends Thread{
    int num;
    @Override
    public void run(){
        for(int i = 0; i < 10000; i++){
            num++;
        }
    }
}

class RunnableAddNum implements Runnable {
    int num;
    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            num++;
        }
    }
}

public class thread_test {

    public static void main(String[] args) {
        ThreadAddNum t1 = new ThreadAddNum(), t2=new ThreadAddNum();
        t1.start();
        t2.start();

        RunnableAddNum r1 = new RunnableAddNum();
        new Thread(r1).start();
        new Thread(r2).start();
    }
}

Thread safety issues

Atomicity and synchronized

Slightly change the previous code

class ThreadAddNum extends Thread{
    int num;
    @Override
    public void run(){
        for(int i = 0; i < 10000; i++){
            num++;
        }
    }
}

class RunnableAddNum implements Runnable {
    int num;
    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            num++;
        }
    }
}

public class thread_test {

    public static void main(String[] args) {
        ThreadAddNum t1 = new ThreadAddNum(), t2=new ThreadAddNum();
        t1.start();
        t2.start();
		
		RunnableAddNum r1 = new RunnableAddNum();
        Thread rt1 = new Thread(r1) , rt2=new Thread(r1);
        rt1.start();
        rt2.start();
		//Wait for RT1 and rt2 threads to end
        try {
            rt1.join();
            rt2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(r1.num);
    }
}

The function is the same, except for the output of num after changing the thread started with runnable
It is found that the num results are different every time And generally not 20000
According to our understanding, the answer should be 2w. What's the problem???

The problem is that two threads operate on num in parallel, and the process of changing num is to write and read num, then + +, and then write back
This may cause threads 1 and 2 to read num + + at the same time and write it back at the same time, which is equivalent to adding it only once, or thread 1 reads num and hasn't written it back yet. Thread 2 reads num again, and then thread 1 writes back and thread 2 writes back. The result written back by thread 2 covers thread 1, which is equivalent to adding it only once

Leads to thread insecurity:

  • Thread unsafe:
    Multiple threads can operate a resource at the same time without being regulated, which will make the thread unsafe
  • Solution: ensure the operation atomicity of resources
    • Atomicity: complete in one breath without interruption... Each operation of resources will not affect the results read by other threads

synchronized

The meaning of coordination and synchronization
synchronized usage
synchronized principle

Here, just rewrite the RunnaleAddNum class. Here are three rewriting methods:

//Method 1, synchronized modification method
class RunnableAddNum implements Runnable {
    int num;
    @Override
    synchronized  public void run() {
        for (int i = 0; i < 10000; i++) {
                num++;
        }
    }
}
//Method 2: synchronized (object) modifies the code block, and object uses this
class RunnableAddNum implements Runnable {
    int num;
    Object l = new Object();
    @Override
    synchronized  public void run() {
        for (int i = 0; i < 10000; i++) {
            synchronized (this){
                num++;
            }
        }
    }
}
//Method 3: synchronized (object) modifies the code block, and object uses l
class RunnableAddNum implements Runnable {
    int num;
    Object l = new Object();
    @Override
    synchronized  public void run() {
        for (int i = 0; i < 10000; i++) {
            synchronized (l){
                num++;
            }
        }
    }
}

Visibility and volatile

class RunnbaleVt implements Runnable{
    //volatile boolean flag = true;
    boolean flag = true;
    @Override
    public void run(){
        System.out.println ("Start thread.....");
        while(flag){
//            System.out.print("");
        }
        System.out.println ("Execution task completed.....");

    }
}

public class thread_test {
    	//Main thread
    public static void main(String[] args) {
        RunnbaleVt rtv = new RunnbaleVt();
        //Start thread 1
        new Thread(rtv).start();
		//Start thread 2
        new Thread() {
            @Override
            public void run() {
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                rtv.flag=false;
                System.out.println(rtv.flag);
                System.out.println("rtv.flag=false;");
            }
        }.start();
    }
}

When the thread executes the loop, it will "run flag=false", which will jump out when the loop is executed
Then start thread 2 and change the false value, but thread 1 will never jump out of the loop
explain

  • Because all objects are in heap memory (main memory), heap memory is shared by all threads,
  • Threads 1 and 2 have private stack space respectively,
  • Because the current cpu is multi-core, different threads run on different CPUs and have different cache systems. Due to the cache mechanism, after the thread reads the flag from the heap memory for the first time, it will be cached in its own cache, and the subsequent reading and writing are carried out with its own cache
    Therefore, the flag updated by thread 2 will not be read by thread 1
  • This is the problem of visibility It can be solved with volatile (see Figure)

volatile

A good explanation about volatile

Several key points are summarized:

  • volatile keyword is used to modify variables
  • Through the variable modified by volatile, all read operations about the variable will be directly read from the main memory, rather than the CPU's own cache. All write operations of this variable will be written to main memory
  • It mainly solves two problems:
    1. Visibility between multiple threads,
    2. CPU instruction reordering problem
  • Java volatile happens before rules ensure that instructions will not be emitted out of order, and make volatile have more effects:
    1. When Thread A modifies a volatile variable V, another Thread B immediately reads the variable v. Once Thread B reads variable V, not only variable V is visible to Thread B, but all variables visible to Thread A before Thread A modifies variable V will be visible to Thread B.
    2. When Thread A reads a volatile variable V, all other variables visible to Thread A are also read from main memory.
  • Atomicity cannot be guaranteed
  • Efficiency:
    If you understand the multi-level cache mechanism of the CPU (you can guess if you don't understand it), the efficiency of reading data from the main memory must be much lower than that from the CPU cache. The purpose of including instruction reordering is also to improve the computational efficiency. When the reordering mechanism is limited, the computational efficiency will be affected accordingly. Therefore, we should use volatile keyword only when we need to ensure variable visibility and order.

Keywords: Java Multithreading

Added by epimeth on Tue, 15 Feb 2022 16:15:40 +0200