Thread safety issues

Thread safety issues

Try not to talk nonsense where you can use the map. Let's take a look at a map first:

The figure above describes A multi-threaded execution scenario. Thread A and thread B read and write variables in main memory respectively. The variables in the main memory are shared variables, which are shared among multiple threads. However, threads cannot directly read and write shared variables in main memory. Each thread has its own working memory. When A thread needs to read and write shared variables in main memory, it needs to copy A copy of the variable to its own working memory, and then perform all operations on the copy of the variable in its own working memory, After the thread working memory completes the operation on the variable copy, it needs to synchronize the results to main memory.
 
When we operate shared data, thread safety problems may occur.
Example: threads A and B perform x + + operations on shared variable x at the same time.

1 thread A copies x=0 of the main memory to its own working memory – > 2 thread B also copies x=0 of the main memory; Copy to its own working memory – > 3 thread A performs x + + operation on X copy. At this time, x=1 – > 4 thread A synchronizes x=1 copy to main memory – > repeat 3,4 operations. At this time, main memory x=2 – > thread B performs x + + operation on X copy (x=0). At this time, x=1 – > thread B synchronizes x=1 copy to main memory. Main memory x=1.
Originally, after the operation of thread B, the x value in the main memory should be equal to 3, but the x value is equal to 1. The root cause is that x=2 in the main memory and x=0 in the working memory of thread B.

From the above, we can find that the root cause of thread safety is that multiple threads operate on shared data at the same time, resulting in inconsistent shared data in main memory and thread working memory. Solution: 1. Use thread synchronization mechanism: only one thread can access shared data at the same time. 2. 2. Eliminate shared data.

How to solve thread safety problems?

Thread unsafe example 1:

public class Test implements Runnable {
    private int x = 0;

    private  void count() {
        System.out.println(Thread.currentThread().getName() + "Got it x value" + x + "\t");
        x++;
        sleep(100);
        System.out.println(Thread.currentThread().getName() + "Output x value" + x + "\t");
    }

    private void sleep(long millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void run() {
        count();
    }

    public static void main(String[] args) {
        Test test = new Test();
        new Thread(test).start();
        new Thread(test).start();
    }
}

One of the operation results:
The x value obtained by Thread-0 is 0
The x value obtained by Thread-1 is 0
x value of Thread-0 output 2
x value of Thread-1 output 2
We can see that both threads output 2. This is thread unsafe.

Methods to solve thread safety

(1) Using synchronized
Synchronized: if the object it acts on is non static, the lock it obtains is an object; If the synchronized object is a static method or a class, the lock it obtains is a class. All objects of this class have the same lock, and the lock will be released automatically after the synchronization code block is executed.

Synchronization method
Modify the count() method with: synchronized. (the use of all methods below is improved on the basis of example 1)

private synchronized void count() {
        System.out.println(Thread.currentThread().getName() + "Got it x value" + x + "\t");
        x++;
        sleep(100);
        System.out.println(Thread.currentThread().getName() + "Output x value" + x + "\t");
    }

Synchronous code block
Synchronization is a high overhead operation, so the content of synchronization should be minimized. Generally, it is not necessary to synchronize the whole method. You can use the synchronized code block to synchronize the key code.
Use: synchronize code that operates on shared variables with synchronized code blocks

private void count() {
        synchronized(this){
        System.out.println(Thread.currentThread().getName() + "Got it x value" + x + "\t");
        x++;
        sleep(100);
        System.out.println(Thread.currentThread().getName() + "Output x value" + x + "\t");}
    }

(2) Using volatile
Volatile variables can be regarded as a kind of "less synchronized"; Volatile variables have synchronized visibility, but not atomic properties. This means that the thread can automatically discover the latest value of the volatile variable.
Use: modify the member variable x with volatile

 private volatile int x = 0;

One of the operation results:
The x value obtained by Thread-0 is 0
The x value obtained by Thread-1 is 0
x value of Thread-0 output 2
x value of Thread-1 output 2
We found that it does not solve the thread safety problem. As I said earlier, volatile can only ensure the atomicity of a single volatile variable, but it does not have atomicity for the composite operation of volatile + +.

Another example
Modify the member variable flag with volatile

public class Test  {
    public static volatile boolean flag = true;

    public static void main(String[] args) throws InterruptedException {

        new Thread(new Runnable() {
            @Override
            public void run() {
                while (flag) {
                    
                }
                System.out.println(Thread.currentThread().getName() + "The thread stops and the loop is opened");
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                flag = false;
                System.out.println(Thread.currentThread().getName() + "modify flag by" + flag);
            }
        }).start();
    }
}

Operation results:
Modify the flag of Thread-1 to false
Thread-0 thread deadlock is opened

Do not use volatile to modify the member variable flag

 public static  boolean flag = true;

Operation results:
Modify the flag of Thread-1 to false
Thread-0 will not end and will continue to loop.

(3) Use Lock
Lock must be released manually (when an exception occurs, the lock will not be released automatically). If the lock is not released actively, it may lead to deadlock. Therefore, generally speaking, lock must be used in the try... catch... Block, and the operation of releasing the lock must be placed in the finally block to ensure that the lock must be released and prevent deadlock.
Four methods are declared in the Lock interface to obtain the Lock.

  1. lock() method
  2. tryLock() method
  3. tryLock(long time, TimeUnit unit) method
  4. lockInterruptibly() method

ReentrantLock is a class that implements the Lock interface.

lock() method: get the lock. Wait if the lock has been acquired by another thread.

 private Lock lock=new ReentrantLock();//Declare this lock

    private  void count() {
        lock.lock();//Lock
        try {
            System.out.println(Thread.currentThread().getName() + "Got it x value" + x + "\t");
            x++;
            sleep(100);
            System.out.println(Thread.currentThread().getName() + "Output x value" + x + "\t");

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();//Unlock
        }
    }

Operation results:
The x value obtained by Thread-0 is 0
x value 1 of Thread-0 output
The x value obtained by Thread-1 is 1
x value of Thread-1 output 2

tryLock() method: the of the return value. If the lock is obtained successfully, return true; If the lock acquisition fails, false is returned, that is, the method will return immediately anyway (it will not wait there when the lock cannot be obtained).

private Lock lock=new ReentrantLock();//Declare this lock

    private  void count() {
        if(lock.tryLock()) {//Lock
            try {
                System.out.println(Thread.currentThread().getName() + "Got it x value" + x );
                x++;
                sleep(100);
                System.out.println(Thread.currentThread().getName() + "Output x value" + x );

            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();//Unlock
            }
        }else {
            System.out.println(Thread.currentThread().getName() +"Failed to acquire lock");
        }
    }

Operation results:
The x value obtained by Thread-0 is 0
Thread-1 failed to acquire lock
x value 1 of Thread-0 output

tryLock(long time, TimeUnit unit) method: it is similar to tryLock() method. The difference is that it will wait for a certain time when it can't get the lock. If it still can't get the lock during the waiting period, it returns false and can respond to the interrupt. If the lock is obtained during the waiting period, return true.

 private Lock lock=new ReentrantLock();//Declare this lock

    private  void count() {
        try {
            if(lock.tryLock(1000, TimeUnit.MILLISECONDS)) {//Lock
                try {
                    System.out.println(Thread.currentThread().getName() + "Got it x value" + x );
                    x++;
                    sleep(100);
                    System.out.println(Thread.currentThread().getName() + "Output x value" + x );

                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();//Unlock
                }
            }else {
                System.out.println(Thread.currentThread().getName() +"Failed to acquire lock");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();//In case of thread interruption, handle it accordingly
        }
    }

When lock.tryLock(10, TimeUnit.MILLISECONDS), the lock is not obtained during the waiting period. Operation results:
The x value obtained by Thread-1 is 0
Thread-0 failed to acquire lock
x value of Thread-1 output 1
lock.tryLock(1000, TimeUnit.MILLISECONDS), the lock is obtained during the waiting period. Operation results:
The x value obtained by Thread-0 is 0
x value 1 of Thread-0 output
The x value obtained by Thread-1 is 1
x value of Thread-1 output 2

lockInterruptibly() method: when acquiring a lock, if the thread is waiting to acquire the lock, the thread can respond to the interrupt, that is, interrupt the waiting state of the thread.

public class Test implements Runnable {
    private int x = 0;
    private Lock lock=new ReentrantLock();//Declare this lock

    private  void count() {
        try {
            lock.lockInterruptibly();
        try {
            System.out.println(Thread.currentThread().getName() + "Got it x value" + x);
            x++;
            System.out.println(Thread.currentThread().getName() + "Output x value" + x);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();//Unlock
        }
        } catch (InterruptedException e) {
            System.out.println(Thread.currentThread().getName() + "Was interrupted" );
        }
    }
    @Override
    public void run() {
        count();
    }

    public static void main(String[] args) {
        Test test = new Test();
        new Thread(test).start();
        Thread thread = new Thread(test);
        thread.start();
        thread.interrupt();
    }
}

Operation results:
Thread-1 was interrupted
The x value obtained by Thread-0 is 0
x value 1 of Thread-0 output

Note that the interrupt() method can only interrupt the thread in the blocking process, not the thread in the running process.

ReentrantReadWriteLock
We know that read operations and write operations will conflict, and write operations and write operations will also conflict, but read operations and read operations will not conflict. ReentrantReadWriteLock in Lock enables multiple threads to read only, and there will be no conflict between threads.

public class Test {
    private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
 
    public static void main(String[] args) {
        final Test test = new Test();
 
        new Thread("A") {
            public void run() {
                test.get(Thread.currentThread());
            };
        }.start();
 
        new Thread("B") {
            public void run() {
                test.get(Thread.currentThread());
            };
        }.start();
    }
 
    public void get(Thread thread) {
        rwl.readLock().lock(); // Get the lock outside
        try {
            long start = System.currentTimeMillis();
            System.out.println("thread " + thread.getName() + "Start read operation...");
            while (System.currentTimeMillis() - start <= 1) {
                System.out.println("thread " + thread.getName() + "A read operation is in progress...");
            }
            System.out.println("thread " + thread.getName() + "Read operation completed...");
        } finally {
            rwl.readLock().unlock();
        }
    }
}

Operation results:
Thread A starts A read operation
Thread B starts read operation
Thread A is reading
Thread A is reading
Thread B is reading
...
Thread A read operation completed
Thread B read operation completed
We can see

We can see that thread A and thread B perform read operations at the same time, which greatly improves the efficiency of read operations. However, it should be noted that if A thread has occupied A read lock, if other threads want to apply for A write lock at this time, the thread applying for A write lock will always wait for the read lock to be released. If A thread has occupied A write lock, if other threads apply for A write lock or A read lock, the applied thread will also wait for the write lock to be released.
(4) Using ThreadLocal
ThreadLocal creates a copy of variables for each thread. Each thread operates independently and does not affect each other (eliminates shared variables, and there is no thread safety problem without shared variables).

We know that SimpleDateFormat is thread unsafe. There are four solutions as follows.

  1. Define SimpleDateFormat as a local variable.
    Disadvantages: each time the method is called, a SimpleDateFormat object will be created, which wastes too much memory and puts pressure on GC.
  2. Method adds a synchronization lock synchronized.
    Disadvantages: poor performance. Every time, other threads can enter after the lock is released.
  3. The third-party library joda time is used to consider thread insecurity.
  4. Use ThreadLocal: each thread has its own SimpleDateFormat object. (recommended)

Tags: Java

Posted on Sun, 24 Oct 2021 18:29:56 -0400 by cupboy