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:
- 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
- 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