Getting started with thread safety and locks

Keywords: Java Spring intellij-idea

Thread safety and lock use

When we use multiple threads to access the same resource (which can be the same variable, the same file, the same record, etc.), if multiple threads only have read operations, thread safety problems will not occur, but if multiple threads have read and write operations on resources, thread safety problems are easy to occur.

We demonstrate thread safety through a case:
The cinema sells tickets. We simulate the ticket selling process of the cinema. Assuming that the movie to be played is "huluwa vs. Altman", there are 100 seats in this movie
(the film can only sell 100 tickets).
Let's simulate the ticket window of the cinema and realize that multiple windows sell tickets for the film "huluwa vs. Altman" at the same time (multiple windows sell these 100 tickets together)

Same resource problem

1. Local variables cannot be shared

package com.atguigu.safe;

public class SaleTicketDemo1 {
	public static void main(String[] args) {
		Window w1 = new Window();
		Window w2 = new Window();
		Window w3 = new Window();
		
		w1.start();
		w2.start();
		w3.start();
	}
}
class Window extends Thread{
	public void run(){
		int total = 100;
		while(total>0) {
			System.out.println(getName() + "Sell one ticket and the rest:" + --total);
		}
	}
}

Results: 300 tickets were found to have been sold.

Problem: local variables are independent every time the method is called, so the total of run() of each thread is independent, not shared data.

2. Instance variables of different objects are not shared

package com.atguigu.safe;

public class SaleTicketDemo2 {
	public static void main(String[] args) {
		TicketSaleThread t1 = new TicketSaleThread();
		TicketSaleThread t2 = new TicketSaleThread();
		TicketSaleThread t3 = new TicketSaleThread();
		
		t1.start();
		t2.start();
		t3.start();
	}
}
class TicketSaleThread extends Thread{
	private int total = 10;
	public void run(){
		while(total>0) {
			System.out.println(getName() + "Sell one ticket and the rest:" + --total);
		}
	}
}

Results: 300 tickets were found to have been sold.

Problem: the instance variables of different instance objects are independent.

3. Static variables are shared

package com.atguigu.safe;

public class SaleTicketDemo3 {
	public static void main(String[] args) {
		TicketThread t1 = new TicketThread();
		TicketThread t2 = new TicketThread();
		TicketThread t3 = new TicketThread();
		
		t1.start();
		t2.start();
		t3.start();
	}
}
class TicketThread extends Thread{
	private static int total = 10;
	public void run(){
		while(total>0) {
			try {
				Thread.sleep(10);//Add this to make the problem more obvious
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			System.out.println(getName() + "Sell one ticket and the rest:" + --total);
		}
	}
}

Results: it was found that nearly 100 tickets were sold.

Question (1): but there are duplicate votes or negative votes.

Cause: thread safety issue

Question (2): if you want to consider two films, sell 100 tickets each, etc

Reason: the static variable of the TicketThread class is shared by all objects of the TicketThread class

Wait, how to solve it?

Try to solve thread safety problems

To solve the security problem of multi-threaded concurrent access to a resource, that is, to solve the problem of duplicate tickets and non-existent tickets, Java provides a synchronization mechanism
(synchronized).

According to the case description:

When window 1 thread enters the operation, window 2 and window 3 threads can only wait outside. When window 1 operation ends, window 1, window 2 and window 3 have the opportunity to enter the code for execution. In other words, when a thread modifies a shared resource, other threads cannot modify the resource. After the modification is completed and synchronized, they can grab CPU resources and complete the corresponding operation, which ensures the synchronization of data and solves the phenomenon of thread insecurity.

In order to ensure that each thread can normally perform atomic operations, Java introduces thread synchronization mechanism. Note: at most one thread is allowed to have a synchronization lock at any time. Whoever gets the lock will enter the code block, and other threads can only wait outside (BLOCKED).

Synchronization method: the synchronized keyword directly modifies the method, indicating that only one thread can enter the method at the same time, and other threads are waiting outside.

public synchronized void method(){
    Code that may cause thread safety problems
}

Synchronized code block: the synchronized keyword can be used in front of a block, indicating that mutually exclusive access is only implemented to the resources of the block.
Format:

synchronized(Synchronous lock){
     Code that requires synchronization
}

Lock object selection

Synchronization lock object:

**-The lock object can be of any type.

  • Multiple thread objects should use the same lock**

1. Lock object problem of synchronization method

(1) Static method: the Class object of the current Class

(2) Non static method: this

Example code 1:
public class SaleTicketSafeDemo1 {
	public static void main(String[] args) {
		// 2. Create resource object
		//Note that only one Ticket object is created here, so you can use the non static method: this instead of the static method: the Class object of the current Class
		Ticket ticket = new Ticket();
	// 3. Start multiple threads to operate on the object of the resource class
	//Inherit Thread class
	//*Anonymous inner class objects are used to create and start threads*
	Thread t1 = new Thread("Window one") {
		public void run() {
			while (true) {
				try {
					Thread.sleep(10);// Add this to make the problem more obvious
					ticket.sale();
				} catch (Exception e) {
					e.printStackTrace();
					break;
				}
			}
		}
	};
	Thread t2 = new Thread("Window II") {
		public void run() {
			while (true) {
				try {
					Thread.sleep(10);// Add this to make the problem more obvious
					ticket.sale();
				} catch (Exception e) {
					e.printStackTrace();
					break;
				}
			}
		}
	};
	//Implement Runnable interface
	//*Anonymous inner class objects are used to create and start threads*
	Thread t3 = new Thread(new Runnable() {
		public void run() {
			while (true) {
				try {
					Thread.sleep(10);// Add this to make the problem more obvious
					ticket.sale();
				} catch (Exception e) {
					e.printStackTrace();
					break;
				}
			}
		}
	}, "Window three");

	t1.start();
	t2.start();
	t3.start();
}

}

// 1. Write resource class
class Ticket {
	private int total = 10;
//The lock object implied by non static methods is this
//Note that only one object is created here, and the Ticket can use the non static method: this
public synchronized void sale() {
	if (total > 0) {
		System.out.println(Thread.currentThread().getName() + "Sell one ticket and the rest:" + --total);
	} else {
		throw new RuntimeException(Thread.currentThread().getName() + "I found no tickets");
	}
}

public int getTotal() {
	return total;
}

}

Example code 2:
public class SaleTicketSafeDemo2 {
	public static void main(String[] args) {
			//Note that only one object, tickelrunnable, is created here, so you can use the non static method: this instead of the static method: the Class object of the current Class
		TicketRunnable tr = new TicketRunnable();
		Thread t1 = new Thread(tr,"Window one");
		Thread t2 = new Thread(tr,"Window II");
		Thread t3 = new Thread(tr,"Window three");
		
		t1.start();
		t2.start();
		t3.start();
	}
}

class TicketRunnable implements Runnable {
	private int ticket = 10;

	@Override
	public void run() {
		while(ticket > 0){
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			sellTicket();
		}
	}

	//The lock object implied by non static methods is this
	//Note that only one object, TicketRunnable, is created here. Use the non static method: this
	public synchronized void sellTicket() {
		if (ticket > 0) {
			System.out.println(Thread.currentThread().getName() + "Selling:" + ticket--);
		}
	}
}
Example code 3:
public class SaleTicketSafeDemo3 {
	public static void main(String[] args) {
	//Note that three objects TicketThread are created here, so the static method is used: the Class object of the current Class and the non static method this is not the unique object ID
		TicketThread t1 = new TicketThread();
		TicketThread t2 = new TicketThread();
		TicketThread t3 = new TicketThread();
		
		t1.start();
		t2.start();
		t3.start();
	}
}
class TicketThread extends Thread {
	private static int ticket = 100;

	@Override
	public void run() {
		while (ticket>0) {
			try {
				Thread.sleep(100);
			} catch(InterruptedException e) {
				e.printStackTrace();
			}
			sellTicket();
		}
	}
	//This must be a static method, because if it is a non static method and the implicit lock object is this, multiple threads are not the same lock object
	//The lock object implied by the static method is the Class object of the current Class
	public synchronized static void sellTicket(){
		if(ticket>0){//There are tickets for sale 
			System.out.println(Thread.currentThread().getName() + "Selling:" + ticket--);
		}
	}

}

2. Lock object for synchronization code block

Synchronization lock object:

  • The lock object can be of any type.
  • Multiple thread objects should use the same lock.
  • It is customary to consider this first, but pay attention to whether it is the same this

Example code 1: this object

public class SaleTicketSafeDemo1 {
	public static void main(String[] args) {
		// 2. Create resource object
		Ticket ticket = new Ticket();

		// 3. Start multiple threads to operate on the object of the resource class
		Thread t1 = new Thread("Window one") {
			public void run() {
				while (true) {
					try {
						Thread.sleep(10);// Add this to make the problem more obvious
						ticket.sale();
					} catch (Exception e) {
						e.printStackTrace();
						break;
					}
				}
			}
		};
		Thread t2 = new Thread("Window II") {
			public void run() {
				while (true) {
					try {
						Thread.sleep(10);// Add this to make the problem more obvious
						ticket.sale();
					} catch (Exception e) {
						e.printStackTrace();
						break;
					}
				}
			}
		};
		Thread t3 = new Thread(new Runnable() {
			public void run() {
				while (true) {
					try {
						Thread.sleep(10);// Add this to make the problem more obvious
						ticket.sale();
					} catch (Exception e) {
						e.printStackTrace();
						break;
					}
				}
			}
		}, "Window three");

		t1.start();
		t2.start();
		t3.start();
	}
}

// 1. Write resource class
class Ticket {
	private int total = 10;

	public void sale() {
		synchronized (this) {
			if (total > 0) {
				System.out.println(Thread.currentThread().getName() + "Sell one ticket and the rest:" + --total);
			} else {
				throw new RuntimeException(Thread.currentThread().getName() + "I found no tickets");
			}
		}
	}

	public int getTotal() {
		return total;
	}
}

Example code 2: this object

public class SaleTicketSafeDemo2 {
	public static void main(String[] args) {
		TicketRunnable tr = new TicketRunnable();
		Thread t1 = new Thread(tr,"Window one");
		Thread t2 = new Thread(tr,"Window II");
		Thread t3 = new Thread(tr,"Window three");
		
		t1.start();
		t2.start();
		t3.start();
	}
}

class TicketRunnable implements Runnable {
	private int ticket = 10;

	@Override
	public void run() {
		while(ticket > 0){
			try {
				Thread.sleep(100);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			synchronized (this) {
				if (ticket > 0) {
					System.out.println(Thread.currentThread().getName() + "Selling:" + ticket--);
				}
			}
		}
	}

}

Example code 3: other objects

public class SaleTicketSafeDemo3 {
	public static void main(String[] args) {
		TicketThread t1 = new TicketThread();
		TicketThread t2 = new TicketThread();
		TicketThread t3 = new TicketThread();
		
		t1.start();
		t2.start();
		t3.start();
	}
}
class TicketThread extends Thread{
	private static int total = 10;
	private static final Object myLock = new Object();
	public void run(){
		while(total>0) {
			try {
				Thread.sleep(10);//Add this to make the problem more obvious
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
//			synchronized (this) {/ / this object cannot be selected as a lock here because this is different for the above three threads
//			synchronized (TicketThread.class) {/ / Yes, because there is only one class object of the TicketThread class in the JVM
//			synchronized ("") {/ / Yes, because there is only one '' string object in the JVM
			synchronized (myLock) {//Yes, because there is only one myLock object in the JVM
				if(total>0){
					System.out.println(getName() + "Sell one ticket and the rest:" + --total);
				}
			}
			
		}
	}
}

Scope of lock

The scope of the lock is too small: it cannot solve the security problem

The scope of lock is too large: once a thread grabs the lock, other threads can only wait. Therefore, if the scope is too large, the efficiency will be reduced and can not be used reasonably CPU resources.

How to write multithreaded programs?

  • principle:

    • Thread operation resource class
    • High cohesion and low coupling
  • Steps:

    • Write resource class
    • Considering thread safety, consider using synchronous code blocks or synchronous methods in resource classes
public class TestSynchronized {
	public static void main(String[] args) {
		// 2. Create resource object
		Ticket ticket = new Ticket();

		// 3. Start multiple threads to operate on the object of the resource class
		Thread t1 = new Thread("Window one") {
			public void run() {
				while (true) {
					try {
						Thread.sleep(10);// Add this to make the problem more obvious
						ticket.sale();
					} catch (Exception e) {
						e.printStackTrace();
						break;
					}
				}
			}
		};
		Thread t2 = new Thread("Window II") {
			public void run() {
				while (true) {
					try {
						Thread.sleep(10);// Add this to make the problem more obvious
						ticket.sale();
					} catch (Exception e) {
						e.printStackTrace();
						break;
					}
				}
			}
		};
		Thread t3 = new Thread(new Runnable() {
			public void run() {
				while (true) {
					try {
						Thread.sleep(10);// Add this to make the problem more obvious
						ticket.sale();
					} catch (Exception e) {
						e.printStackTrace();
						break;
					}
				}
			}
		}, "Window three");

		t1.start();
		t2.start();
		t3.start();
	}
}

// 1. Write resource class
class Ticket {
	private int total = 10;

	public synchronized void sale() {
		if(total<=0){
			throw new RuntimeException(Thread.currentThread().getName() + "I found no tickets");
		}
		System.out.println(Thread.currentThread().getName() + "Sell one ticket and the rest:" + --total);
	}
	public int getTotal() {
		return total;
	}
}

Thread safety in singleton design pattern

1. Starving Han style has no thread safety problem

public class OnlyOneDemo {
	public static void main(String[] args) {
		OnlyOne o1 = OnlyOne.INSTANCE;
		OnlyOne o2 = OnlyOne.INSTANCE;
		
		System.out.println(o1);
		System.out.println(o2);
		System.out.println(o1==o2);
	}
}
class OnlyOne{
	public static final OnlyOne INSTANCE = new OnlyOne();
	private OnlyOne(){
		
	}
}

2. Lazy thread safety issues

Delay object creation

```java
public class SingleTest {
    @Test
    public void test1() {
        Single s1 = Single.getInstance();
        Single s2 = Single.getInstance();
        System.out.println(s1);
        System.out.println(s2);
        System.out.println(s1 == s2);
    }

    Single s1;
    Single s2;

    @Test
    public void test2() throws InterruptedException {
        new Thread(new Runnable() {
            @Override
            public void run() {
                s1 = Single.getInstance();
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                s2 = Single.getInstance();
            }
        }).start();
        Thread.sleep(1000);
        System.out.println(s1 + " : " + s2);
        System.out.println(s1 == s2);
    }
}
```

Wait for wake-up mechanism

What is the waiting wake-up mechanism

This is a collaboration mechanism between multiple threads. When it comes to threads, we often think of the race between threads, such as competing for locks, but this is not the whole story. There will also be a cooperation mechanism between threads.

When a thread meets a certain condition, it enters the wait state (wait()/wait(time)), waits for other threads to wake up after executing their specified code (notify()); Or you can specify the time of wait and wake up automatically when the time comes; When there are multiple threads waiting, you can use notifyAll() to wake up all waiting threads if necessary. wait/notify is a cooperative mechanism between threads.

  1. Wait: the thread is no longer active, no longer participates in scheduling, and enters the wait set. Therefore, CPU resources will not be wasted, nor will it compete for locks. At this time, the thread state is waiting or TIMED_WAITING. It also waits for other threads to perform a special action, that is, when the "notify" or waiting time expires, the threads waiting on this object are released from the wait set and re entered into the ready queue
  2. notify: select a thread in the wait set of the notified object to release;
  3. notifyAll: release all threads on the wait set of the notified object.

be careful:

After the notified thread is awakened, it may not be able to resume execution immediately, because the place where it was interrupted was in the synchronization block, and now it does not hold the lock, so she needs to try to obtain the lock again (it is likely to face competition from other threads). After success, she can resume execution at the place where the wait method was called.

The summary is as follows:

  • If the lock can be obtained, the thread will change from WAITING state to RUNNABLE state;
  • Otherwise, the thread changes from WAITING state to BLOCKED state

Details of calling wait and notify methods

  1. The wait method and notify method must be called by the same lock object. Because: the corresponding lock object can wake up the thread after using the wait method called by the same lock object through notify.
  2. The wait method and notify method are methods belonging to the Object class. Because: the lock Object can be any Object, and the class of any Object inherits the Object class.
  3. The wait method and notify method must be used in the synchronization code block or synchronization function. Because: these two methods must be called through the lock object.

producer consumer problem

Waiting for wake-up mechanism can solve the classic problem of "producer and consumer".

Producer consumer problem, also known as bounded buffer problem, is a classic case of multi-threaded synchronization problem. This problem describes what happens when two (more) threads sharing a fixed size buffer -- the so-called "producer" and "consumer" -- actually run. The main role of the producer is to generate a certain amount of data into the buffer, and then repeat the process. At the same time, consumers consume this data in the buffer. The key to this problem is to ensure that producers will not add data when the buffer is full, and consumers will not consume data when the buffer is empty.

There are actually two problems implied in the problem of producers and consumers:

  • Thread safety problem: because producers and consumers share data buffers, but this problem can be solved using synchronization.
  • Coordination of threads:
    • To solve this problem, the producer thread must wait when the buffer is full and pause to enter the blocking state. When the next consumer consumes the data in the buffer, notify the waiting thread to return to the ready state and restart adding data to the buffer. Similarly, you can also let the consumer thread wait when the buffer is empty, pause and enter the blocking state, wait until the producer adds data to the buffer, and then notify the waiting thread to return to the ready state. Such problems can be solved through such a communication mechanism.

A cook a waiter problem

Case: there is a restaurant with a small food intake, which can only put 10 fast food. The cook puts the fast food on the workbench of the intake, and the waiter takes out the fast food from the workbench to the customer. Now there is a cook and a waiter.

Multiple chefs and multiple waiters

Case: there is a restaurant with a small food intake, which can only put 10 fast food. The cook puts the fast food on the workbench of the intake, and the waiter takes out the fast food from the workbench to the customer. There are now multiple chefs and waiters.

practice

1. Two threads are required to print letters at the same time, and each thread can print three letters continuously. Two threads print alternately, one thread prints the lowercase form of letters, and the other thread prints the uppercase form of letters, but the letters are continuous. When the letter loops to z, it goes back to a.

public class PrintLetterDemo {
	public static void main(String[] args) {
		// 2. Create resource object
		PrintLetter p = new PrintLetter();

		// 3. Create two thread print
		new Thread("Lowercase letters") {
			public void run() {
				while (true) {
					p.printLower();
					try {
						Thread.sleep(1000);// Control rhythm
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
			}
		}.start();

		new Thread("capital") {
			public void run() {
				while (true) {
					p.printUpper();
					try {
						Thread.sleep(1000);// Control rhythm
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
			}
		}.start();
	}
}

// 1. Define resource classes
class PrintLetter {
	private char letter = 'a';

	public synchronized void printLower() {
		for (int i = 1; i <= 3; i++) {
			System.out.println(Thread.currentThread().getName() + "->" + letter);
			letter++;
			if (letter > 'z') {
				letter = 'a';
			}
		}
		this.notify();
		try {
			this.wait();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}

	public synchronized void printUpper() {
		for (int i = 1; i <= 3; i++) {
			System.out.println(Thread.currentThread().getName() + "->" + (char) (letter - 32));
			letter++;
			if (letter > 'z') {
				letter = 'a';
			}
		}
		this.notify();
		try {
			this.wait();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
}

Thread life cycle

Viewpoint 1: 5 states

In short, the life cycle of a thread has five states: New, Runnable, Running, Blocked, and Dead. The CPU needs to switch between multiple threads, so the thread state will switch between Running, blocking and ready many times.

deadlock

Different threads lock the synchronization monitor objects required by the other party and do not release them. When they wait for the other party to give up first, a thread deadlock is formed. In case of deadlock, the whole program will not give any exception or prompt, but all threads are blocked and cannot continue.

public class TestDeadLock {
	public static void main(String[] args) {
		Object g = new Object();
		Object m = new Object();
		Owner s = new Owner(g,m);
		Customer c = new Customer(g,m);
		new Thread(s).start();
		new Thread(c).start();
	}
}
class Owner implements Runnable{
	private Object goods;
	private Object money;

	public Owner(Object goods, Object money) {
		super();
		this.goods = goods;
		this.money = money;
	}

	@Override
	public void run() {
		synchronized (goods) {
			System.out.println("Please pay first.");
			synchronized (money) {
				System.out.println("deliver goods");
			}
		}
	}
}
class Customer implements Runnable{
	private Object goods;
	private Object money;

	public Customer(Object goods, Object money) {
		super();
		this.goods = goods;
		this.money = money;
	}

	@Override
	public void run() {
		synchronized (money) {
			System.out.println("Ship first");
			synchronized (goods) {
				System.out.println("Give me more money");
			}
		}
	}
}

Interview question: the difference between sleep() and wait() methods

(1) sleep() does not release the lock, wait() releases the lock

(2) sleep() specifies the sleep time, and wait() can specify the time or wait indefinitely until notify or notifyAll

(3) sleep() is a static method declared in the Thread class, and the wait method is declared in the Object class

Because we call the wait () method by the lock Object, and the type of the lock Object is any type of Object. Then, you can only declare the methods that you want any type of Object to have in the Object class.

Posted by jayR on Wed, 17 Nov 2021 03:52:01 -0800