Multithreading and Locks in Java

Posted by Billett on Mon, 20 May 2019 01:32:45 +0200

1. Introduction
  1. First introduce the problems in multithreaded programming.Here is an example (multiple threads update counters at the same time):
/*
 * Multiple threads update counters simultaneously (simulate problems with multiple threads)
 */
public class Temp_1 {
	public static void main(String[] args) {
		// 10 consecutive simulations
		for(int i = 0;i < 10;i++) {
			update_counter_demo();
		}
	}
	// Construct worker threads to simulate multiple threads updating counters simultaneously
	public static void update_counter_demo(){
		Counter counter = new Counter(0);
		int worker_thread_num = 10; // 10 worker threads
		Thread[] workers = new Thread[worker_thread_num];
		for(int j = 0;j < worker_thread_num;j++) {			
			workers[j] = new Thread(new Runnable() {
				@Override
				public void run() {
					try {
						Thread.sleep(100); // Wait 100 milliseconds first
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					for(int i = 0;i < 100;i++) { // Execution cumulative 100 times
						counter.plusOne();
					}
				}
			});
		}
		// Start all worker threads
		for(int i = 0;i < worker_thread_num;i++) {
			workers[i].start();
		}
		// Waiting for thread to end
		for(int i = 0;i < worker_thread_num;i++) {
			try {
				workers[i].join();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		// Print counter value
		System.out.println(counter);
	}
	/* Simple Counter */
	private static class Counter {
		private int count;
		public Counter(int count) {
			this.count = count;
		}
		/* Counter plus 1 */
		public void plusOne() {
			count++;
		}
		@Override
		public String toString() {
			return "[ count is " + count + " ]";
		}
	}
}

The results are as follows:

[ count is 838 ]
[ count is 932 ]
[ count is 995 ]
[ count is 969 ]
[ count is 827 ]
[ count is 979 ]
[ count is 1000 ]           // Correct results
[ count is 1000 ]           // Correct results
[ count is 820 ]
[ count is 998 ]

You can find that 10 consecutive simulations happened to work correctly two of them!We know that the error results from not synchronizing multiple threads'operations on shared variables.

  • There is a visibility problem when multiple threads operate on shared variables, and the result of thread A's accumulation of counters is not visible to thread B.For the cumulative operation of the counter, the current operation is based on the result of the previous operation. Each operation result is related to each other. If each operation is based on the result of the previous operation, there will be no error result. This is to execute the same operation serially.
  • The cumulative operation can be simplified to three instructions (although the actual situation may be more complex):
    1. Read operation (read counter values to thread private memory)
    2. Perform an addition 1 operation
    3. Write operation (write private memory values back to main memory)
  • When multiple threads execute the same accumulative operation at the same time (plusOne()), due to thread scheduling, the accumulative operations of multiple threads are interwoven, and it does not cause problems for multiple threads to use CPU time slices in turn (that is, thread scheduling). The problem is that the write operations of these threads overwrite each other and read before the write operation.The fetch operation, which is the result of step 1, is untrusted (in this read-to-write mode, the write operation is based on an unreliable read operation), as shown in the following figure (Thread D reads a count of 1 after each of the three threads has performed a cumulative operation!)., the cumulative operation steps of multiple threads are interpolated, they do not collaborate with each other, communicate/communicate, do their own things, but influence each other!
  • Solve the problem by introducing Mutual Exclusion, which allows only one thread to perform an accumulative operation (i.e., mutually exclusive) at a time, so that the accumulative operation in multiple threads can be executed serially, as shown in the following figure
    1. You might think of atomic operations here.An atomic operation is an indivisible set of operations that either succeed or fail.This is the concept of a transaction.What's going on here is the problem of how multiple threads operate on shared data, that is, how to make operations scattered across multiple threads that interact with each other stop interfering with each other. The root cause of this interference is that an unpredictable sequence of instructions composed of multiple sets of operation instructions attempts to change a shared variable (as shown above, in thread A/B/C).There are 3 * 3 = 9 instruction sequences.
  1. The Java language provides the most basic guarantee for mutually exclusive operations, that is, the synchronized keyword, which is equivalent to an exclusive lock. Modify the Counter counter class in the above program with the following code (simply using the synchronized keyword to modify the method of performing an accumulative operation so that the accumulative operation scattered across multiple threads is executed serially):
    1. The volatile keyword is also frequently mentioned with synchronized.However, in this scenario where multiple threads perform write operations, the volatile keyword is a bit of a problem. It only guarantees that the thread can read the latest counter values, but it does not prevent write overrides or guarantee the reliability of read operations.
/* Simple Counter */
	private static class Counter {
		private int count;
		public Counter(int count) {
			this.count = count;
		}
		/* Counter plus 1, decorated with synchronized keyword*/
		synchronized public void plusOne() {
			count++;
		}
		@Override
		public String toString() {
			return "[ count is " + count + " ]";
		}
	}

Run the program again with the following results:

[ count is 1000 ]
[ count is 1000 ]
[ count is 1000 ]
[ count is 1000 ]
[ count is 1000 ]
[ count is 1000 ]
[ count is 1000 ]
[ count is 1000 ]
[ count is 1000 ]
[ count is 1000 ]

We can see that the results are correct and the synchronized keyword guarantees us.

  1. Having said the synchronized keyword, let's look at the Wait/Notify mechanism in Java, the wait() / notify() method provided in the Object class.
    • We know that wait() / notify() must be used in conjunction with synchronized so that synchronized methods or blocks of code running in different threads can communicate/communicate with each other (that is, threads must actively wait for and passively wake up) (wait() and notify() methods must be used together).The wait() method provides a wait mechanism that queues waiting threads and processes interrupt requests from the thread and wakes a waiting thread in response to notification requests from the notify() function.Using the exclusivity provided by synchronized combined with the wait() and notify() functions to provide a wait/notification mechanism, a simple lock can be implemented, using a custom lock instead of the synchronized keyword for the cumulative operation in the Counter, with the following code (modifying only the Counter counter class and adding the custom lock class MyLock):
/*
 * Update counters simultaneously by multiple threads (synchronize multithreaded operations with custom locks)
 */
public class Temp_1 {
	public static void main(String[] args) {
		// 10 consecutive simulations
		for(int i = 0;i < 10;i++) {
			update_counter_demo();
		}
	}
	// Construct worker threads to simulate multiple threads updating counters simultaneously
	public static void update_counter_demo(){
		Counter counter = new Counter(0);
		int worker_thread_num = 10;
		Thread[] workers = new Thread[worker_thread_num];
		for(int j = 0;j < worker_thread_num;j++) {			
			workers[j] = new Thread(new Runnable() {
				@Override
				public void run() {
					try {
						Thread.sleep(100); // Wait 100 milliseconds first
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					for(int i = 0;i < 100;i++) {
						counter.plusOne();
					}
				}
			});
		}
		// Start all worker threads
		for(int i = 0;i < worker_thread_num;i++) {
			workers[i].start();
		}
		// Waiting for thread to end
		for(int i = 0;i < worker_thread_num;i++) {
			try {
				workers[i].join();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		// Print counter value
		System.out.println(counter);
	}
	/* Simple Counter */
	private static class Counter {
		private int count;
		private MyLock lock;
		public Counter(int count) {
			this.count = count;
			this.lock = new MyLock();
		}
		/* Counter plus 1 */
		public void plusOne() {
			lock.lock(); // Acquire locks: Protect cumulative operations
			count++;
			lock.unLock(); // Release lock
		}
		@Override
		public String toString() {
			return "[ count is " + count + " ]";
		}
	}
}
/* Simple lock implementation */
class MyLock{
	private int count = 0;
	/* Acquire locks */
	public void lock() {
		synchronized(this) {			
			while(count != 0) {
				try {
					wait(); // Current thread actively waits (expected to be notify() or interrupted())
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
			count++;
		}
	}
	/* Release lock */
	public void unLock() {
		synchronized (this) {
			if(count > 0) {
				count--;
				notify(); // Notify (wake up) threads waiting on the same object monitor
			}
		}
	}
}

Execute the program and run the result correctly.In the custom lock MyLock, the lock() method performs a lock operation, which checks the state of the lock in a synchronized block of code. If the state of the lock is not zero, it means'locked', performs a wait operation, or acquires the lock by changing the state of the lock.Similar to releasing a lock, synchronized provides exclusive protection by changing the state of the lock to indicate'lock'and'unlock'.Of course, the obvious drawback of this custom lock is that: 1 There is no record that the thread owns the lock, which does not prevent the thread that did not perform the lock operation from releasing the lock.

  1. The concept of synchronization.In contrast to Synchronization, Asynchronization is a concept that is used in multithreaded programming.First,'Asynchronous', thread A delegates thread B to perform an operation, but it doesn't have to wait for thread B to finish executing, it continues to do something else.And'synchronization'does not mean that two or more threads are executing at the same time,'synchronization' means that multiple threads cooperate with each other and work together to complete their work. Threads communicate with each other and communicate with each other. It is not an'island'.The use of the synchronized keyword and the Wait/Notify mechanism to synchronize multiple threads to complete counter accumulation are examples of synchronization.

Topics: Programming Java