Moment For Technology

Java multithreading details (attached source code)

Posted on Nov. 27, 2023, 10:51 p.m. by Bradley Stanley
Category: The back-end Tag: The back-end java security The compiler

Threads are the smallest unit of program execution, and multithreading means that a program can have more than one unit of execution running at the same time (depending on your CPU core).

Starting a new Thread in Java is as simple as creating a Thread object, calling its start method, and a new Thread is started.

So where does the execution code go? There are two ways to create a Thread object: 1. When creating a Thread object, duplicate its run method and put the code executing it in the run method. 2. When creating a Thread object, pass it a Runnable object and place the execution code in the Runnable object's run method.

If multiple threads operate on different resources, the threads will not interact with each other and will not cause any problems. However, if multiple threads operate on the same resource (shared variables), there will be multithreading conflicts. To understand why these conflicts occur, you need to understand the Java Memory Model (JMM).

Java Memory Model (JMM)

1.1 Java Memory Model (JMM) Introduction

The Java memory model determines when a write to a shared variable by one thread is visible to another thread. From a sampling point of view: shared variables between threads are stored in main memory. Each thread has a private local memory where it can read/write copies of the shared variables.

There are two types of memory: main memory and thread-local memory. When a thread starts, a copy of the shared variable is placed in local memory.

When a shared variable is changed, the thread flusher it to the main memory (not necessarily immediately, but at the thread's own control).

When a variable changes in main memory, a signal is sent to inform other threads to set the cache row of that variable to invalid, so that when another thread reads the variable from local memory and finds that the variable is invalid, it reads it again from memory.

1.2 the visibility

From the introduction above, we can see that a problem with multithreading shared variables is visibility: changes made by one thread to a shared variable are not immediately visible to another thread.

classData{inta =0; intb =0; intx =0; inty =0; // a thread executes publicvoidthreadA(){a =1; x = b; }// thread b executes publicvoidthreadB(){b =2; y = a; }}

If two threads execute threadA and threadB methods at the same time, respectively. X ==y==0.

Because after a and b is the assignment, not flushed to main memory, is executed x = b and y = a statement, the thread does not know when a and b also has been changed, is still the original value of 0.

1.3 order

To improve program execution performance, the Java memory model allows the compiler and processor to reorder instructions. The reordering process does not affect the execution of single-threaded programs, but affects the correctness of multi-threaded concurrent execution.

classReorder{intx =0; booleanflag =false; publicvoidwriter(){ x =1; flag =true; }publicvoidreader(){if(flag) {inta = x * x; . }}}

For example, in the above example, we use flag to indicate that the x variable has been assigned. However, there is no data dependency between the two statements, so they might be reordered, i.e., flag = true before x = 1. Is this a problem?

In single-threaded mode, there is no problem because the Writer method is a whole. Other methods can be executed only after the Writer method is executed. Therefore, the change of the order of flag = true statements and x = 1 statements has no impact.

In multithreaded mode, this may cause problems because the writer method is not finished, but the reader method is called by another thread. If the order of the flag = true statement and the x = 1 statement changes, the flag may be true, but the x has not been assigned. As with the program intent, unexpected problems can arise.

1.4 atomic

In Java, reads and assignments to variables of primitive data types are atomic operations, that is, they are uninterruptible and either performed or not performed.

x =1; // atomicity y = x; // Not atomic x = x +1; // not atomic x++; // Not atomic system.out.println (x); / / atomicity

Formula 2: There are two atomic operations that read x and assign y. Formula 3: also three atomic operations, read the value of x, add 1, assign to x. Formula 4: Same as formula 3.

So there are two types of atomic operations: 1. Assign a basic datatype constant to a variable. 2. Read variable values of basic data types. No computational operation is atomic.

1.5 summary

Multithreaded manipulation of shared variables raises three problems, visibility, order, and atomicity.

Visibility: When one thread changes a shared variable, it may not immediately flush to main memory, when another thread reads the shared variable, changing the previous value. So this shared variable change is not visible to other threads.

Orderliness: The compiler and processor reorder instructions and change the order of statements, which can lead to strange exceptions in multithreaded situations.

Atomicity: Only reads and assignments to variables of primitive data types are atomic operations.

There are two ways to solve these three problems:

The volatile keyword: It solves only two problems visibility and orderliness, but if volatile modifs a basic datatype variable and only does reads and assignments, there is no atomicity problem. For example, use it to modify a Boolean variable.

Locking: This ensures that only the same thread operates on a shared variable at the same time. When the current thread operates on a shared variable, the shared variable will not be modified by other threads, so visibility, order, and atomicity problems are solved. It can be divided into synchronized Lock and JUC Lock.

The volatile keyword

The effect of the volatile keyword

1. Visibility: A read of a volatile variable always shows the last write to that volatile variable (any thread).

Ordering: The prohibition of instruction reordering in a program in which operations on volatile variables must have been performed in such a way that all prior operations must have been performed and subsequent operations must not have been performed because the results are visible to subsequent operations.

For a detailed explanation of this, see the happens-before rule in Understanding the Java Memory Model.

ClassVolatileFeaturesExample {/ / use volatile declare a basic data type variable vlvolatilelongvl = 0 l; // Assign to a volatile base variable publicVoidSet (longl){vl = l; } / / for a single basic data type variable volatile compound operation publicvoidgetAndIncrement () {vl++; }// Read publicLongget (){returnvl; }} classVolatileFeaturesExample {/ / declare a basic data type variable vllongvl = 0 l; / / equivalent to add the sync lock publicsynchronizedvoidset (longl) {vl = l; } / / ordinary methods publicvoidgetAndIncrement () {longtemp = the get (); temp +=1L; set(temp); } / / added synchronous equivalent to lock publicsynchronizedlongget () {returnvl; }}

If volatile modifies a primitive datatype variable and only reads and assigns to that variable, it is equivalent to adding a synchronization lock.

Synchronized Synchronized lock

Synchronized When accessing a locked resource, only the thread that obtains the lock can operate the locked resource. Other threads must block and wait.

So for a thread to block and wait, to run, what are the states of the thread?

3.1 Thread Status

State transition diagram

Threads are divided into five states:

Create state (New) : Create a Thread object, the Thread object is the New state.

Runnable: Indicates that this thread can run at any time, as long as the CPU has execution rights.

Note: This state can result from a new state transition (by calling thread's start method) or from a blocking state transition

Running: Indicates that the thread is Running. Note that the Running state can only be reached from the runnable state.

Blocked: Indicates that the thread is currently stopped, which can be divided into three types:

1). Synchronization blocking state: if the thread fails to acquire the synchronization lock, it enters the synchronization blocking state.

2). Blocked state: the thread calls the WAIT method to enter this state. Note: The join method is also essentially implemented via the wait method.

3). Other blocking states: Let the Thread sleep through thread. sleep method, open IO flow and let the Thread wait for blocking.

Dead: A thread enters the Dead state when its run method has finished running. This state cannot be changed to another state.

3.2 synchronized Synchronized method or block

How does a synchronized method or block work?

Equivalent to a large room, there is a lock lock on the door of the room, where all the synchronization methods or synchronization blocks associated with the lock lock are stored.

When a thread wants to execute a synchronization method or block of the lock lock, it goes to the door of the room, and if it finds the lock lock is still there, it takes the lock into the room and locks the room, and it can execute any synchronization method or block in the room.

When another thread wants to execute a synchronized method or block of the lock, it comes to the door, finds that the lock is gone, and waits outside the door. At this point, the thread is in the synchronized synchronized blocking thread pool.

As soon as the thread that took the lock, the synchronization method or the synchronization block completes, it exits the room and places the lock on the door.

At this point, the thread waiting outside the door will compete for the lock lock, the thread that holds the lock can enter the room, the other threads have to continue to wait.

Note: A synchronized lock is a lock that locks all synchronized methods or blocks associated with the lock.

What exactly is a synchronized lock?

In Java, each object has a lock marker (monitor). When multiple threads access an object at the same time, the thread can access the object only after acquiring the lock of the object.

3.3 Wait and Notify and notifyAll

These three methods are mainly used to implement the problem of threads waiting for each other.

Calling the object lock's wait method causes the current thread to wait, putting the current thread into the thread wait pool for the object Lock. Calling the notify method of the object lock wakes up a random thread from the thread wait pool, and notifyAll wakes up all threads.

Note: the object lock wait and notify, and notifyAll method call must be on synchronization method based on object lock lock or synchronized block, otherwise you will be thrown IllegalMonitorStateException anomalies.

How do wait and notify and notifyAll operate?

As in synchronized, when the wait method for locking a lock is called, the thread exits the room and returns the lock, but not into the synchronized synchronized blocking thread pool, but into the thread wait pool that locked the lock.

If another thread notifies the lock, it will wake up a random thread from the thread waiting pool and place it into the synchronized synchronized blocking thread pool (remember that only the thread holding the lock can lock the room). The notifyAll method of locking is called, which awakens the thread to wait for all the threads in the pool.

Note: When a thread blocked by WAIT enters a synchronized block again, execution continues from where the wait method was called.

There are only four ways to wake up a thread in the thread waiting pool that locks it:

Wake up with notify()

Wake up with notifyAll()

Wake up with interrupt()

If a thread is in the wait state by calling wait(long timeout), it will wake up when the time times out.

Notice of wait, notify, and notifyAll method must first obtain the lock to call, otherwise throw IllegalMonitorStateException anomalies. Only synchronized modules allow the current thread to acquire the lock, so wait can only be performed in synchronized modules.

Other important methods

4.1 the join method

Make the current thread wait for another thread to complete before continuing.

publicfinalvoidjoin()throwsInterruptedException {join(0); } publicfinalsynchronizedvoidjoin throwsInterruptedException (longmillis) {/ / get the current system milliseconds longbase = System.currentTimeMillis(); longnow =0; / / millis less than zero, throw an exception if (millis 0) {thrownewIllegalArgumentException (" the timeout value is negative, "); }if(millis ==0) {// check whether the current thread isAlive by isAlive while(isAlive()) {// wait(0) indicates that the current thread is waiting indefinitely for wait(0); While (isAlive()) {longdelay = millis-now; if(delay =0) {break; }// The current thread is waiting for delay in milliseconds. now = System.currentTimeMillis() - base; }}}

The join method is the method in Thread, and the lock object synchronized is the Thread object. The current Thread can be made to wait by calling the wait method of Thread object

Note: We are making the current Thread wait, that is, the Thread calling the Join method, not the Thread of the Thread object. So when does the current thread wake up?

NotifyAll is called to wake up all threads in the Thread pool when the Thread is dead.


publicstaticvoidjoinTest(){ Thread thread =newThread(newRunnable() { @Overridepublicvoidrun(){for(inti =0; i 10; i++) {try{ Thread.sleep(100); }catch(InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+": i==="+i); } } },"t1"); thread.start(); try{ thread.join(); }catch(InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+": end"); }

4.2 the sleep method

Simply make the current thread wait a certain amount of time before continuing.

4.3 yield method

Transitions the current thread state from a running state to a runnable state, and continues if CPU execution is acquired again.

4.4 interrupt method

Interrupts a thread, which interrupts a blocked thread but has no effect on a running thread.


PublicstaticvoidinterruptTest () {/ / in the blocking state under Thread Thread Thread = newThread (newRunnable {@ Overridepublicvoidrun () () {try { System. Out. Println (Thread. CurrentThread (). The getName () + "start"); Thread.sleep(1000); System. Out. Println (Thread. CurrentThread (). The getName () + "end"); } Catch (InterruptedException e) {system.out.println (thread.currentThread ().getName()+" generate exception "); } } },"t1"); thread.start(); Thread1 =newThread(newRunnable() {@overridePublicVoidRun (){ System. Out. Println (Thread. CurrentThread (). The getName () + "start"); inti =0; while(i Integer.MAX_VALUE -10) { i = i +1; for(intj =0; j i; j++); } System.out.println(Thread.currentThread().getName()+" i=="+i); System. Out. Println (Thread. CurrentThread (). The getName () + "end"); } },"t2"); thread1.start(); try{ Thread.sleep(10); }catch(InterruptedException e) { e.printStackTrace(); } system.out.println (thread.currentThread ().getName()+" interrupt "); thread.interrupt(); thread1.interrupt(); }

4.5 isInterrupted method

Returns whether this thread is interrupted. Note that the isInterrupted method of a thread returns true after the interrupt method is called. If the exception is handled, this flag is set to false, which means that isInterrupted returns false.

4.6 Thread priority and daemon threads

Thread priorities in Java range from 1 to 10, with the default priority being 5.

In Java, threads are divided into user threads and daemon threads. IsDaemon returns true to indicate that it is a daemon thread. The Java virtual machine exits when all user threads have completed execution, regardless of whether there are any daemons remaining.

When a new thread is created, the priority of the new thread is equal to the priority of the thread that created it, and the new thread is the daemon thread only if the thread that created it is the daemon thread.

It is also possible to change the priority of a thread by using setDaemon.

V. Examples to explain

5.1 No synchronization lock is added

importjava.util.Collections; importjava.util.List; importjava.util.concurrent.CopyOnWriteArrayList; importjava.util.concurrent.CountDownLatch; classData {intnum; publicData(intnum){this.num = num; }publicintgetAndDecrement(){returnnum--; }}classMyRun implements Runnable {privateData data; // Use to record the number of all tickets sold privateListlist; privateCountDownLatch latch; publicMyRun(Data data, Listlist, CountDownLatch latch){ = data; this.list=list; this.latch = latch; } @Overridepublicvoidrun(){try{ action(); } finally {// release latch.countdown (); Num 0 is not used as a judgment condition until the thread exits. // This will result in the use of the shared variable data.num between the two places, so there are more conditions to consider when doing multi-threaded synchronization. // We loop for only 5 times, which means only 5 tickets are sold per thread, and store all sold numbers into the list collection. publicvoidaction(){for(inti =0; i 5; i++) {try{ Thread.sleep(10); }catch(InterruptedException e) { e.printStackTrace(); }intnewNum = data.getAndDecrement(); System.out.println(" Thread "+ thread.currentThread ().getName()+" num=="+newNum); list.add(newNum); } }}publicclassThreadTest {publicstaticvoidstartThread(Data data, String name, Listlist,CountDownLatch latch){ Thread t =newThread(newMyRun(data,list, latch), name); t.start(); } publicStaticVoidMain (String[] args){CountDownLatch =newCountDownLatch(6); CountDownLatch =newCountDownLatch(6); longstart = System.currentTimeMillis(); Listlist=newCopyOnWriteArrayList(); Data data =newData(30); startThread(data,"t1",list, latch); startThread(data,"t2",list, latch); startThread(data,"t3",list, latch); startThread(data,"t4",list, latch); startThread(data,"t5",list, latch); startThread(data,"t6",list, latch); try{ latch.await(); }catch(InterruptedException e) { e.printStackTrace(); }// Process the list collection, sort and flip collections.sort (list); Collections.reverse(list); System.out.println(list); longtime = System.currentTimeMillis() - start; System.out.println("\n mainline end time=="+time); }}

The output is zero

Thread T2NUM ==29 t6NUM ==27 T4NUM ==28 T1NUM ==30 T3NUM ==30 T4NUM ==26 T4NUM ==24 T5NUM ==25 T5NUM ==23 T1num ==2 T3num ==21 t4NUM ==20 T5NUM ==18 T6NUM ==17 T1NUM ==16 T4NUM ==15 T4NUM ==12 T6NUM ==13 T1NUM == 9 threads t3num = = 10 threads t2num 11 thread t1num = = = = 8 thread t6num = = 5 thread t2num t5num = = = = 7 threads 3 thread t3num = = 4 thread t4num = = 6 [30,30,29,28,28,27,26,25,24,23,22,21, 20,19,18,17,16,15,14,13,12,11,10,9,8,7,6,5,4,3] the main thread end time = = 62

A problem was found in the results, there were duplicate tickets, so 30 tickets were not sold out. The main reason is that the getAndDecrement method operations of the Data class are not multithreaded safe.

First, it does not guarantee atomicity. It is divided into three operations: first, it reads the value of num, then it decrement, and then it returns the value before decrement.

Because num is not modified by the volatile keyword, it does not guarantee visibility or order.

So simply ensuring that getAndDecrement is multithreaded and secure should address the above issues. What about ensuring that getAndDecrement is multithreaded? The easiest way to do this is to prefix the getAndDecrement method with the synchronized keyword.

This is a synchronized key lock and that's an instance of a data object, so that means that when multiple threads call getAndDecrement, only one thread can call it, and that when that call completes, other threads can call getAndDecrement.

Because only one thread calls the getAndDecrement method at a time, it doesn't have to worry about changing the num variable when it does num--. So atomicity, visibility and order can be guaranteed.

5.2 Using minimum Synchronization Lock

classData{intnum; public Data(intnum) {this.num=num; } / / lock getAndDecrement method with synchronous public synchronizedintgetAndDecrement () {returnnum -; }}

The output

T1num ==30 T6NUM ==29 T6NUM ==28 T4NUM ==26 T5NUM ==25 T6NUM ==22 T1NUM ==21 T1NUM ==23 T4NUM ==24 T4num ==2 T5num ==19 for the 0 thread T2NUM ==18 T3NUM ==17 T4NUM ==13 T4NUM ==14 T6NUM ==15 for the 16 thread T2NUM ==12 T4NUM ==9 T5num ==10 for the 7 thread Thread t3num 11 thread t6num = = = = 8 thread t4num t2num = = = = 6 threads 3 thread t1num = = 2 thread t3num = = 4 thread t5num = = 5 thread t6num = = 1 [30,29,28,27,26,25,24,23,22,21,20,19,18 The main thread, 17,16,15,14,13,12,11,10,9,8,7,6,5,4,3,2,1] end time = = 61

We simply added a synchronization lock to Data's getAndDecrement method and found that we solved the multi-threaded concurrency issue. The main reason is that we only use the shared variable num in one place, so we only need to synchronize that place. And you'll notice that the total time spent in the end is almost the same as it would have been without the synchronization lock, because our synchronization code is small enough.

On the contrary, we add synchronous lock is not reasonable, may also be able to achieve multi-threaded safety, but the time will be greatly increased.

5.3 Unreasonable Use of a Synchronization Lock

@Overridepublicvoidrun(){try{synchronized(data){ action(); }}finally{// Release latch share latch. CountDown (); }}

Input result:

Thread t1num = = 30 thread t1num t1num = = = = 29 threads and thread t1num 27 thread t1num = = = = 26 thread t6num = = 25 thread t6num t6num = = = = 24 thread 23 thread t6num t6num = = = = 22 thread thread t5num 21 = = 2 T5num ==19 t5NUM ==18 T5NUM ==17 T4NUM ==16 T4NUM ==15 T4NUM ==13 T4NUM ==12 T3NUM ==10 T3NUM == 9 thread t3num = = 8 thread t3num t3num = = = = 7 thread 6 thread t2num = = 5 thread t2num = = 4 thread t2num = = 3 thread t2num 2 thread t2num = = = = 1 [30,29,28,27,26,25,24,23,22,21,20,19,18 The main thread, 17,16,15,14,13,12,11,10,9,8,7,6,5,4,3,2,1] end time = = 342

In this case, putting the entire Action method in a sync block also solved the multi-threaded conflict problem, but that would have taken several times as long as adding a sync lock to getAndDecrement.

So when we add a synchronization lock, which needs to be synchronized, is to see where the shared variables are used. For example, synchronization variables are only used in the getAndDecrement method, so you can simply lock it.

However, if data.num0 is used as a loop condition in the action method, then the entire action method must be placed in the synchronization module when the lock is applied, because we must ensure that, Num 0 indicates that the getAndDecrement method calls these codes in a synchronized module, which would otherwise cause multi-threaded conflicts.


If you want to know more about multi-threading, you can follow me. I will also sort out more knowledge about multi-threading in the future and share it with you. By the way, I recommend an exchange learning group: 650385180, which will share some videos recorded by senior architects: Spring, MyBatis, Netty source code analysis, high concurrency, high performance, distributed, multi-threading, microservice architecture principle, JVM performance optimization these become architects necessary knowledge system. I can also get free learning resources, which I benefit a lot from now.

About (Moment For Technology) is a global community with thousands techies from across the global hang out!Passionate technologists, be it gadget freaks, tech enthusiasts, coders, technopreneurs, or CIOs, you would find them all here.