What is thread safety?

Thread safety is a concept in computer program code when multithreaded programming. In a program where multiple threads with shared data execute in parallel, thread-safe code ensures that each thread can execute normally and correctly through a synchronization mechanism, without unexpected conditions such as data contamination. Multiple threads running the same piece of code at the same time are thread-safe if the result of each run is the same as that of a single thread, and the result is the same as expected; otherwise, it is not safe.

Second, solve the “thread unsafe” idea

Here’s an example of a “thread unsafe” trigger:

Scenario: Ticket grabbing (many individuals grabbing tickets at the same time, so multi-threaded, concurrent requests)

Example: Suppose in the ticket grabbing scenario, we only have 10 train tickets in total. At the last moment, we have sold 9 tickets and only the last one is left. At this time, the system sends multiple concurrent requests, and these concurrent requests read that the remaining number of train tickets is 1 at the same time, and then pass the judgment of the remaining number, and finally lead to oversend. In other words, originally we only sold 10 train tickets and only generated 10 orders at most, but because of thread insecurity, the concurrent requests of users resulted in more than 10 orders of users who successfully booked the tickets. This is called thread insecurity.

Thread unsafe code:

public class TicketRunnable implements Runnable {

	// The number of votes remaining
	static int count=10;
	// How many tickets do you get
	static int num=0;
	// Whether the tickets are sold out
	boolean flag=false;
	
	@Override
	public void run(a) {
		// TODO Auto-generated method stub
		// If the tickets are not sold out, continue to grab them
		while (!flag) {
			sale();
		}
	}

	/**
	 * 售票
	 */
	private void sale(a) {
			if(count<=0){
				flag=true;
				return;
			}
			// The remaining votes shall be reduced by one
			count--;
			// If you get the first ticket, you will get one
			num++;
			System.out.println(Thread.currentThread().getName()+"Grab the number one."+num+"One ticket, the rest."+count+"A ticket."); }}Copy the code

2. Test entry code:

public class Test {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		/** * Create thread */
		TicketRunnable ticketRunnable=new TicketRunnable();
		// The first person
		Thread t1=new Thread(ticketRunnable,"Zhang");
		// The second person
		Thread t2=new Thread(ticketRunnable,"Bill");
		// The third person
		Thread t3=new Thread(ticketRunnable,"Fifty");
		// The fourth person
		Thread t4=new Thread(ticketRunnable,"Daisy");
		// The fifth person
		Thread t5=new Thread(ticketRunnable,"Cropland 7");
		/** * Start thread */t1.start(); t2.start(); t3.start(); t4.start(); t5.start(); }}Copy the code

3. Operation result:

 

The thread is not safe. The thread is not safe. The thread is not safe.

Method 1: Use the synchronized keyword to pessimistic the locking idea

Pessimistic Lock. When you query the data, you assume that someone else will change it. So each time you query the data, the Lock will be brought up. This locking mechanism is used in traditional relational databases, such as select…. For update Locks data.

Synchronized is a form of synchronization that modifies instance methods, static methods, and blocks of code. The synchronized keyword means that whenever A thread (such as thread A) runs into this method, it checks to see if any other thread B (or C, D, etc.) is using this method (or any other synchronized method of the class). If so, run thread A until thread B (or C or D), which is using synchronized methods, finishes running this method. If not, lock the caller and run directly. The specific use method is shown in the following table:

Add “synchronized” to the sale() method as follows:

    /** * ticketing * use synchronized method */
	private synchronized void sale(a) {
			if(count<=0){
				flag=true;
				return;
			}
			// The remaining votes shall be reduced by one
			count--;
			// If you get the first ticket, you will get one
			num++;
			System.out.println(Thread.currentThread().getName()+"Grab the number one."+num+"One ticket, the rest."+count+"A ticket.");
	}
Copy the code

Use synchronized code block, effect is same as synchronized method, thread code change to:

public class TicketRunnable implements Runnable {

	// The number of votes remaining
	static int count=10;
	// How many tickets do you get
	static int num=0;
	// Whether the tickets are sold out
	boolean flag=false;
	
	@Override
	public void run(a) {
		// TODO Auto-generated method stub
		// If the tickets are not sold out, continue to grab them
		while (true) {
			// Use synchronized to synchronize code blocks
			synchronized (this) {
				if(count<=0) {break;
				}
				// The remaining votes shall be reduced by one
				count--;
				// If you get the first ticket, you will get one
				num++;
				System.out.println(Thread.currentThread().getName()+"Grab the number one."+num+"One ticket, the rest."+count+"A ticket."); }}}}Copy the code

3. Let’s see the final result:

 

The thread is safe, but it is not fair. Some users can’t get a ticket at all, while others can get several tickets.

Conclusion: Pessimistic locking doesn’t make sense in “high concurrency” snapping scenarios, where users make concurrent requests, each request has to wait for the “lock,” and some thread may never get a chance to grab the “lock,” and the request dies. At the same time, the number of such requests is so high that the average response time of the system increases dramatically, resulting in an avalanche state in which the number of available connection processes is exhausted.

Method 2: Lock Lock mechanism

We encounter the following problems when using the synchronized keyword:

  1. Uncontrollability, can not do at will lock and release the lock.
  2. For example, we can read two files simultaneously, but if we use synchronized to synchronize the objects we read, the other threads would have to wait for one thread to enter.
  3. There is no way to know if the thread acquired the lock.

Lock can solve all of these problems with synchronized, and since jdk1.5, various types of locks (such as read and write locks) have been provided. However, it is important to note that you do not need to manually release a Lock when using the synchronized keyword, but you must manually release a Lock when using the Lock keyword.

1, we modify the thread code:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class TicketRunnable implements Runnable {

	// The number of votes remaining
	static int count = 10;
	// How many tickets do you get
	static int num = 0;
	// Whether the tickets are sold out
	boolean flag = false;
	// Define the lock object
	final Lock lock = new ReentrantLock();

	@Override
	public void run(a) {
		// TODO Auto-generated method stub
		// If the tickets are not sold out, continue to grab them
		while (!flag) {
			sale();
		}
	}

	/**
	 * 售票
	 */
	private void sale(a) {
		/ / lock
		lock.lock();
		try {
			// The code that needs to be thread-safe is put in the try{}
			if (count <= 0) {
				flag = true;
				return;
			}
			// The remaining votes shall be reduced by one
			count--;
			// If you get the first ticket, you will get one
			num++;
			System.out.println(Thread.currentThread().getName() + "Grab the number one." + num + "One ticket, the rest." + count + "A ticket.");
		} finally {
			/ / the liberation of the locklock.unlock(); }}}Copy the code

2. Operation result:

Conclusion: As a result, lock.lock() is applied to the current thread. When the thread is finished, it calls lock.unlock() to release the lock. At this time, other threads can acquire the lock. Again, as with pessimistic locks, there is no fairness.

Method 3: MySQL transactions

This method is valid when you are working on a database. If you are using MySQL, the transaction will lock the row and other transactions will have to wait until the transaction is committed.

Method 4: FIFO queue idea

We put the user’s requests directly into the queue, using FIFO mode (First Input First Output), so that we don’t cause some requests to never get the lock. First-in, first-out (FIFO) is defined as the first element inserted into the queue is also the first element out of the queue, similar to queuing function, to some extent, this queue also embodies a kind of fairness.

BlockingQueue: A BlockingQueue based on memory. As you can see from the word BlockingQueue, access to a BlockingQueue can be blocked in one of two ways:

  • Enqueued when the queue is full
  • The outbound operation is performed when the queue is empty

Therefore, when a thread attempts to enqueue a queue that is already full, it will be blocked unless another thread dequeues. Similarly, when a thread attempts to dequeue an empty queue, it will be blocked unless another thread enqueues.

BlockingQueue’s core method:

  • Insert method:

    • Add (E E) : True is returned on success, and IllegalStateException is thrown on failure
    • Offer (E E) : Returns true on success or false if this queue is full.
    • Put (E E) : Inserts elements into the end of this queue, blocking if the queue is full
  • Delete method:

    • Remove (Object O) : removes the specified element, returning true on success or false on failure
    • Poll () : retrieves and removes the header element of this queue, or returns null if the queue is empty
    • Take () : fetch and remove this queue header element, blocking without it.
  • Check the method

    • Element () : gets but does not remove the header element of this queue, throwing an exception if no element is present
    • Peek () : gets but does not remove the head of this queue; If the queue is empty, null is returned.

1. Modify the entry code:

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

public class Test {
	
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		/** * Create thread object */
		TicketRunnable ticketRunnable = new TicketRunnable();
		// The first person
		Thread t1 = new Thread(ticketRunnable, "Zhang");
		// The second person
		Thread t2 = new Thread(ticketRunnable, "Bill");
		// The third person
		Thread t3 = new Thread(ticketRunnable, "Fifty");
		// The fourth person
		Thread t4 = new Thread(ticketRunnable, "Daisy");
		// The fifth person
		Thread t5 = new Thread(ticketRunnable, "Cropland 7");

		try {
			// Memory based blocking queue
			BlockingQueue<Thread> queue = new LinkedBlockingQueue<Thread>();
			// To join the queue,put(E E) inserts the element into the end of the queue, and blocks if the queue is full
			queue.put(t1);
			queue.put(t2);
			queue.put(t3);
			queue.put(t4);
			queue.put(t5);
			// Execute queue
			while(! queue.isEmpty()) {//take() fetch and remove the queue header element, blocking without any element.queue.take().start(); }}catch (InterruptedException e) {
			// TODO Auto-generated catch blocke.printStackTrace(); }}}Copy the code

However, queues also have a downside. In high concurrency scenarios, it is possible to “burst” the queue memory in an instant due to the number of requests, and then “avalanche” the system again. , you might think, as long as I design a great memory queue is not ok, it may be a method, but the speed of the system processes the request within a queue cannot be compared with crazy into the queue number, that is, within the queue request will accumulate more more, eventually the Web system average response time will have fallen sharply, The system is still in an avalanche state.

Method five: optimistic locking

Optimistic Lock: Every time I query the data, I think that others will not modify it, so I will not Lock it. However, when updating the data, I will judge whether others have updated the data during this period, using the version number, time stamp and other mechanisms. Optimistic locking is applicable to multiple read applications, which improves throughput.

Optimistic locking can be implemented in two ways:

  1. Version number mechanism
  2. CAS algorithm

Version number mechanism: All requests are eligible to modify this data, but will receive a version number of the data. Only those that match the version number will be updated successfully, others will fail. This way, we don’t need to worry about queues, but it will increase the CPU computing overhead, but overall, it is a good solution.

For example, here is a table with a version attribute in the field

id num version
1 99 1

And then we have two threads, let’s say A and B, and we take the record with id=1. First, thread A initiates A query request with the following statement:

Select num,version from table_name where id='1';Copy the code

Select (num=99, version=1); select (num=99, version=1); select (num=99, version=1);

Next, thread A performs the update operation with the following update statement:

Update 表名 set num =${num} +1,version=${version}+1 where id='1' and version='1'Copy the code

Thread A is using A row-level lock, and the version number is 2. Thread B initiates an update, and the version number is 2.

Update 表名 set num =${num} +1,version=${version}+1 where id='1' and version='1'Copy the code

Eventually thread B returns the result that the execution failed.

CAS algorithm: Compare and swap is a well-known lock-free algorithm, also known as non-blocking Synchronization. CAS is an optimistic locking technique. When multiple threads try to update the same variable at the same time using CAS, only one of them can update the value of the variable while the other threads fail. The failed thread is not suspended, but is told that it failed in the race and can try again. The CAS algorithm involves three operands:

  • Memory value V to be read and written
  • The value A for the comparison
  • The new value B to write

CAS atomically updates the value of V with the new value B if and only if the value of V is equal to A, otherwise nothing is done (compare and replace is an atomic operation). In general, it’s a spin operation, that is, repeated retries. CAS operations are atomic, so multiple threads can use CAS concurrently to update data without using locks. The JDK makes heavy use of CAS to update data and to prevent locking (synchronized heavyweight locks) to keep atomic updates.

Create a CAS algorithm for spinlock.java:

Spin-locking is when the thread repeatedly checks whether the lock variable is available until it succeeds. Because the thread keeps executing during this process, it is a busy wait, and once it has acquired the spin lock, it holds it until it is explicitly released.


import java.util.concurrent.atomic.AtomicReference;

/** * CAS algorithm: spin lock *@author Administrator
 *
 */
public class SpinLock {
    /** * AtomicReference is an atomic operation on an "object". The AtomicReference is used to specify the nature of atomic variables, which ensures that you can make thread-safe changes to an object reference. * The Atomic family is designed to ensure atomicity in multi-threaded environments and is lighter than synchronized. * /
	private AtomicReference<Thread> atomicReference = new AtomicReference<>();

    public void lock(a) {
        // Get the current thread
        Thread current = Thread.currentThread();
        // Loop until the atomicReference is not empty
        while(! atomicReference.compareAndSet(null, current)) {}
    }

    public void unlock(a) {
        // Get the current thread
        Thread current = Thread.currentThread();
        // Set atomicReference to null
        atomicReference.compareAndSet(current, null); }}Copy the code

An AtomicReference is an atomic operation on an “object” that ensures thread-safe changes to an object reference. The principle is as follows:

2, modify thread code:

public class TicketRunnable implements Runnable {

	// The number of votes remaining
	static int count = 10;
	// How many tickets do you get
	static int num = 0;
	// Whether the tickets are sold out
	boolean flag = false;
	/ / the spin lock
	private SpinLock spinLock;
	
	// The constructor
	public TicketRunnable(a) {}public TicketRunnable(SpinLock lock) {
        this.spinLock = lock;
    }
    
	@Override
	public void run(a) {
		// TODO Auto-generated method stub
		// If the tickets are not sold out, continue to grab them
		while (!flag) {
			sale();
		}
	}

	/**
	 * 售票
	 */
	private void sale(a) {	
		try {
			/ / lock
			spinLock.lock();
			if (count <= 0) {
				flag = true;
				return;
			}
			// The remaining votes shall be reduced by one
			count--;
			// If you get the first ticket, you will get one
			num++;
			System.out.println(Thread.currentThread().getName() + "Grab the number one." + num + "One ticket, the rest." + count + "A ticket.");
		}finally {
			/ / releases the lockspinLock.unlock(); }}}Copy the code

3. Modify the entry code:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Test {
	
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		/** * Create thread object */
		SpinLock lock = new SpinLock();
		TicketRunnable ticketRunnable = new TicketRunnable(lock);
		// The first person
		Thread t1 = new Thread(ticketRunnable, "Zhang");
		// The second person
		Thread t2 = new Thread(ticketRunnable, "Bill");
		// The third person
		Thread t3 = new Thread(ticketRunnable, "Fifty");
		// The fourth person
		Thread t4 = new Thread(ticketRunnable, "Daisy");
		// The fifth person
		Thread t5 = new Thread(ticketRunnable, "Cropland 7");
		// Start the thread
		t1.start();
		t2.start();
		t3.start();
		t4.start();
		t5.start();
		/ * * * / / create a fixed reusable thread pool ExecutorService ExecutorService = Executors. NewFixedThreadPool (5); For (int I = 0; int I = 0; i < 5; i++) { executorService.execute(new TicketRunnable(lock)); } executorService.shutdown(); * /}}Copy the code

4. Operation result:

Disadvantages of CAS:

  • High CPU overhead In the case of high concurrency, if many threads repeatedly try to update a variable, but the update is not successful, the cycle will bring great pressure to the CPU.
  • The CAS mechanism guarantees only atomicity of one variable, not atomicity of the entire code block. For example, Synchronized is required to ensure atomicity of all three variables.

CAS can cause ABA problems:

What is the ABA problem? Let’s say we have A shared variable whose value is A, and thread 1 changes it. While thread 1 is changing it, thread 2 has changed it to B, and thread 3 has changed it to A. When thread 1 is finished with the change, and the result is equal to the actual value of shared memory, thread 1 thinks that the variable has not been changed, and thread 1 commits successfully, but in fact the variable has been changed. In this case, A is not that A, which is the ABA problem. The most common is the financial problem, which is if someone misappropriates your money, and they pay it back before you know it, and in the process, what you don’t know is that they’ve done something illegal with your money, and they’ve broken the law.

  • In a normal scenario, ABA problems don’t matter, you put money in the bank, but the money you put in the bank might be used by the bank to do other things, and eventually your money comes back, we don’t care about the process, we care about the outcome.
  • But in certain situations, ABA problems can have a big impact, such as the example below:

Solutions to ABA problems:

Each time you modify a shared variable commit, not only do you commit a new value, but you should also add a version number or a timestamp to the variable. Before each commit, check whether the old expected value is the same as the actual shared memory value. If the old expected value is the same as the actual shared memory value, then compare the version number or timestamp. If both are the same, then commit the modification.

Java can use the AtomicStampedReference function to solve ABA problems. The basic usage of an AtomicStampedReference is as follows:

// Constructor, passing in references and stamps
public AtomicStampedReference(V initialRef, int initialStamp)
// Return the reference
public V getReference(a)
// Return the version stamp
public int getStamp(a)
// If the current reference equals the expected value and the current version stamp equals the expected version stamp, the new reference and the new version stamp are updated to memory
public boolean compareAndSet(V   expectedReference,
                                 V   newReference,
                                 int expectedStamp,
                                 int newStamp)
// If the current reference equals the expected reference, the updated version is stamped into memory
public boolean attemptStamp(V expectedReference, int newStamp)
// Set the new reference and version stamp for the current reference
public void set(V newReference, int newStamp) 
Copy the code

AtomicStampedReference’s compareAndSet() method source code parsing:

/** * If the current reference equals the expected value and the current version stamp equals the expected version stamp, the new reference and the new version stamp are updated to memory. * expectedReference: expect reference (the original value before the update) * newReference: newReference (the new value to be updated) * expectedStamp: expected version number * newStamp: new version number */  
public boolean compareAndSet(V   expectedReference,
                             V   newReference,
                             int expectedStamp,
                             int newStamp) {
    // Get the current element and version number
    Pair<V> current = pair;
    return
        expectedReference == current.reference && // If the reference is not changed, the element's value is not changed
        expectedStamp == current.stamp && // The version number of the element has not changed
        ((newReference == current.reference &&
          newStamp == current.stamp) || // If the new element and the new version number are equal to the old element, no update is required
         casPair(current, Pair.of(newReference, newStamp))); / / update the CAS
}

private boolean casPair(Pair<V> cmp, Pair<V> val) {
    return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
}
Copy the code

 

Originally intended to use AtomicStampedReference to transform the ticket example code, but did not study out, capable big guy can improve!