[concurrent programming] (learning notes - management of shared model) - part3

Concurrent programming - management of shared model-3

This article is a video guide 👉 Dark horse programmer - Concurrent Programming

1. Problems caused by sharing

1-1 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
    • Problems occur when multiple threads interleave instructions when reading and writing to shared resources
  • 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 zone in the following code:

static int counter = 0;
 
static void increment() 
// Critical zone 
{   
    counter++; 
}
 
static void decrement() 
// Critical zone 
{ 
    counter--; 
}

1-2 Race Condition

When multiple threads execute in the critical area, the results cannot be predicted due to different execution sequences of code, which is called race condition

2.synchronized solution

2-1 solution

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 section uses a blocking solution: synchronized to solve the above problems, commonly known as * * object lock. It uses mutually exclusive * * to make at most one thread hold the object lock at the same time. When other threads want to obtain the object lock, they will be blocked. This ensures that the thread with the 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 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.

2-2 use of synchronized

grammar

synchronized(object) {
	//Critical zone
}

solve

static int counter = 0; 
//Create a public object as the object of the object lock
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) {     
        counter++;            
       	 }       
 	   }    
    }, "t1");
 
    Thread t2 = new Thread(() -> {       
        for (int i = 0; i < 5000; i++) {         
            synchronized (room) {            
            counter--;          
            }    
        } 
    }, "t2");
 
    t1.start();    
    t2.start(); 
    t1.join();   
    t2.join();    
    log.debug("{}",counter); 
}

2-3 synchronized on method

  • On the member method, the lock is the current instance object

    public class Demo {
    	//Add the synchronized keyword to the method
    	public synchronized void test() {
    	
    	}
    	//Equivalent to
    	public void test() {
    		synchronized(this) {
    		
    		}
    	}
    }
    
  • On a static method, the lock is the Class object of the current Class

    public class Demo {
    	//Add the synchronized keyword to the static method
    	public synchronized static void test() {
    	
    	}
    	//Equivalent to
    	public void test() {
    		synchronized(Demo.class) {
    		
    		}
    	}
    }
    

3. Thread safety analysis of variables

3-1 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

3-2 are local variables thread safe?

  • Local variables are thread safe

  • However, the object referenced by the local variable may not be (depending on whether the object is shared and read-write operations are performed)

    • If the object does not escape the scope of the method, it is thread safe
    • If the object escapes from the scope of the method, thread safety needs to be considered
  • Local variables are thread safe - each method creates a stack frame in the stack of the corresponding thread and will not be shared by other threads

  • If the called object is shared and a read-write operation is performed, the thread is not safe

  • If it is a local variable, the corresponding object will be created in the heap, and there will be no thread safety problem

3-3 common thread safety classes

  • String
  • Integer
  • StringBffer
  • Random
  • Vector (thread safe implementation class of List)
  • Hashtable (thread safe implementation class of Hash)
  • Classes under java.util.concurrent package

Here, they are thread safe, which means that when multiple threads call a method of their same instance, they are thread safe

For example:

Hashtable table = new Hashtable();

new Thread(()->{
    table.put("key", "value1");
}).start();
new Thread(()->{
    table.put("key", "value2");
}).start();
  • Each of their methods is atomic (synchronized is added)
  • However, note that the combination of their multiple methods is not atomic, so thread safety problems may occur

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);
}

It can be that the combination of multiple methods is not atomic, so thread safety problems may occur

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 that can change the value. How do these methods ensure thread safety?

This is because the return values of these methods create a new object instead of directly changing the String and Integer objects themselves.

3-4 example analysis

Example 1:

public class MyServlet extends HttpServlet {
    // Is it safe? no
    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 remains unchanged, but the year, month and day in it are variable
    final Date D2 = new Date();

    public void doGet(HttpServletRequest request, HttpServletResponse response) {
        //Use the above variables
    }
}

Example 2:

public class MyServlet extends HttpServlet {
    // Is it safe? no, shared
    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;//There may be a problem with multiple thread calls here

    public void update() {
        // ...
        count++;
    }
}

Example 3:

@Aspect
@Component
public class MyAspect {//In a single instance, member variables are shared
    // 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));
    }
}

4.Monitor

4-1 Java object header

Take the 32-bit virtual machine as an example:

Common objects:

Array object:

The Mark Word structure is:

64 bit virtual machine Mark Word:

4-2 principle of Monitor

Monitor is translated as monitor or tube pass

Each Java object can be associated with a Monitor object. If you use synchronized to lock the object (heavyweight), the pointer to the Monitor object will be set in the Mark Word of the object header

The Monitor structure is as follows:

  • At the beginning, the Owner in Monitor is null
  • When Thread-2 executes synchronized(obj), the Owner of the Monitor will be set to Thread-2. There can only be one Owner in the Monitor
  • When Thread-2 is locked, if Thread-3, Thread-4 and Thread-5 also execute synchronized(obj), it will enter the EntryList (BLOCKED)
  • Thread-2 executes the contents of the synchronization code block, and then wakes up the waiting threads in the EntryList to compete for the lock. The competition is unfair
  • In the figure, Thread-0 and Thread-1 in the WaitSet are threads that have obtained locks before, but the conditions are not met to enter the WAITING state. They will be analyzed later when we talk about wait notify

be careful:

  • synchronized must enter the monitor of the same object to have the above effect
  • Objects that are not synchronized are not associated with monitors and do not comply with the above rules

5. Advanced synchronized principle

5-1 lightweight lock

  • Lightweight lock usage scenario: when an object is accessed by multiple threads, but the access time is staggered (there is no competition), lightweight locks can be used to optimize.
  • Lightweight locks are transparent to the user, that is, the syntax is still synchronized

Here's a chestnut: suppose there are two methods to synchronize blocks and lock the same object

static final Object obj = new Object();
public static void method1() {
    synchronized( obj ) {
        // Synchronization block A
        method2();
    }
}
public static void method2() {
    synchronized( obj ) {
        // Synchronization block B
    }
}
  • Create a Lock Record object. The stack frame of each thread will contain a Lock Record object. The mark word of the locked object can be stored internally (Monitor is no longer used at the beginning)

  • Let the Object reference in the lock record point to the lock Object, and try to replace the mark word in the Object with cas. Put the mark word into the lock record and save it

  • If cas replacement is successful, the Object header of the Object is replaced with the address and state 00 of the lock record (lightweight lock state), and the thread locks the Object

  • If cas fails, there are two situations:

    • If other threads already hold the lightweight lock of the Object, it indicates that there is competition and enters the lock expansion process

    • If the synchronized lock reentry is performed by yourself, add another Lock Record as the reentry count

  • When exiting the synchronized code block (when unlocking), if there is a lock record with a value of null, it indicates that there is reentry. At this time, reset the lock record, indicating that the reentry count is reduced by one

  • When exiting the synchronized code block (when unlocking), the value of the lock record is not null. In this case, cas is used to restore the value of Mark Word to the object header

    • If successful, the unlocking is successful
    • Failure indicates that the lightweight lock has undergone lock expansion or has been upgraded to a heavyweight lock. Enter the heavyweight lock unlocking process

5-2 lock expansion

If the CAS operation fails when trying to add a lightweight lock, then another thread adds a lightweight lock (with competition) to this object. At this time, lock expansion is required to change the lightweight lock into a heavyweight lock.

  • When Thread-1 performs lightweight locking, Thread-0 has already applied lightweight locking to the object

  • At this time, Thread-1 fails to add a lightweight lock and enters the lock expansion process

    • That is, apply for the Monitor lock for the Object object and let the Object point to the heavyweight lock address
    • Then enter the EntryList BLOCKED of Monitor

  • When Thread-0 exits the synchronization block unlocking, cas is used to restore the value of Mark Word to the object header, which fails. At this time, the heavyweight unlocking process will enter, that is, find the Monitor object according to the Monitor address, set the Owner to null, and wake up the BLOCKED thread in the EntryList

5-3 spin optimization

When the heavyweight lock competes, spin can also be used for optimization. If the current thread spins successfully (that is, the lock holding thread has exited the synchronization block and released the lock), the current thread can avoid blocking

  • Spin will occupy CPU time. Single core CPU spin is a waste, and multi-core CPU spin can give play to its advantages.
  • After Java 6, the spin lock is adaptive. For example, if the object has just succeeded in a spin operation, it is considered that the possibility of successful spin this time will be high, so spin more times; On the contrary, less spin or even no spin. In short, it is more intelligent.
  • After Java 7, you can't control whether to turn on the spin function

5-4 deflection lock

The lightweight lock still needs to perform CAS operation every time it re enters when there is no competition (just its own thread).

Java 6 introduces bias lock for further optimization: only when CAS is used for the first time to set the thread ID to the Mark Word header of the object, and then it is found that the thread ID is its own, it means that there is no competition and there is no need to re CAS. In the future, as long as there is no competition, the object belongs to the thread

Biased state

  • Normal: normal status. No lock is added. The first 62 bits store the information of the object. The last 2 bits are status (01). The penultimate bit indicates whether to use bias lock (unused: 0)
  • Biased: biased status. Biased lock is used. The first 54 bits save the ID of the current thread. The last 2 bits are status (01). The penultimate bit indicates whether to use biased lock (use: 1)
  • Lightweight: a lightweight lock is used. The first 62 bits hold the pointer of the lock record, and the last two bits are the status (00)
  • Heavyweight: the heavyweight lock is used. The first 62 bits store the address pointer of the Monitor, and the last two bits are the status (10)

  • If the bias lock is turned on (on by default), the last three digits of Mark Word of the object should be 101 when creating the object
  • However, the bias lock is delayed by default and will not take effect as soon as the program starts. Instead, the created object will not be set to the bias state until the program runs for a period of time (a few seconds)
  • If the bias lock is not opened, the last three digits of Mark Word of the object should be 001

Revocation bias

The following situations will invalidate the bias lock of the object

  • Call the hashCode method of the object
  • The object is used by multiple threads
  • The wait/notify method is called (calling the wait method will cause lock inflation and use heavyweight locks)

Batch re bias

  • If the object is accessed by multiple threads, but there is no competition between threads, the object biased to T1 still has the opportunity to re bias to T2. Re bias resets the Thread ID
  • When the revocation exceeds 20 times (exceeding the threshold), the JVM will think whether the bias is wrong. At this time, it will re bias to the locking thread when locking the object

Batch undo

When the unbiased lock threshold is revoked more than 40 times, the jvm will feel that it is really biased wrong and should not be biased at all. Therefore, all objects of the whole class will become non biased, and the new objects will also be non biased

6.Wait/Notify

6-1 principle

  • When the Owner thread finds that the conditions are not met, it calls the wait method to enter the WaitSet and change to the WAITING state
  • Both BLOCKED and WAITING threads are BLOCKED and do not occupy CPU time slices
  • The BLOCKED thread wakes up when the Owner thread releases the lock
  • The WAITING thread will wake up when the Owner thread calls notify or notifyAll, but after waking up, it does not mean that the Owner obtains the lock immediately. It still needs to enter the EntryList to compete again

A picture in the art of Java Concurrent Programming is attached here:

6-2 API introduction

  • wait() causes the thread entering the object monitor to wait in the waitSet
  • notify() selects one of the threads waiting for waitSet on the object to wake up
  • notifyAll() wakes up all the threads waiting for waitSet on the object
  • The wait() method will release the lock of the object and enter the WaitSet waiting area, so that other threads can get the lock of the object. Unlimited wait until notify
  • wait(long n) a time limited wait until n milliseconds, or notify

Note: the wait and notify methods can only be called after the object is locked

example:

@Slf4j
public class Test6 {
    final static Object lock = new Object();

    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (lock) {
                log.debug("implement...");
                try {
                    lock.wait();//Let the thread wait on the lock
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug("Other codes...");
            }
        }, "t1").start();

        new Thread(() -> {
            synchronized (lock) {
                log.debug("implement...");
                try {
                    lock.wait();//Let the thread wait on the lock
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug("Other codes...");
            }
        }, "t2").start();
        Sleeper.sleep(2);
        log.debug("awaken lock Thread on");
        synchronized (lock) {
            lock.notify(); //Wake up a thread on lock
            //lock.notifyAll(); // Wake up all waiting threads on lock
        }
    }
}
//output
20:36:08 DEBUG [t1] (Test6.java:13) - implement...
20:36:08 DEBUG [t2] (Test6.java:25) - implement...
20:36:10 DEBUG [main] (Test6.java:35) - awaken lock Thread on
20:36:10 DEBUG [t1] (Test6.java:19) - Other codes...

6-3 correct use of wait / notify

Before you start, look at the difference between sleep(long n) and wait(long n):

  1. sleep is the Thread method, while wait is the Object method
  2. sleep does not need to be used with synchronized forcibly, but wait needs to be used with synchronized
  3. sleep does not release the object lock while sleeping, but wait releases the object lock while waiting
  4. Their status is TIMED_WAITING

The following parts are recommended for reference: Notes after reading the art of Java Concurrent Programming - part4

The waiting party shall follow the following principles:

  1. Get lock of object
  2. If the condition is not satisfied, call the wait() method of the object, and check the condition after being notified
  3. If the conditions are met, the corresponding logic is executed

Corresponding pseudo code:

synchronized(object){
    while(Conditions not met){
        object.wait();
    }
    Corresponding processing logic
}

The notifying party shall follow the following principles:

  1. Get lock of object
  2. change a condition
  3. Notifies all threads waiting on the object

Corresponding pseudo code:

synchronized(object){
    change a condition
    object.notifyAll();
}

7.Park/Unpark

7-1 basic use

park/unpark are methods in the LockSupport class

//Pause thread
LockSupport.park;

//Resume thread operation
LockSupport.unpark(thread);

park before unpark:

@Slf4j
public class Test24 {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            log.debug("start...");
            Sleeper.sleep(1);
            log.debug("park...");
            //Pause thread
            LockSupport.park();
            log.debug("resume...");
        }, "t1");
        t1.start();
        Sleeper.sleep(2);
        log.debug("unpark...");
        //Resume thread operation
        LockSupport.unpark(t1);
    }
}
13:11:08 DEBUG [t1] (Test24.java:12) - start...
13:11:09 DEBUG [t1] (Test24.java:14) - park...
13:11:10 DEBUG [main] (Test24.java:20) - unpark...
13:11:10 DEBUG [t1] (Test24.java:16) - resume...

7-2 features

Compared with wait/notify of Object:

  • wait, notify and notifyAll must be used together with Object Monitor, but park and unpark do not
  • park and unpark block and wake up threads in terms of threads, while notify can wake up only one waiting thread randomly. notifyAll wakes up all waiting threads, which is not so accurate
  • Park & unpark can unpark first, while wait & notify cannot notify first
  • park does not release the lock, while wait releases the lock

7-3 principle

Each thread has its own Park object, and the object_ counter,_ cond,__ mutex composition

  • When park is called before unpark is called

    • Call park first

      • When the thread runs, the_ Set the value of counter to 0;
      • When park is called, check whether the value of counter is 0. If it is 0, put the thread into the blocking queue cond
      • After being placed in the blocking queue, the counter will be set to 0 again
    • Then call unpark.

      • After the unpark method is called, the value of counter is set to 1

      • To wake up the thread in the blocking queue cond

      • The thread continues to run and sets the value of counter to 0

  • Call unpark first, then park
    • Call unpark
      • Will set counter to 1 (runtime 0)
    • Call the park method
      • Check whether the counter is 0
      • Because unpark has set the counter to 1, set the counter to 0 at this time, but do not put it into the blocking queue cond

8. Re understand thread state transition

Suppose there is a thread Thread t

Case 1: NEW – > runnable

  • When the t.start() method is called, NEW – > runnable

Case 2: runnable < – > waiting

  • When the t thread is called and the object lock is obtained with synchronized(obj)
    • When the obj.wait() method is called, the t thread starts from RUNNABLE – > waiting
    • When calling obj.notify(), obj.notifyAll(), t.interrupt()
      • The contention lock is successful, and the t thread starts from WAITING – > runnable
      • Contention lock failed, t thread from WAITING – > blocked

Case 3: runnable < – > waiting

  • When the current thread calls the t.join() method, the current thread starts from RUNNABLE – > waiting
    • Note that the current thread is waiting on the monitor of the t thread object
  • t when the thread ends running, or the interrupt() of the current thread is called, the current thread starts from WAITING – > runnable

Case 4: runnable < – > waiting

  • Calling the LockSupport.park() method by the current thread will cause the current thread to change from RUNNABLE – > waiting
  • Calling locksupport.unpark (target thread) or calling interrupt() of the thread will cause the target thread to change from WAITING – > runnable

Case 5: runnable < – > timed_ WAITING

After the t thread obtains the object lock with synchronized(obj)

  • When the obj.wait(long n) method is called, the t thread starts from RUNNABLE – > timed_ WAITING
  • The waiting time of T thread exceeds n milliseconds, or when calling obj.notify(), obj.notifyAll(), t.interrupt()
    • Contention lock successful, t thread from TIMED_WAITING –> RUNNABLE
    • Contention lock failed, t thread from TIMED_WAITING –> BLOCKED

Case 6: runnable < – > timed_ WAITING

  • When the current thread calls the t.join(long n) method, the current thread starts from RUNNABLE – > timed_ WAITING
    • Note that the current thread is waiting on the monitor of the t thread object
  • When the waiting time of the current thread exceeds n milliseconds, or the running of the t thread ends, or the interrupt() of the current thread is called, the current thread starts from TIMED_WAITING –> RUNNABLE

Case 7: runnable < – > timed_ WAITING

  • The current thread calls Thread.sleep(long n). The current thread starts from RUNNABLE – > timed_ WAITING
  • The waiting time of the current thread has exceeded n milliseconds. The current thread is from TIMED_WAITING –> RUNNABLE

Case 8: runnable < – > timed_ WAITING

  • When the current thread calls LockSupport.parkNanos(long nanos) or locksupport.parkuntil (long miles), the current thread starts from RUNNABLE – > timed_ WAITING
  • Calling locksupport.unpark (target thread), calling interrupt() of the thread, or waiting for timeout will cause the target thread to start from TIMED_WAITING–> RUNNABLE

Case 9: runnable < – > blocked

  • When the t thread obtains the object lock with synchronized(obj), if the contention fails, it starts from RUNNABLE – > blocked
  • After the synchronization code block of the obj lock thread is executed, it will wake up all BLOCKED threads on the object to compete again. If t threads compete successfully, from BLOCKED – > runnable, other failed threads are still BLOCKED

Case 10: runnable < – > terminated

After all the codes of the current thread have been run, enter TERMINATED

A picture of the art of Java Concurrent Programming is attached here to summarize:

9. Multiple locks

class BigRoom {
    //Create additional objects as locks
	private final Object studyRoom = new Object();
	private final Object bedRoom = new Object();
}

Subdivide the granularity of locks

  • Benefit: it can enhance concurrency
  • Disadvantages: if a thread needs to obtain multiple locks at the same time, it is prone to deadlock

10. Activity

10-1 deadlock

There is such a situation: a thread needs to obtain multiple locks at the same time, and deadlock is easy to occur

For example:

  • The t1 thread obtains the lock of the A object. Next, it wants to obtain the lock of the B object
  • The t2 thread obtains the lock of the B object. Next, it wants to obtain the lock of the A object

Let's look at a deadlock Chestnut:

@Slf4j
public class Test26 {
    public static void main(String[] args) {
        test1();
    }

    private static void test1() {
        Object A = new Object();
        Object B = new Object();
        Thread t1 = new Thread(() -> {
            synchronized (A) {
                log.debug("lock A");
                Sleeper.sleep(1);
                synchronized (B) {
                    log.debug("lock B");
                    log.debug("operation...");
                }
            }
        }, "t1");
        Thread t2 = new Thread(() -> {
            synchronized (B) {
                log.debug("lock B");
                Sleeper.sleep(1);
                synchronized (A) {
                    log.debug("lock A");
                    log.debug("operation...");
                }
            }
        }, "t2");
        t1.start();
        t2.start();
    }
}

Necessary conditions for deadlock generation:

  1. Mutually exclusive condition. Only the competition for resources that must be mutually exclusive will lead to deadlock (such as philosopher's chopsticks, printer equipment). Resources such as memory and speakers that can be used by multiple processes at the same time will not cause deadlock (because processes do not have to block and wait for such resources).
  2. Conditions of non deprivation. The resources obtained by a process cannot be forcibly taken away by other processes before they are used up, but can only be released actively.
  3. Request and hold conditions. The process has maintained at least one resource, but it puts forward a new resource request, and the resource is occupied by other processes. At this time, the requesting process is blocked, but it keeps its existing resources.
  4. Cycle wait condition. There is a circular waiting chain of process resources. The resources obtained by each process in the chain are requested by the next process at the same time.

If you want to learn more about deadlocks, you can refer to Operating system - deadlock

10-2 positioning deadlock

To detect deadlock, you can use jconsole tool, or use jps to locate the process id, and then use jstack to locate the deadlock

  • jps+jstack ThreadID

  • jconsole

10-3 dining problems of philosophers

Code demonstration:

public class Test27 {
    public static void main(String[] args) {
        Chopstick c1 = new Chopstick("1");
        Chopstick c2 = new Chopstick("2");
        Chopstick c3 = new Chopstick("3");
        Chopstick c4 = new Chopstick("4");
        Chopstick c5 = new Chopstick("5");
        new Philosopher("Socrates", c1, c2).start();
        new Philosopher("Plato", c2, c3).start();
        new Philosopher("Aristotle", c3, c4).start();
        new Philosopher("Heraclitus", c4, c5).start();
        new Philosopher("Archimedes", c5, c1).start();
    }
}

/**
 * Chopsticks
 */
class Chopstick {
    String name;

    public Chopstick(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "chopsticks{" + name + '}';
    }
}

/**
 * Philosophers
 */
@Slf4j
class Philosopher extends Thread {
    Chopstick left;
    Chopstick right;

    public Philosopher(String name, Chopstick left, Chopstick right) {
        super(name);
        this.left = left;
        this.right = right;
    }

    public void eat() {
        log.debug("eating...");
        Sleeper.sleep(1);
    }

    @Override
    public void run() {
        while (true) {
            while (true) {
                // Get left-hand chopsticks
                synchronized (left) {
                    // Get right-hand chopsticks
                    synchronized (right) {
                        // having dinner
                        eat();
                    }
                    // Put down your right chopsticks
                }
                // Put down your left chopsticks
            }
        }
    }
}
//This code will deadlock

10-4 movable lock

Livelock occurs when two threads change each other's end conditions, and no one can end after the end

for instance:

@Slf4j
public class Test28 {
    static volatile int count = 10;
    static final Object lock = new Object();

    public static void main(String[] args) {
        new Thread(() -> {
            while (count > 0) {
                Sleeper.sleep(0.2);
                count--;
                log.debug("count:{}", count);
            }
        }, "t1").start();

        new Thread(() -> {
            while (count < 20) {
                Sleeper.sleep(0.2);
                count++;
                log.debug("count:{}", count);
            }
        }, "t2").start();
    }
}
//The sleep time of the two can be changed to make them staggered to avoid livelock

The difference between deadlock and livelock:

  • Deadlock: it is the phenomenon that threads hold the desired locks of objects and do not release them. Finally, the threads block and stop running.
  • Livelock: it is a phenomenon that the code is running all the time but can't finish running because the end condition of each other is modified between threads

10-5 hunger

Because a thread's priority is too low, it can't be scheduled by the CPU for execution and can't end. This phenomenon is hunger

11.ReentrantLock

11-1 features + Grammar

Compared with synchronized, ReentrantLock has the following characteristics:

  • Interruptible
  • You can set the timeout
  • Can be set to fair lock
  • Support multiple conditional variables (with multiple waitset s)

Like synchronized, reentrant is supported

Basic syntax:

// Acquire lock
reentrantLock.lock();
try {
    // Critical zone
} finally {
    // Release lock
    reentrantLock.unlock();
}

11-2 reentrant

  • Reentrant means that if the same thread obtains the lock for the first time, it has the right to obtain the lock again because it is the owner of the lock
  • If it is a non reentrant lock, you will be blocked by the lock the second time you obtain the lock

Lock reentry example:

@Slf4j
public class Test29 {
    private static ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {
        lock.lock();
        try {
            log.debug("enter main");
            m1();
        } finally {
            lock.unlock();
        }
    }

    public static void m1() {
        lock.lock();
        try {
            log.debug("enter m1");
            m2();
        } finally {
            lock.unlock();
        }
    }

    public static void m2() {
        lock.lock();
        try {
            log.debug("enter m2");

        } finally {
            lock.unlock();
        }
    }
}
//output
16:10:20 DEBUG [main] (Test29.java:13) - enter main
16:10:20 DEBUG [main] (Test29.java:22) - enter m1
16:10:20 DEBUG [main] (Test29.java:31) - enter m2

11-3 interruptible

If a thread is in a blocking state, you can call its interrupt method to stop blocking and fail to obtain the lock

@Slf4j
public class Test30 {
    private static ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            try {
                //If there is no contention, the method acquires the lock object lock
                //If there is competition, it enters the blocking queue and can be interrupted by other threads with the interrupt method
                log.debug("Attempt to acquire lock");
                lock.lockInterruptibly();//Interruptible lock! This method must be used, and lock() cannot be interrupted
            } catch (InterruptedException e) {
                e.printStackTrace();
                log.debug("No lock,return");//The process of waiting for the lock is interrupted
                return;
            }
            try {
                log.debug("Get lock");
            } finally {
                lock.unlock();
            }
        }, "t1");
        lock.lock();//The main thread obtains the lock
        log.debug("Got the lock");
        t1.start();
        try {
            sleep(1);
            t1.interrupt();
            log.debug("Execution interrupt");
        } finally {
            lock.unlock();
        }
    }
}
16:24:33 DEBUG [t1] (Test30.java:15) - Attempt to acquire lock
16:24:34 DEBUG [main] (Test30.java:31) - interrupt t1
16:24:34 DEBUG [t1] (Test30.java:19) - No lock,return

11-4 lock timeout

  • Using the lock.tryLock() method will return whether the lock acquisition was successful. If successful, it returns true; otherwise, it returns false.
  • And the tryLock() method can specify the waiting time. The parameters are: tryLock(long timeout, TimeUnit unit), where timeout is the longest waiting time and TimeUnit is the time unit

Improve the previous deadlock problem:

@Override
public void run() {
    while (true) {
        if (left.tryLock()) {
            try {
                if(right.tryLock()){
                    try {
                        eat();
                    }finally {
                        right.unlock();
                    }
                }
            } finally {
                left.unlock();
            }
        }
    }
}
//Need to inherit ReentrantLock
class Chopstick extends ReentrantLock {
    String name;

    public Chopstick(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "chopsticks{" + name + '}';
    }
}

11-5 fair lock

When a thread fails to acquire a lock and enters the blocking queue, the first to enter will obtain the lock after the lock is released. Such access is fair.

The default is unfair lock, which needs to be specified as fair lock when creating:

ReentrantLock lock = new ReentrantLock(true);

Fair locks are generally unnecessary and reduce concurrency

11-6 conditional variables

There are also conditional variables in synchronized, that is, the waitSet lounge when we talk about the principle. When the conditions are not met, enter the waitSet and wait

The advantage of ReentrantLock's conditional variables over synchronized is that it supports multiple conditional variables, which is like

  • synchronized means that those threads that do not meet the conditions are waiting for messages in a lounge
  • ReentrantLock supports multiple lounges, including a lounge dedicated to waiting for cigarettes and a lounge dedicated to waiting for breakfast. It also wakes up according to the lounge when waking up

Use process:

  • You need to obtain a lock before await
  • After await is executed, it will release the lock and enter the conditionObject to wait
  • await's thread is awakened (or interrupted, or timed out) to re compete for the lock lock
  • After the contention lock is successful, the execution continues after await
@Slf4j
public class Test80 {
    static final Object room = new Object();
    static boolean hasCigarette = false;//Is there any smoke
    static boolean hasTakeout = false;
    static ReentrantLock ROOM = new ReentrantLock();
    static Condition waitCigaretteSet = ROOM.newCondition();
    static Condition waitTakeoutSet = ROOM.newCondition();

    public static void main(String[] args) {
        new Thread(() -> {
            ROOM.lock();
            try {
                log.debug("Any smoke?[{}]", hasCigarette);
                while (!hasCigarette) {
                    log.debug("No smoke, take a break!");
                    try {
                        waitCigaretteSet.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("Any smoke?[{}]", hasCigarette);
                if (hasCigarette) {
                    log.debug("You can start working");
                } else {
                    log.debug("No dry survival");
                }
            } finally {
                ROOM.unlock();
            }
        }, "Xiaonan").start();
        new Thread(() -> {
            ROOM.lock();
            try {
                log.debug("Did you deliver the takeout?[{}]", hasTakeout);
                while (!hasTakeout) {
                    log.debug("No takeout, take a break!");
                    try {
                        waitTakeoutSet.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("Did you deliver the takeout?[{}]", hasTakeout);
                if (hasTakeout) {
                    log.debug("You can start working");
                } else {
                    log.debug("No dry survival..");
                }
            } finally {
                ROOM.unlock();
            }
        }, "my daughter").start();

        Sleeper.sleep(1);
        new Thread(() -> {
            ROOM.lock();
            try {
                hasTakeout = true;
                waitTakeoutSet.signal();
            } finally {
                ROOM.unlock();
            }
        }, "Delivery").start();
        Sleeper.sleep(1);
        new Thread(() -> {
            ROOM.lock();
            try {
                hasCigarette = true;
                waitCigaretteSet.signal();
            } finally {
                ROOM.unlock();
            }
        }, "Cigarette delivery").start();
    }
}

Finally, don't forget to click three times 🍭🍭🍭

Tags: Java Back-end Concurrent Programming JUC

Posted on Mon, 01 Nov 2021 19:53:16 -0400 by jainsy