Chapter 3 - management of shared model

Chapter 3 - management of shared model (1)

Contents of this chapter

  • Sharing problem
  • synchronized
  • Thread safety analysis
  • Monitor
  • wait/notify
  • Thread state transition
  • Activity
  • Lock

Problems caused by sharing

Little story

  • Lao Wang (operating system) has a powerful abacus (CPU). Now he wants to rent it and earn some extra money

  • Xiao Nan and Xiao nu (thread) use this abacus to make some calculations and pay Lao Wang according to the time
  • But Xiao Nan can't use the abacus 24 hours a day. He often has to take a nap, or go to eat and go to the bathroom (blocking io operation). Sometimes he needs a cigarette. When there is no cigarette, he has no idea. These situations are collectively referred to as (blocking)

  • At these times, the abacus was not used (I couldn't collect money), and Lao Wang felt a little uneconomical
  • In addition, the little girl also wants to use the abacus. If Xiao Nan always occupies the abacus, she will feel unfair
  • So Lao Wang had an idea and thought of a way [let each of them use the abacus for a while and take turns]
  • In this way, when Xiaonan is blocked, the abacus can be distributed to the little girl without waste, and vice versa
  • Recently, the calculation performed is complex and needs to store some intermediate results, but the students' brain capacity (working memory) is not enough, so Lao Wang applied for a notebook (main memory) and recorded some intermediate results in the notebook first
  • The calculation process is like this

  • However, due to the time-sharing system, an accident occurred one day
  • Xiao Nan just read the initial value 0 and did a + 1 operation. He hasn't had time to write back the result
  • Lao Wang said [Xiao Nan, your time is up, it's someone else's turn, remember the result, let's go], so Xiao Nan said [the result is 1, the result is 1...] unwilling to stay aside (context switching)
  • Lao Wang said, "little girl, it's your turn". The little girl saw that the notebook also wrote 0, did a - 1 operation, and wrote the result - 1 into the notebook
  • At this time, the little girl ran out of time. Lao Wang woke up Xiao Nan again: [Xiao Nan, finish your last question], and Xiao Nan wrote the result 1 in his mind into his notebook

  • Both Xiaonan and Xiaonv think they have done nothing wrong, but the result in the notebook is 1 instead of 0

The embodiment of Java code triggered by the story

Two threads do self increment and self decrement for static variables with initial value of 0 for 5000 times respectively. Is the result 0?

@Slf4j(topic = "c.Test17")
public class Test17 {
    static int count = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                count++;
            }
        }, "t1");

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                count--;
            }
        }, "t2");

        t1.start();
        t2.start();
        t1.join();
        t2.join();
        log.debug("{}", count);
    }

}

Program running result is not unique:

problem analysis

  • The above results may be positive, negative or zero. Why? Because the self increment and self decrement of static variables in Java are not atomic operations, they must be analyzed from bytecode to fully understand them
  • For example, for i + + (i is a static variable), the following JVM bytecode instructions will actually be generated:

  • The corresponding i-- is similar:

  • The memory model of Java is as follows. To complete the self increase and self decrease of static variables, data exchange needs to be carried out in main memory and working memory:

Thread 1 reads the value of i from the main memory, then performs i + +, and then stores the value of i in the main memory

Thread 2 reads in the value of i from main memory, then performs i –, and then stores the value of i in main memory

  • If it is a single thread, there is no problem that the above 8 lines of code are executed in sequence (without interleaving):

However, these 8 lines of code may run alternately under multithreading:

  • In case of negative numbers:

  • When a positive number occurs:

Critical Section

  • There is no problem for a program to run multiple threads
  • The problem is that multiple threads access shared resources
    • In fact, there is no problem for multiple threads to read shared resources
    • When multiple threads read and write to shared resources, instruction interleaving occurs, which will lead to problems
  • If there are multithreaded read-write operations on shared resources in a code block, this code block is called a critical area

For example, the critical area in the following code

static int counter = 0;

static void increment() 
// Critical zone
{ 
	counter++; 
}

static void decrement() 
// Critical zone
{ 
	counter--; 
}

Race Condition

When multiple threads execute in the critical area, the results cannot be predicted due to different execution sequences of codes, which is called race condition

synchronized solutions

In order to avoid the competitive conditions in the critical zone, there are many means to achieve the goal.

  • Blocking solution: synchronized, Lock
  • Non blocking solution: atomic variables

This time, the blocking solution: synchronized is used to solve the above problem, which is commonly known as the [object lock]. It uses a mutually exclusive way so that at most one thread can hold the [object lock] at the same time. When other threads want to obtain the [object lock], it will be blocked. In this way, it can ensure that the thread with lock can safely execute the code in the critical area without worrying about thread context switching

be careful

Although the synchronized keyword can be used to complete mutual exclusion and synchronization in java, they are different:

  1. Mutual exclusion is to ensure that the race condition of the critical area occurs. Only one thread can execute the code of the critical area at the same time
  2. Synchronization is due to the different execution order of threads. One thread needs to wait for other threads to run to a certain point

synchronized

grammar

synchronized(object) // Thread 1, thread 2(blocked)
{
	Critical zone
}

solve

public class Test17 {
    static int count = 0;
    static final Object room = new Object();
    
  public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                synchronized (room){
                    count++;
                }
            }
        }, "t1");

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                synchronized (room){
                    count--;
                }
            }
        }, "t2");

        t1.start();
        t2.start();
        t1.join();
        t2.join();
        log.debug("{}", count);
    }
}

No matter how many times it is run, the result is 0

Analysis of synchronized principle

An analogy can be made:

  • The object in synchronized can be imagined as a room with a unique entrance (door). Only one person can enter the room at a time for calculation, and threads t1 and t2 can be imagined as two people
  • When thread t1 executes synchronized(room), it is like t1 enters the room, locks the door, takes away the key, and executes count + + code in the door
  • At this time, if t2 also runs to synchronized(room), it finds that the door is locked and can only wait outside the door. Context switching occurs and it is blocked
  • In this process, even if t1's cpu time slice is unfortunately used up and kicked out of the door (don't mistakenly understand that it can be executed all the time if the object is locked), the door is still locked, t1 still holds the key, and t2 thread is still blocked and can't enter. Only when t1 gets the time slice again can it open the door and enter
  • When t1 finishes executing the code in the synchronized {} block, it will come out of the obj room and unlock the door, wake up the t2 thread and give him the key. The t2 thread can then enter the obj room, lock the door, take the key, and execute its count -- code

Represented by a diagram

reflection

synchronized actually uses object locks to ensure the atomicity of the code in the critical area. The code in the critical area is inseparable externally and will not be interrupted by thread switching.

To deepen your understanding, please think about the following questions

  • If synchronized(obj) is placed outside the for loop, how to understand it-- Atomicity
    • It is locked for 5000 operations in the whole cycle. Guaranteed atomicity
  • What happens if t1 synchronized(obj1) and t2 synchronized(obj2)-- Lock object
    • Different objects are equivalent to different locks, so atomicity cannot be guaranteed.
  • What happens if T1 is synchronized (obj) but t2 is not? How to understand-- Lock object
    • For the same shared resource, all operations must be locked. Otherwise, t1 is locked, but t2 is not locked. When t2 runs to the critical area, it will not try to get the lock and will not be blocked.

Object oriented improvement

Put the shared variables that need to be protected into a class

@Slf4j(topic = "c.Test17")
public class Test17 {

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

        Room room = new Room();

        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                room.increment();
            }
        }, "t1");

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                room.decrement();
            }
        }, "t2");

        t1.start();
        t2.start();
        t1.join(); //Wait for t1 thread to end
        t2.join(); //Wait for t2 thread to end
        log.debug("{}", room.getCounter());
    }

}

class Room {
    private int counter = 0;

    public void increment() {
        synchronized (this) {
            counter++;
        }
    }

    public void decrement() {
        synchronized (this) {
            counter--;
        }
    }

    public int getCounter() {
        synchronized (this) {
            return counter;
        }
    }

}

synchronized on method

class Test{

	public synchronized void test() {
 
  }

}

// Equivalent to
class Test{

	public void test() {
		synchronized(this) { //this object is locked
 		
    }
 	}

}
class Test{

  public synchronized static void test() {
 
  }

}

// Equivalent to
class Test{

  public static void test() {
    synchronized(Test.class) { //Locked is the Test class
      
    }
  }

}

Method without synchronized

The method of not synchronizing is like people who don't obey the rules and don't line up honestly (like going through the window)

The so-called "thread eight locks"

In fact, it is to investigate which object synchronized locks

Case 1:12 or 21

@Slf4j(topic = "c.Test8Locks")
public class Test8Locks {

    public static void main(String[] args) {
        Number n1 = new Number();

        new Thread(() -> { //Thread 1
            log.debug("begin");
            n1.a();
        }).start();

        new Thread(() -> { //Thread 2
            log.debug("begin");
            n1.b();
        }).start();
    }

}

@Slf4j(topic = "c.Number")
class Number{

    public synchronized void a() { //this object is locked
        log.debug("1");
    }

    public synchronized void b() { //this object is locked
        log.debug("2");
    }

}

Case 2: 1 2 after 1s, or 1 after 2 1s

@Slf4j(topic = "c.Test8Locks")
public class Test8Locks {

    public static void main(String[] args) {
        Number n1 = new Number();

        new Thread(() -> { //Thread 1
            log.debug("begin");
            n1.a();
        }).start();

        new Thread(() -> { //Thread 2
            log.debug("begin");
            n1.b();
        }).start();
    }

}

@Slf4j(topic = "c.Number")
class Number{

    public synchronized void a() {
        sleep(1);
        log.debug("1");
    }

    public synchronized void b() {
        log.debug("2");
    }

}

  • Method a slept for 1 second.
  • It may be that thread 1 gets the lock first and executes method a, but sleeps for 1 second, then prints 1, and then thread 2 gets the lock and prints 2 immediately. So the result is 1 second after 1 second
  • Thread 2 may get the lock first, execute method b to print 2, and then thread 1 gets the lock, immediately execute method a, sleep for 1 second, and then print 1. So the result is 1 after 2 1s

Case 3: 12 or 23 1s after 3 1s, 1 or 32 1s after 1

@Slf4j(topic = "c.Test8Locks")
public class Test8Locks {

    public static void main(String[] args) {
        Number n1 = new Number();

        new Thread(() -> { //Thread 1
            log.debug("begin");
            n1.a();
        }).start();

        new Thread(() -> { //Thread 2
            log.debug("begin");
            n1.b();
        }).start();

        new Thread(() -> { //Thread 3
            log.debug("begin");
            n1.c();
        }).start();
    }

}

@Slf4j(topic = "c.Number")
class Number{

    public synchronized void a() {
        sleep(1);
        log.debug("1");
    }

    public synchronized void b() {
        log.debug("2");
    }

    public  void c() {
        log.debug("3");
    }

}

  • The c method is not synchronized and will not be locked, so 3 will print before 1 (because 1 will sleep for 1 second first).
  • The order of printing 2 or 3 is not certain. It depends on who gets the time slice first

Case 4: 2 1s later

@Slf4j(topic = "c.Test8Locks")
public class Test8Locks {

    public static void main(String[] args) {
        Number n1 = new Number();
        Number n2 = new Number();

        new Thread(() -> {
            log.debug("begin");
            n1.a();
        }).start();

        new Thread(() -> {
            log.debug("begin");
            n2.b();
        }).start();

    }

}

@Slf4j(topic = "c.Number")
class Number{

    public synchronized void a() { //this object is locked
        sleep(1);
        log.debug("1");
    }

    public synchronized void b() { //this object is locked
        log.debug("2");
    }

}

  • The two methods do not lock the same object. Thread 1 locks the n1 object and thread 2 locks the n2 object.
  • Because thread 1 always sleeps for 1s before printing 1. Thread 2 does not need to wait for the lock of 1, so it can print 2 directly. So always print 1 after 1s of print 2.

Case 5:2 1s later

@Slf4j(topic = "c.Test8Locks")
public class Test8Locks {

    public static void main(String[] args) {
        Number n1 = new Number();

        new Thread(() -> {
            log.debug("begin");
            n1.a();
        }).start();

        new Thread(() -> {
            log.debug("begin");
            n1.b();
        }).start();

    }
}

@Slf4j(topic = "c.Number")
class Number{

    public static synchronized void a() { //Locked is the Number class object
        sleep(1);
        log.debug("1");
    }

    public synchronized void b() { //this object is locked
        log.debug("2");
    }

}

  • The a method is static, and the locked object is number class
  • b method is a member method that locks this of the object
  • The objects of the two locks are different, so they are executed at the same time, but because a always sleep for 1s before printing 1. So the result is: 1 after 2 1s

Case 6: 12 after 1s, or 1 after 2 1s

@Slf4j(topic = "c.Test8Locks")
public class Test8Locks {

    public static void main(String[] args) {
        Number n1 = new Number();

        new Thread(() -> {
            log.debug("begin");
            n1.a();
        }).start();

        new Thread(() -> {
            log.debug("begin");
            n1.b();
        }).start();

    }
}

@Slf4j(topic = "c.Number")
class Number{

    public static synchronized void a() { //Locked is the Number class object
        sleep(1);
        log.debug("1");
    }

    public static synchronized void b() { //Locked is the Number class object
        log.debug("2");
    }

}

  • Methods a and b are static static methods, and the object of the lock is number class
  • So they have the same lock

Case 7:2 1s later

@Slf4j(topic = "c.Test8Locks")
public class Test8Locks {

    public static void main(String[] args) {
        Number n1 = new Number();
        Number n2 = new Number();

        new Thread(() -> {
            log.debug("begin");
            n1.a();
        }).start();

        new Thread(() -> {
            log.debug("begin");
            n2.b();
        }).start();

    }
}
@Slf4j(topic = "c.Number")
class Number{

    public static synchronized void a() { //Locked is the Number class object
        sleep(1);
        log.debug("1");
    }

    public synchronized void b() { //this object is locked
        log.debug("2");
    }

}

  • The lock of method a is number class
  • The lock of b method is n2 object
  • The two locks are different

Case 8: 12 after 1s, or 1 after 2 1s

@Slf4j(topic = "c.Test8Locks")
public class Test8Locks {

    public static void main(String[] args) {
        Number n1 = new Number();
        Number n2 = new Number();

        new Thread(() -> {
            log.debug("begin");
            n1.a();
        }).start();

        new Thread(() -> {
            log.debug("begin");
            n2.b();
        }).start();

    }
}
@Slf4j(topic = "c.Number")
class Number{

    public static synchronized void a() { //Locked is the Number class object
        sleep(1);
        log.debug("1");
    }

    public static synchronized void b() { //Locked is the Number class object
        log.debug("2");
    }

}

  • The lock of method a is number The lock of the class, b method is also number class
  • Although it is called through two different objects: n1 and n2, there is only one class. So it's the same lock

Thread safety analysis of variable

Are member variables and static variables thread safe?

  • If they are not shared, they are thread safe
  • If they are shared, they can be divided into two cases according to whether their state can be changed
    • Thread safe if there are only read operations
    • If there are read and write operations, this code is a critical area and thread safety needs to be considered

Are local variables thread safe?

  • Local variables are thread safe
  • However, objects referenced by local variables (which can also be referenced by others) are not necessarily
    • If the object is not accessed as an escape method, it is thread safe
    • If the object escapes from the scope of the method, such as return, thread safety needs to be considered

Thread safety analysis of local variables

public static void test1() {
  int i = 10; 
  i++; 
}

When each thread calls the test1() method, multiple copies of the local variable i will be created in the stack frame memory of each thread, so there is no sharing. The local variable is private to each thread

As shown in the figure:

The reference of local variables is slightly different. Let's take a look at an example of member variables

list is a member variable

public class TestThreadSafe {

    static final int THREAD_NUMBER = 2;
    static final int LOOP_NUMBER = 200;

    public static void main(String[] args) {
        ThreadUnsafe test = new ThreadUnsafe();
        for (int i = 0; i < THREAD_NUMBER; i++) { //Create 2 threads
            new Thread(() -> {
                test.method1(LOOP_NUMBER); //200 cycles
            }, "Thread" + (i+1)).start();
        }

    }

}

class ThreadUnsafe {

		// Member variable list
    ArrayList<String> list = new ArrayList<>();

    public void method1(int loopNumber) {
        for (int i = 0; i < loopNumber; i++) {
          // {critical zone, which will produce race conditions
            method2();
            method3();
          // }Critical zone
        }
    }

    private void method2() {
        list.add("1"); //Add an element to the collection
    }

    private void method3() {
        list.remove(0); //Delete an element from the collection
    }
}

In one case, thread 2 preempts the time slice to execute method1. If thread 1 has not been add ed, thread 2 remove will report an error:

  • method2 in any thread refers to the list member variable in the same object in the heap
  • method3 is the same as method2 analysis

Change list to local variable

public class TestThreadSafe {

    static final int THREAD_NUMBER = 2;
    static final int LOOP_NUMBER = 200;

    public static void main(String[] args) {
        ThreadUnsafe test = new ThreadUnsafe();
        for (int i = 0; i < THREAD_NUMBER; i++) { //Create 2 threads
            new Thread(() -> {
                test.method1(LOOP_NUMBER); //200 cycles
            }, "Thread" + (i+1)).start();
        }

    }

}

class ThreadSafe {

    public final void method1(int loopNumber) {
        ArrayList<String> list = new ArrayList<>(); //Local variable list
        for (int i = 0; i < loopNumber; i++) {
            method2(list);
            method3(list);
        }
    }

    private void method2(ArrayList<String> list) {
        list.add("1");
    }

    private void method3(ArrayList<String> list) {
        list.remove(0);
    }
}

Then there will be no such problem

  • list is a local variable. When each thread calls, it will create a different instance without sharing
  • The parameters of method2 are passed from method1 and reference the same object as method1
  • The parameter analysis of method3 is the same as that of method2

Exposure list

Thinking brought by the method access modifier, if the methods of method2 and method3 are modified to public, will it proxy thread safety?

  • Case 1: there are other threads calling method2 and method3
    • Other threads call method2, and the parameter passed is not the list of method1, so there will be no thread safety problem
  • Case 2: on the basis of case 1, add a subclass for ThreadSafe class, and the subclass overrides method2 or method3 methods, i.e
    • After subclass rewrites method3, thread 1 or thread 2 will come to rewritten method3 and use the same shared resources as thread 3, so thread safety problems may arise
public class TestThreadSafe {

    static final int THREAD_NUMBER = 2;
    static final int LOOP_NUMBER = 200;

    public static void main(String[] args) {
        ThreadSafeSubClass test = new ThreadSafeSubClass();
        for (int i = 0; i < THREAD_NUMBER; i++) { //Create 2 threads
            new Thread(() -> {
                test.method1(LOOP_NUMBER); //200 cycles
            }, "Thread" + (i+1)).start();
        }

    }

}

class ThreadSafe {

    public void method1(int loopNumber) {
        ArrayList<String> list = new ArrayList<>(); //Local variable list
        for (int i = 0; i < loopNumber; i++) {
            method2(list);
            method3(list);
        }
    }

    public void method2(ArrayList<String> list) {
        list.add("1");
    }

    public void method3(ArrayList<String> list) {
        list.remove(0);
    }

}

class ThreadSafeSubClass extends ThreadSafe{

    @Override
    public void method3(ArrayList<String> list) {
        new Thread(() -> {
            list.remove(0);
        },"Thread3").start();
    }

}

  • From this example, we can see the meaning of [security] provided by private or final. Please understand the [closing] in the opening and closing principle
  • You can add final to method1 to prevent it from being overridden, and private to method2 and method3 to prevent it from being overridden by subclasses
public class TestThreadSafe {

    static final int THREAD_NUMBER = 2;
    static final int LOOP_NUMBER = 200;

    public static void main(String[] args) {
        ThreadSafeSubClass test = new ThreadSafeSubClass();
        for (int i = 0; i < THREAD_NUMBER; i++) { //Create 2 threads
            new Thread(() -> {
                test.method1(LOOP_NUMBER); //200 cycles
            }, "Thread" + (i + 1)).start();
        }

    }

}

class ThreadSafe {

    public final void method1(int loopNumber) {
        ArrayList<String> list = new ArrayList<>(); //Local variable list
        for (int i = 0; i < loopNumber; i++) {
            method2(list);
            method3(list);
        }
    }

    private void method2(ArrayList<String> list) {
        list.add("1");
    }

    private void method3(ArrayList<String> list) {
        list.remove(0);
    }

}

class ThreadSafeSubClass extends ThreadSafe {

    public void method3(ArrayList<String> list) {
        new Thread(() -> {
            list.remove(0);
        }, "Thread3").start();
    }

}
  • After adding private, the subclass cannot override method3. Method3 of the subclass is only used as its own member method.
  • After startup, both thread 1 and thread 2 will access method2 and method3 of this class, so they have their own local variable list, so there will be no shared resources, so there will be no thread safety problem

Common thread safety classes

  • String
  • Integer
  • StringBuffffer
  • Random
  • Vector
  • Hashtable
  • java. util. Classes under concurrent package

Here, they are thread safe, which means that when multiple threads call a method of their same instance, they are thread safe. It can also be understood as

Hashtable table = new Hashtable();
new Thread(()->{
	table.put("key", "value1");
}).start();

new Thread(()->{
	table.put("key", "value2");
}).start();
  • Each of their methods is atomic (because synchronized is directly added to the method)
  • But note that the combination of their multiple methods is not atomic. See the analysis later

You can see from the source code that the put method of Hashtable is added with synchronized

Combination of thread safe class methods

Analyze whether the following code is thread safe?

Hashtable table = new Hashtable();
// Thread 1, thread 2
if( table.get("key") == null) {
	table.put("key", value);
}

Immutable class thread safety

String and Integer are immutable classes. Because their internal state cannot be changed, their methods are thread safe

Some students may have questions. String has methods such as replace and substring to change the value. How do these methods ensure thread safety?

substring source code of String

  • Directly new a new String object to ensure the immutability of the original object

Case analysis

Code example 1

public class MyServlet extends HttpServlet {

  // Is it safe? no 	 HashTable is thread safe
	Map<String,Object> map = new HashMap<>();
	// Is it safe? yes
	String S1 = "...";
	// Is it safe? yes
	final String S2 = "...";
	// Is it safe? no
	Date D1 = new Date();
	// Is it safe? no
  // The reference of D2 will not change, but the attributes in the date object can change
	final Date D2 = new Date();

	public void doGet(HttpServletRequest request,	HttpServletResponse response) {
  // Use the above variables
 	}

}

Code example 2

public class MyServlet extends HttpServlet {

	// Is it safe? no
  private UserService userService = new UserServiceImpl();
	
  public void doGet(HttpServletRequest request, HttpServletResponse response) {
		userService.update(...);
  }

}

public class UserServiceImpl implements UserService {
	// Record the number of calls
	private int count = 0; //shared resource

  public void update() {
		// Critical zone
		count++;
 	}

}
  • userService is a member variable and a shared resource. Thread safety problems will occur when calling multiple threads.

Code example 3

@Aspect
@Component
public class MyAspect {
	// Is it safe? no
	private long start = 0L;
	
  @Before("execution(* *(..))")
	public void before() {
		start = System.nanoTime();
	}

  @After("execution(* *(..))")
	public void after() {
		long end = System.nanoTime();
		System.out.println("cost time:" + (end-start));
 	}

}
  • All objects in spring are singleton without any additional description. It indicates that the object of this class will be shared, and the member variables of the object will also be shared.
  • The best solution is to change the pre and post notifications into surround notifications, and make the member variables into local variables.

Code example 4

public class MyServlet extends HttpServlet {

  // Is it safe yes
	private UserService userService = new UserServiceImpl();
	public void doGet(HttpServletRequest request, HttpServletResponse response) {
		userService.update(...);
  }

}

public class UserServiceImpl implements UserService {
  // Is it safe yes
  private UserDao userDao = new UserDaoImpl();

  public void update() {
    userDao.update();
  }
}

public class UserDaoImpl implements UserDao { 

  public void update() {
    String sql = "update user set password = ? where username = ?";
    // Is it safe yes
    try (Connection conn = DriverManager.getConnection("","","")){
      // ...
    } catch (Exception e) {
      // ...
    }
  }

}
  • UserDaoImpl is thread safe and has no member variables. The resource variable Connection also appears as a local variable in the method
  • UserServiceImpl is thread safe, the member variable UserDao is thread safe, and the internal state will not change.
  • UserService is thread safe and does not change its internal state.

Code example 5

public class MyServlet extends HttpServlet {
  // Is it safe
  private UserService userService = new UserServiceImpl();
  
  public void doGet(HttpServletRequest request, HttpServletResponse response) {
    userService.update(...);
  }
}

public class UserServiceImpl implements UserService {
  // Is it safe
  private UserDao userDao = new UserDaoImpl();

  public void update() {
    userDao.update();
  }
}

public class UserDaoImpl implements UserDao {
  // Is it safe no
  private Connection conn = null;

  public void update() throws SQLException {
    String sql = "update user set password = ? where username = ?";
    conn = DriverManager.getConnection("","","");
    // ...
    conn.close();
  }
}
  • UserDaoImpl is thread unsafe because it is a member variable. In multithreading, one thread may be closed by the other as soon as it starts Connection.
  • UserServiceImpl and UserService have also become thread unsafe.

Code example 6

public class MyServlet extends HttpServlet {
  // Is it safe
  private UserService userService = new UserServiceImpl();
  public void doGet(HttpServletRequest request, HttpServletResponse response) {
    userService.update(...);
  }
}

public class UserServiceImpl implements UserService { 
  public void update() {
    UserDao userDao = new UserDaoImpl(); //local variable
    userDao.update();
  }
}

public class UserDaoImpl implements UserDao {
  // Is it safe
  private Connection = null;
  public void update() throws SQLException {
    String sql = "update user set password = ? where username = ?";
    conn = DriverManager.getConnection("","","");
    // ...
    conn.close();
  }
}
  • Because userDao is newly created every time in UserServiceImpl and is used as a local variable. If a new is created every time, the member variable Connection in UserDaoImpl is also new. There will be no thread safety issues.
  • However, it is not recommended to write in this way. It is recommended to turn Connection into a local variable, which is the most correct way

Code example 7

public abstract class Test {

  public void bar() {
    // no is a local variable, but it may be exposed to other threads as a parameter in foo()
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    foo(sdf); 
  }

  public abstract foo(SimpleDateFormat sdf);

  public static void main(String[] args) {
    new Test().bar();
  }

}
  • foo's behavior is uncertain and may lead to unsafe occurrence, which is called extraterrestrial method
public void foo(SimpleDateFormat sdf) {
  String dateStr = "1999-10-11 00:00:00";
  for (int i = 0; i < 20; i++) {
    new Thread(() -> {
      try {
        sdf.parse(dateStr);
      } catch (ParseException e) {
        e.printStackTrace();
      }
    }).start();
  }
}
  • For those that do not want to be exposed for external use, it is recommended to add final or private, so as to enhance the security of subclasses

Insert an interview question

Why should the String class be set to final?

A: because it is not set to final, it may be inherited by subclasses and destroy the behavior of one of the methods

exercises

Ticket selling exercise

Test the following code for thread safety problems and try to correct them

@Slf4j(topic = "c.ExerciseSell")
public class ExerciseSell {

    public static void main(String[] args) throws InterruptedException {
        // Simulate multi person ticket buying
        TicketWindow window = new TicketWindow(1000);

        // Collection of all threads
        List<Thread> threadList = new ArrayList<>();
        // According to the statistics of votes sold, Vector is thread safe
        List<Integer> amountList = new Vector<>();
        for (int i = 0; i < 2000; i++) {
            Thread thread = new Thread(() -> {
                // Buy a ticket
                int amount = window.sell(random(5));
                // Count the number of tickets bought
                amountList.add(amount);
            });
						//Although ArrayList is thread unsafe, this threadList is only used by the main thread
						//It will not be shared by multiple threads, so there is no need to consider thread safety
            threadList.add(thread); 
            thread.start();
        }

				// Wait for all threads to complete execution
        for (Thread thread : threadList) {
            thread.join();
        }

        // Count the number of votes sold and the number of remaining votes
        log.debug("Remaining tickets:{}",window.getCount());
        log.debug("Number of votes sold:{}", amountList.stream().mapToInt(i-> i).sum());
    }

    // Random is thread safe
    static Random random = new Random();

    // Random 1 ~ 5
    public static int random(int amount) {
        return random.nextInt(amount) + 1;
    }
}

// ticket window
class TicketWindow {
		// Number of remaining tickets
    private int count;

    public TicketWindow(int count) {
        this.count = count;
    }

    // Number of remaining tickets obtained
    public int getCount() {
        return count;
    }

    // sell ticket
    public int sell(int amount) {
        if (this.count >= amount) {
            this.count -= amount;
            return amount;
        } else {
            return 0;
        }
    }
}

  • The window is a shared variable, which will be accessed by all threads to buy tickets
// Simulate multi person ticket buying
TicketWindow window = new TicketWindow(1000);
  • In the sell operation, the read-write operation involving the remaining ticket variables belongs to the critical area

  • The number of remaining votes count in TicketWindow is a shared variable

  • When each thread buys a ticket, it reads the count first and then modifies the count. In the case of multiple threads, instruction interleaving is easy to occur and the thread is unsafe

resolvent

  • Add synchronized to the sell method to lock the instance object of TicketWindow, that is, lock the count, which protects the count and ensures thread safety
// sell ticket
public synchronized int sell(int amount) {
    if (this.count >= amount) {
        this.count -= amount;
        return amount;
    } else {
        return 0;
    }
}

In addition, can you use the following code? Why?

List<Integer> amountList = new ArrayList<>();
  • No, in the original example, it is new Vector, which is thread safe. If you replace ArrayList, the addition of tickets will also become thread unsafe.

The two methods are thread safe respectively. Is it thread safe when combined?

// Buy a ticket
int amount = window.sell(random(5));
// Count the number of tickets bought
amountList.add(amount);
  • In fact, it is still thread safe, because although the two methods are combined, they are not synchronized on the combined methods, and instruction interleaving may occur under multithreading. But even if it happens, it doesn't affect, because the two methods don't operate on the same shared variable. Is thread safe.

Transfer exercise

Test the following code for thread safety problems and try to correct them

@Slf4j(topic = "c.ExerciseTransfer")
public class ExerciseTransfer {

    public static void main(String[] args) throws InterruptedException {
        Account a = new Account(1000);
        Account b = new Account(1000);

        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                a.transfer(b, randomAmount()); //Simulate a to b transfer
            }
        }, "t1");

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                b.transfer(a, randomAmount()); //Simulate transfer from b to a
            }
        }, "t2");

        t1.start();
        t2.start();
        t1.join();
        t2.join();

        // View the total amount after 2000 transfers
        log.debug("total:{}", (a.getMoney() + b.getMoney()));
    }

    // Random is thread safe
    static Random random = new Random();

    // Random 1 ~ 100
    public static int randomAmount() {
        return random.nextInt(100) + 1;
    }
}

// account
class Account {

    private int money; //Member variable

    public Account(int money) {
        this.money = money;
    }

    public int getMoney() {
        return money;
    }

    public void setMoney(int money) {
        this.money = money;
    }

    // transfer accounts
    public void transfer(Account target, int amount) {
        if (this.money >= amount) {
            this.setMoney(this.getMoney() - amount);
            target.setMoney(target.getMoney() + amount);
        }
    }

}

  • There are two account objects. Each account object has its own balance variable money as the shared variable.
Account a = new Account(1000);
Account b = new Account(1000);

  • There is a transfer method in the account, which involves reading and writing the shared variable money, which belongs to the critical area

  • Then open 1000 threads to let a transfer money to b, open 1000 threads to let b transfer money to a, and call the transfer method.
  • That is, the method transfer, which reads and writes the shared variable money, is exposed to a multithreaded environment.
Thread t1 = new Thread(() -> {
    for (int i = 0; i < 1000; i++) {
        a.transfer(b, randomAmount());
    }
}, "t1");

Thread t2 = new Thread(() -> {
    for (int i = 0; i < 1000; i++) {
        b.transfer(a, randomAmount());
    }
}, "t2");
  • Under multithreading, instruction interleaving occurs and thread safety problems occur, resulting in a total amount of 2054 > 2000
11:05:15.743 c.ExerciseTransfer [main] - total:2054

Process finished with exit code 0

resolvent

Lock the transfer() method

  • Directly add synchronized to the transfer() method? Unfortunately, this will not solve the problem.
// transfer accounts
public synchronized void transfer(Account target, int amount) {
        if (this.money >= amount) {
            this.setMoney(this.getMoney() - amount);
            target.setMoney(target.getMoney() + amount);
        }
}
  • As a result, thread safety problems still occur
11:15:35.814 c.ExerciseTransfer [main] - total:423

Process finished with exit code 0
  • This is because the transfer() method does not just operate on its own shared variable this Money also operates the shared variable target of the transfer object money.
  • And synchronize is directly added to the member method, which is equivalent to synchronized(this). For its own member variable this Lock money.
  • Therefore, both shared variables should be locked. You can use synchronized(Account.class) to lock the Account class. Then all instance objects of the Account class will be locked, that is, the money that locks the two objects is realized
		// transfer accounts
    public void transfer(Account target, int amount) {
        synchronized (Account.class) {
            if (this.money >= amount) {
                this.setMoney(this.getMoney() - amount);
                target.setMoney(target.getMoney() + amount);
            }
        }
    }
  • The test results are correct for many times
11:24:38.288 c.ExerciseTransfer [main] - total:2000

Process finished with exit code 0

Keywords: Java Multithreading JUC synchronized

Added by morphboy23 on Tue, 01 Feb 2022 10:02:58 +0200