Java: Multithreading (2)

Methods related to thread life cycle:

When sleep calls the sleep method, it will enter the timing waiting state. When the waiting time is up, it will enter the ready state.

Yield Calling the yield method will allow other threads to execute, but it does not ensure that it is actually yielded. Rarely used, official comments say It is rarely appropriate to use this method

join calls the join method and will wait for the thread to finish executing before executing other threads.

There was a stop method before interrupt that could interrupt the thread, but it is outdated. There is currently no way to force a thread to terminate. interrupt is used to request the termination of a thread.

The wait/notify notification mechanism can be used to implement inter-thread communication. wait represents the thread’s waiting. Calling this method will cause the thread to block until another thread calls the notify or notifyAll method to execute it.

The classic producer and consumer model is completed using the wait/notify mechanism.

What are the differences between multi-process and multi-threading? The essential difference is that each process has its own set of variables, while threads share data.

There is no way to force a thread to terminate. However, the interrupt method can be used to request termination of the thread. When the interrupt method is called on a thread, the thread’s interrupt bit will be set. This is a boolean flag that every thread has. Each thread should check this flag from time to time to determine whether the thread has been interrupted.

 Runnable r = () -> {
            try {
                // ...
                while (!Thread.currentThread().isInterrupted()) {
                    //do more work
                }
            } catch (InterruptedException e) {
                // thread was interrupted during sleep or wait
            } finally {
                // clean up, if required
            }
            // exiting the run method terminates the thread
        };

If the sleep method (or other interruptible method) is called after every iteration of work, the isInterrupted check is neither necessary nor useful. If the sleep method is called when the interrupt status is set, it will not sleep. Instead, it will clear the state and throw InterruptedException. Therefore, if your loop calls sleep, the interrupt status will not be detected. Instead, catch the InterruptedException exception as shown below.

 Runnable r = () -> {
            try {
                // ...
                while (more work to do) {
                    // do more work
                    Thread.sleep(delay);
                }
            } catch (InterruptedException e) {
                // thread was interrupted during sleep or wait
            } finally {
                // clean up, if required
            }
            // exiting the run method terminates the thread
        };

The interrupt() method in Thread sends an interrupt request to the thread. The thread’s interrupt status will be set to true. If the thread is currently blocked by a sleep call, an InterruptedException is thrown.

Once a thread starts running, it does not have to stay running. In fact, the running thread is interrupted in order to give other threads a chance to run. The details of thread scheduling depend on the services provided by the operating system. The preemptive scheduling system gives each runnable thread a time slice to perform tasks. When the time slice runs out, the operating system deprives the thread of its right to run and gives another thread a chance to run. When selecting the next thread, the operating system considers the thread’s priority.

The following is a case where the user transfers money in the bank, and a new thread is created for each transfer. If thread-safe, the bank account balance will be the same after every transfer.

This is the Bank class, which has transfer methods.

import java.util.Arrays;

public class Bank {

    private final double[] accounts;

    public Bank(int n, double initialBalance) {
        accounts = new double[n];
        Arrays.fill(accounts, initialBalance);
    }

    public void transfer(int from, int to, double amount) {
        if (accounts[from] < amount) {
            return;
        }
        System.out.println(Thread.currentThread().getName());
        accounts[from] -= amount;
        System.out.printf(" .2f from %d to %d", amount, from, to);
        accounts[to] + = amount;
        System.out.printf("Total Balance: .2f%n", getTotalBalance());
    }

    public double getTotalBalance() {
        double sum = 0;
        for (double a : accounts) {
            sum + = a;
        }
        return sum;
    }

    public int size() {
        return accounts.length;
    }

}

Test class, start multi-threading.

public class UnsynchBankTest {

    public static final int NACCOUNTS = 100;
    public static final double INITIAL_BALANCE = 1000;
    public static final double MAX_AMOUNT = 1000;
    public static final int DELAY = 10;

    public static void main(String[] args) {
        final Bank bank = new Bank(NACCOUNTS, INITIAL_BALANCE);
        for (int i = 0; i < NACCOUNTS; i + + ) {
            final int fromAccount = i;
            Runnable r = new Runnable() {
                @Override
                public void run() {
                    try {
                        while (true) {
                            int toAccount = (int) (bank.size() * Math.random());
                            double amount = MAX_AMOUNT * Math.random();
                            bank.transfer(fromAccount, toAccount, amount);
                            Thread.sleep((int) (DELAY * Math.random()));
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            };
            Thread t = new Thread(r);
            t.start();
        }
    }

}

There are a total of 100 accounts in the bank, each account is worth 1,000 yuan, and the total bank balance is 100,000 yuan. After starting the code you can see:

Thread-1
Thread-2
Thread-0
Thread-4
Thread-6
Thread-5
         790.94 from 2 to 4Thread-37
Thread-36
         979.74 from 36 to 39Thread-35
         104.57 from 35 to 37Thread-34
         358.38 from 34 to 34Thread-33
         865.67 from 33 to 14Thread-32
          96.28 from 32 to 2 Total Balance: 96794.75
Thread-31
         731.15 from 31 to 69 Total Balance: 96794.75

You can see that after running for a period of time, errors have occurred in the bank total. The problem occurs because the transfer method may be interrupted during execution. If you can ensure that the method completes before the thread loses control, the state of the bank account object will never be wrong.

There are two mechanisms to protect your code from concurrent access:

  • Keyword synchronized
  • Enable ReentrantLock class since 1.5

Start with the ReentrantLock lock.

Modify the code of Bank as follows:

public class Bank {

    // ReentrantLock implements the Lock interface
    private Lock bankLock = new ReentrantLock();

    ...

    public void transfer(int from, int to, double amount) {
        bankLock.lock();
        try {
            System.out.println(Thread.currentThread().getName());
            accounts[from] -= amount;
            System.out.printf(" .2f from %d to %d", amount, from, to);
            accounts[to] + = amount;
            System.out.printf("Total Balance: .2f%n", getTotalBalance());
        } finally {
            bankLock.unlock();
        }
    }
}

The running results are as follows.

Thread-1
         859.57 from 1 to 47 Total Balance: 100000.00
Thread-66
         771.69 from 66 to 34 Total Balance: 100000.00
Thread-2
         463.58 from 2 to 29 Total Balance: 100000.00
Thread-0
         444.86 from 0 to 93 Total Balance: 100000.00

Suppose a thread calls transfer and is deprived of execution rights before execution ends. Assume that the second thread also calls transfer. Since the second thread cannot obtain the lock, it will be blocked when calling the lock method. It must wait for the first thread to complete the execution of the transfer method before it can be activated again. When the first thread releases the lock, the second thread can start running.

Locks are reentrant because a thread can repeatedly acquire a lock that is already held. The lock maintains a hold count to track nested calls to the lock method. Every time a thread calls lock, it must call unlock to release the lock. Because of this feature, code protected by a lock can call another method that uses the same lock.

For example, the transfer method calls the getTotalBalance method, which also blocks the bankLock object. At this time, the bankLock object has a hold count of 2. When the getTotalBalance method exits, the holding count changes back to 1. When the transfer method exits, the holding count becomes 0. The thread releases the lock.

condition object

Often, a thread enters a critical section only to find that it cannot execute until a certain condition is met. Use a condition object to manage threads that have acquired a lock but are unable to do useful work.

Continuing with the above example, what should we do when there is not enough balance in the account? Wait until another thread funds the account. However, this thread has just gained exclusive access to bankLock, so other threads have no chance to perform deposit operations. That’s why we need conditional objects.

public class Bank {

    private final double[] accounts;
    // ReentrantLock implements the Lock interface
    private Lock bankLock;
    private Condition sufficientFunds;

    public Bank(int n, double initialBalance) {
        accounts = new double[n];
        Arrays.fill(accounts, initialBalance);
        bankLock = new ReentrantLock();
        sufficientFunds = bankLock.newCondition();
    }

    public void transfer(int from, int to, double amount) {
        bankLock.lock();
        try {
            while (accounts[from] < amount) {
                sufficientFunds.await();
            }
            System.out.println(Thread.currentThread().getName());
            accounts[from] -= amount;
            System.out.printf(" .2f from %d to %d", amount, from, to);
            accounts[to] + = amount;
            System.out.printf("Total Balance: .2f%n", getTotalBalance());
            sufficientFunds.signalAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            bankLock.unlock();
        }
    }
        ...
}

There is a fundamental difference between a thread waiting to acquire a lock and a thread calling the await method. Once a thread calls the await method, it enters the wait set for that condition. When the lock is available, the thread cannot be unblocked immediately. Instead, it blocks until another thread calls the signalAll method on the same condition.

When another thread transfers funds, it should call sufficientFunds.await( ); This call reactivates all threads waiting because of this condition. When these threads are removed from the wait set, they become runnable again and the scheduler activates them again. At the same time, they will attempt to re-enter the object. Once the lock becomes available, one of them will return from the await call, acquire the lock and continue execution from where it was blocked.

At this point, the thread should test the condition again. Since there is no guarantee that the condition is met, the signalAll method simply notifies the waiting thread that the condition may have been met at this time, and it is worth checking the condition again.

What is critical is that some other thread eventually needs to call the signalAll method. When a thread calls await, it has no way to reactivate itself. It’s banking on other threads. If there are no other threads to reactivate the waiting thread, it will never run again. This will lead to deadlock (deadlock) phenomenon. If all other threads are blocked and the last active thread calls the await method before unblocking the other threads, it is also blocked. No thread can unblock other threads, and the program hangs.

When should signalAll be called? As a rule of thumb, call signalAll when the object’s state is favorable for waiting for the thread’s direction to change. For example, when an account balance changes, the waiting thread should have a chance to check the balance.

To summarize the key points about locks and conditions:

  • Locks are used to protect code fragments. Only one thread can execute the protected code at any time.
  • Locks manage threads trying to enter protected code sections
  • A lock can have one or more related condition objects
  • Each condition object manages threads that have entered the protected code segment but cannot yet run.

Use of keyword synchronized

If a method is declared with the synchronized keyword, then the object’s lock will protect the entire method.

public synchronized void method(){
    // method body
}

The above is equivalent to

public void method(){
    this.intrinsicLock.lock();
    try {
        // method body
    } finally {
        this.intrinsicLock.unlock();
    }
}

For example, you could simply declare the Bank class’s transfer method to be synchronized instead of using an explicit lock.

Internal object locks have only one relevant condition. The wait method adds a thread to the waiting set, and the notifyAll/notify method unblocks the waiting thread. In other words, calling wait or notifyAll is equivalent to intrinsicCondition.await(); intrinsicCondition.signalAll();

It’s much cleaner to write code using the synchronized keyword. Of course, to understand this code, you must understand that every object has an internal lock, and that the lock has an internal condition. The threads that try to enter the synchronized method are managed by locks, and the threads that call wait are managed by conditions.

It is also legal to declare static methods as synchronized. If this method is called, the method acquires the internal lock of the associated class object. For example, if the Bank class has a statically synchronized method, then when that method is called, the lock on the Bank.class object is locked. Therefore, no other thread can call this or any other synchronized static method of the same class.

How to use locks in code?

  • It is better to use neither Lock / Condition nor synchronized keyword. In many cases you can use one of the mechanisms in the java.util.concurrent package which will handle all the locking for you
  • If the synchronized keyword is suitable for your program, then please try to use it. This can reduce the amount of code written and reduce the chance of errors.
  • Use Lock / Condition only if you specifically need the unique features provided by the Lock / Condition structure

A monitor can be thought of as a specially arranged building. This building has a special room, which usually contains some data and code, but only one consumer (thread) can use this room at a time.

When a consumer (thread) uses this room, first he must go to a lobby (Entry Set) to wait. The scheduler will select a consumer (thread) from the lobby based on certain criteria (e.g. FIFO) and enter the special room. , if this thread is suspended for some reason, it will be arranged by the scheduler to the waiting room, and will be reassigned to the special room after a period of time. According to the above line, this building contains three rooms, namely special rooms, halls and waiting rooms.

The monitor is used to monitor threads entering this special room. It ensures that only one thread can access the data and code in the special room at the same time.

In the Java virtual machine, each object Object and Class are associated with the monitor through some kind of logic. In order to realize the mutual exclusion function of the monitor, each object is associated with a lock (sometimes also called a mutex, in operating system books called a semaphore),

In order to prevent data from being accessed by multiple threads, Java provides two implementations of synchronized blocks and synchronized methods. Once a piece of code is embedded in a synchronized keyword, it means that it is placed in the monitoring area, and the JVM will automatically provide this code in the background. The code implements the lock function.

Thread local variables

Avoid shared variables and use the ThreadLocal helper class to provide each thread with its own instance.

For example, the SimpleDateFormat class is thread-unsafe. To construct an instance per thread, you can use the following code:

 private static final ThreadLocal<SimpleDateFormat> dateFormat =
            ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));

    public static void main(String[] args) {
        SimpleDateFormat simpleDateFormat = dateFormat.get();
        String dateStamp = simpleDateFormat.format(new Date());
        System.out.println(dateStamp);
    }

Where withInitial ( Supplier supplier ) creates a thread-local variable whose initial value is generated by calling the given supplier. The get ( ) method gets the current value of this thread. If get is called for the first time, initialize will be called to get this value.

refer to:

https://segmentfault.com/a/1190000014463417

You can get started with multi-threading in three minutes.

https://segmentfault.com/a/1190000014741369

https://blog.csdn.net/wthfeng/article/details/78762343

Java Core Technology Volume 1

The knowledge points of the article match the official knowledge files, and you can further learn related knowledge. Java Skill TreeHomepageOverview 137704 people are learning the system