Learning multithreading is to better optimize the program and improve the overall running efficiency of the program
Inherit Thread class
Thread class
Inherit the Thread class of Java, implement the Thread class, and override the run() method of the parent class
public class Person extends Thread { @Override public void run() { try { System.out.println(getName() + " Start withdrawing money"); Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(getName() + " Withdrawal completed"); } }
The function of thread class is to complete a relatively independent task. Here, Thread.sleep(200) is used to simulate the withdrawal process. The sleep() method is to make the thread sleep, temporarily stop executing, hand over the CPU, and let the CPU perform other tasks. The sleep method parameter is the number of milliseconds
Run thread
The thread needs to call the start() method to start
public class Bank { public static void main(String[] args) { Person thread1 = new Person(); thread1.setName("Zhang San"); Person thread2 = new Person(); thread2.setName("Li Si"); thread1.start(); thread2.start(); } }
The Thread parent class has a name attribute, but it is private, so you can call the setName() method to set the name of the Thread. You can know which Thread is running through getName()
The run() method of thread class is executed after the system calls start(). Programming does not need to call the run method, but it is impossible to know when the system calls it
Implement Runnable interface
Because Java is a single inheritance, the program scalability is greatly reduced after inheriting the Thread class, so priority is given to the implementation of java.lang.Runnable
public class Person implements Runnable { private String name; public String getName() { return name; } public void setName(String name) { this.name = name; } @Override public void run() { try { System.out.println(name + " Start withdrawing money"); Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(name + " Withdrawal completed"); } }
Whether it is the Runnable interface or the Thread class, the run() method is automatically executed by the system in a timely manner
The Thread class that implements the Runnable interface also needs to be wrapped in the instance of the Thread class
public class Bank { public static void main(String[] args) { Person person1 = new Person(); person1.setName("Zhang San"); Thread thread1 = new Thread(person1); Person person2 = new Person(); person2.setName("Li Si"); Thread thread2 = new Thread(person2); thread1.start(); thread2.start(); } }
Thread instance (new Thread(person1)) is equivalent to scheduler, which triggers thread task execution. Tasks cannot be started by themselves and need to be scheduled
Thread safety
Thread.currentThread() returns the instance object of the currently running thread.
When multiple threads operate on the same resource, a conflict occurs, which is called thread insecurity
Use the synchronized keyword in Java to solve the problem of disordered margin:
public class Ticket { public synchronized void sell() { count--; System.out.println(Thread.currentThread().getName() + ": Sell one and there's still one left " + count + " Ticket"); } }
synchronized is also called thread synchronization lock, which means that this method is locked. Only one thread can execute this method at the same time. The thread is waiting for the next lock at a time
synchronized is equivalent to protecting key methods. It is not allowed to execute at the same time. One must be executed
synchronized usage scenarios
Using the synchronized method means that the safety features of two threads are met:
- Atomicity: all methods are executed and the execution process will not be interrupted by any factors
- Visibility: when multiple threads access the same variable, one thread modifies the value of the variable, and other threads can immediately see the modified value
synchronized usage scenarios:
- Scenarios for writing operations, such as modifying personal information, liking, collecting, placing orders, etc
- Lock the smallest code block as accurately as possible, and abstract the most critical write operations into independent methods for locking
Pessimistic lock and optimistic lock
java.util.concurrent is a concurrent programming package provided by the java system
import java.util.concurrent.atomic.AtomicInteger; public class Ticket { //Set the integer of the instance to 30 private AtomicInteger count = new AtomicInteger(30); public synchronized void sell() { int newCount = 0; if (count.get() > 0) { //count.decrementAndGet() replaces count-- newCount = count.decrementAndGet(); } System.out.println(Thread.currentThread().getName() + ": Remaining " + newCount + " Ticket"); } public int getCount() { return count.get(); } }
sell() here is no longer locked, which solves the problem of duplicate ticket allowance. Although AtomicInteger is a class, it is equivalent to an integer. You can specify any integer value when calling the new AtomicInteger() constructor to instantiate the object
AtomicInteger provides a method to ensure the atomicity of data operations without using synchronized, such as decrementAndGet():
It is a combination of three operations. In the case of multithreading, there will be no error of repeated values, which ensures the correctness of the data
The subtraction of one between threads is based on the latest results, so it will not be repeated. This is the embodiment of visibility
However, because the sell method does not lock, multiple statements may be interrupted by other threads during execution, so it is necessary to add synchronized to the whole of sell() to ensure the atomicity of multiple statements as a whole
incrementAndGet() is also a method that ensures the atomicity of the operation without using synchronized:
Although increasing and decreasing are multiple steps, in a multithreaded scenario, other threads will not wait, but will judge whether other threads have modified the data when the data changes. If so, they will modify it according to the latest value, which is optimistic lock
Optimistic lock is not locked. It is always guaranteed to update based on the latest data. Because it is not locked, the performance is improved. For example, the incrementAndGet() and decrementAndGet() methods of AtomicInteger class are optimistic locks
Assuming that other threads will modify the data, be careful in advance. The idea of locking is pessimistic, so it is called pessimistic locking
difference
Optimistic lock:
It is not applicable to scenarios where multiple pieces of data need to be modified and the overall sequence of multiple operations is very strict. Optimistic lock is applicable to application scenarios with a larger proportion of read data
Pessimistic lock:
It is suitable for application scenarios with a larger proportion of write data
Concurrent container
Scenario: multiple tasks have a sequence, but subsequent tasks do not have to wait for all the preceding tasks to be executed after they are completed. Instead, after each preceding task is completed, the corresponding subsequent task is automatically executed.
In this scenario, it is suitable to use the completable future feature
CompletableFuture
When a method is called, you need to wait for the return to get the return value, which is synchronous, and continuing to execute the task without waiting is asynchronous. Asynchronous mode can support parallel execution of multiple tasks. This mechanism is called concurrency
The completabilefuture. Supplyasync () method runs an asynchronous task and returns the result, so the method inside must have a return value
The entire () - > reg. Regid (s) expression statement of the supplyAsync() method parameter is wrapped in another object, which implements the Runnable interface
CompletableFuture.supplyAsync( // Each student is registered with a student number () -> reg.regId(s) ) // After the student number is registered, print the welcome message .thenAccept(student -> { System.out.println("Hello " + student.getName() + ", Welcome to the big family of Chunlei middle school"); });
Function of supplyAsync() method: execute REG and regid (s) statements in a single thread, which is essentially multithreaded programming
After registration, use the thenAccept() method to complete the steps of subsequent tasks. The parameter (sutdent) of the thenAccept() method is the result returned by the predecessor task. The student - > {} subsequent tasks are automatically executed. The subsequent tasks are also executed in a multithreaded manner. The thenAccept() method is usually used at the end of the task chain
Multistep task
supplyAsync() is used at the beginning, thenAccept() is used at the end, and can be called once respectively. There are multiple steps in the middle, and thenApply() can be called multiple times. Since the previous instance object is also used at the end, the thenApply method in the middle always needs to return the instance. Otherwise, the next step cannot be obtained:
- Multiple tasks are parallel, and multiple threads are used to execute multiple tasks at the same time
- Multiple steps of a task are serial. The previous step must be executed before the next step can be executed
Return value
supplyAsync(),thenApply(),thenAccept() returns the completabilefuture instance object. You can set the return value:
CompletableFuture<Void> cf = CompletableFuture.supplyAsync(() -> reg.regId(s)) .thenApply(student -> { return dis.assignClasses(student); }) .thenAccept(student -> { System.out.println("full name:" + student.getName() + ",Student No.:" + student.getId() + ",Class number:" + student.getClassId()); });
The type of the defined variable is completable future. You can use the generic completable future < > to indicate the specific data type contained in it
If the end of the thenAccept() method is not called
CompletableFuture.supplyAsync(() -> reg.regId(s)) .thenApply(student -> { return dis.assignClasses(student); });
Because the thenApply()lambda expression returns a Student object, the completabilefuture instance object contains Student data, so the generic return is completabilefuture < Student >
The data type of the completable future instance returned by these methods depends on the type of the returned value of the Lambda expression
Problems with the main() method
List<CompletableFuture> cfs = new ArrayList<>(); studentList.forEach(s -> { CompletableFuture<Void> cf = CompletableFuture.supplyAsync(() -> reg.regId(s)) .thenApply(student -> { return dis.assignClasses(student); }).thenAccept(student -> { System.out.println("full name:" + student.getName() + ",Student No.:" + student.getId() + ",Class number:" + student.getClassId()); }); cfs.add(cf); }); try { // Wait for all threads to complete execution CompletableFuture.allOf(cfs.toArray(new CompletableFuture[] {})).get(); } catch (Exception e) { e.printStackTrace(); }
Completabilefuture. allOf() is a static method used to collect all task instance objects. allOf() method only supports arrays and does not support collections. Therefore, to convert cfs.toarray (New completabilefuture [] {}), and then call the class method get(), the function is to wait for all task threads (allOf() Collection) to execute
When servers such as SpringBoot run supplyAsync() asynchronous task choreography, it is not necessary to use the get() method to wait for all thread tasks to be executed. The server is often a resident program, rather than exiting the program after the main() method is executed
The get() method causes the main() method to wait, so it is synchronous. The tasks arranged through completable future will not cause the main() method to wait, which is asynchronous
Wrapper class for security Booleans
java.util.concurrent.atomic.AtomicBoolean can manipulate Boolean values atomically
- new AtomicBooolean(true) is true
- new AtomicBooolean(false) is false
Get Boolean value using get() method:
AtomicBoolean ab = new AtomicBoolean(true); boolean value = ab.get();
The instance object can modify the value atomically by calling the compareAndSet() method
compareAndSet(true,false) determines that the current value is true, modifies it to false, and then returns success or failure. These are three steps
- If the modification is successful, return true
- If modification fails, false is returned
The return value of compareAndSet() method is independent of the parameter value
Thread pool
In order to optimize the reuse of Thread objects, it is not necessary to create new objects every time. You can use java thread pool
As the name suggests, a thread pool is like a pool full of threads. Threads can be reused. A thread can execute a task or B task. Threads do not have to be created and destroyed frequently
The thread pool is also limited, the number of threads is also limited, and the number of tasks that can run at the same time is also limited
Core code for creating thread pool:
import org.apache.commons.lang3.concurrent.BasicThreadFactory; import java.util.concurrent.*; public class StudentIDTest { // Thread factory private static final ThreadFactory namedThreadFactory = new BasicThreadFactory.Builder() .namingPattern("studentReg-pool-%d") .daemon(true) .build(); // Waiting queue private static final BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<Runnable>(1024); // Thread pool service private static final ThreadPoolExecutor EXECUTOR_SERVICE = new ThreadPoolExecutor( 20, 200, 30, TimeUnit.SECONDS, workQueue, namedThreadFactory, new ThreadPoolExecutor.AbortPolicy() ); public static void main(String[] args) { } }
The BasicThreadFactory here requires a dependent Library:
<dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.10</version> </dependency>
Creating a thread pool is a fixed writing method
Create thread factory
new BasicThreadFactory.Builder() .namingPattern("studentReg-pool-%d") .daemon(true) .build();
The namingPattern() method defines the format of the thread name, which is equivalent to the thread name template. The studentReg here is the task name
Create thread wait queue instance
The performance is good. There are many CPU cores, and the memory queue can be larger: the new linkedblockingqueue < runnable > (2048) parameter indicates the number of tasks that can be queued
Queues with fewer CPU s are smaller: new linkedblockingqueue < runnable > (512)
Create thread pool instance
Multithreaded programming can adopt the idea of batch processing to prevent the system from crashing due to too many threads
Execute the execute() method of the thread pool object and pass in the instance object that implements the Runnable interface
// Pass in the Runnable object to run the task EXECUTOR_SERVICE.execute(register);
Thread pool and concurrency container
Completable future also uses a thread pool internally. In fact, it puts tasks into the internal default thread pool. Similarly, completable future can also specify a thread pool to run tasks:
CompletableFuture.supplyAsync( () -> reg.regId(s), EXECUTOR_SERVICE )
The supplyAsync() method can have a second parameter passed into the thread pool object built by itself
Specifies the scenario for the thread pool
Generally, the default thread pool of completable future is used. When the task execution is slow and the task accumulates, it is necessary to specify the thread pool and adjust the thread pool parameters