preface

Multithreading (English: Multithreading) refers to the implementation of concurrent execution of multiple threads from software or hardware technology. Computers with multithreading capabilities have hardware support that allows more than one thread to execute at the same time, thereby improving overall processing performance. Systems with this capability include symmetric multiprocessors, multicore processors, and chip-level multiprocessing or simultaneous multithreading processors.

The principle is not complicated, but the implementation can not be so simple, on the Internet to see two pictures, I think to describe multithreading is very vivid, let everyone have a look

Ideal multithreading:

Multithreading in reality:

Do you think it’s very graphic?

In this article, we will first talk about the core principles of process and thread, concurrency and parallelism, as well as the creation of threads. If you want to have a deeper understanding of multithreading, I have also compiled some learning materials and interview materials about multithreading, as follows:

Pay attention to public number: North tour to learn Java, reply “630” can be received

I. Process and Thread

Process: It is a running activity of the code on the data set. It is the basic unit of the system for resource allocation and scheduling.

Thread: is an execution path of a process. There is at least one thread in a process. Multiple threads in a process share the resources of the process.

Although the system distributes resources to processes, the CPU is special and is allocated to threads, so threads are the basic unit of CPU allocation.

Relationship between the two:

There are multiple threads in a process that share the process’s heap and method area resources, but each thread has its own program counter and stack area.

Program counter: An area of memory that records the address of the current instruction to be executed by the thread.

Stack: The local variables used to store the thread, which are private to the thread, as well as the call stack frame of the thread.

Heap: The largest chunk of memory in a process. The heap is shared by all threads in the process.

Method area: This area is used to store the class, constant and static variables loaded by NM, and is also shared by threads.

The difference between the two:

Processes: have a separate address space. If a process crashes, it will not affect other processes in protected mode.

Threads: are different execution paths within a process. Threads have their own stack and local variables, but there is no separate address space between threads, the death of a thread is equal to the death of the whole process.

  • In short, a program has at least one process, and a process has at least one thread.
  • The partition scale of threads is smaller than that of processes, which makes the concurrency of multithreaded programs high.
  • In addition, the process has an independent memory unit during execution, and multiple threads share memory, thus greatly improving the running efficiency of the program.
  • Each individual thread has an entry point for the program to run, a sequential execution sequence, and an exit for the program. But the thread cannot execute independently, must depend on in the application program, by the application program provides multiple thread execution control.
  • From a logical point of view, the meaning of multithreading is that there are multiple execution parts in an application that can be executed simultaneously. However, the operating system does not treat multiple threads as multiple independent applications to implement the scheduling and management of processes and resource allocation. This is the important difference between a process and a thread

II. Concurrency and parallelism

Concurrency: Multiple tasks are executing at the same time in the same period of time, and none of them have finished executing. Concurrent tasks emphasize the simultaneous execution of a time period, and a time period is accumulated by multiple units of time, so concurrent multiple tasks may not be executed at the same time in a unit time.

Parallel: Multiple tasks are performed simultaneously per unit time.

In the practice of multithreaded programming, the number of threads is often greater than the number of CPUs, so it is generally referred to as concurrent multithreaded programming rather than parallel multithreaded programming.

Common problems during concurrency:

1, thread safety issues:

When multiple threads operate on shared variable 1 at the same time, thread 1 will update the value of shared variable 1, but other threads will get the value of shared variable before it is updated. This can lead to inaccurate data problems.

2. Invisibility of shared memory

Java memory model (dealing with shared variables)

The Java memory model states that all variables are stored in main memory. When a thread uses variables, it copies the variables in main memory into its own workspace, or working memory. When a thread reads or writes variables, it operates on the variables in its own working memory. (as shown in the picture above)

(Working Java Memory Model)

The diagram above shows a dual-core CPU system architecture. Each core has its own controller and an arithmetic unit. The controller contains a set of registers and an operation unit. Each CPU core has its own level 1 cache, and in some architectures there is a level 2 cache that is shared by all CPUs. So the working memory in the Java memory model corresponds to the LL or L2 cache or the register in the CPU here

  • Thread A first fetches the value of the shared variable X. Since both Cache levels were missed, it loads the value of X from main memory, if 0. Thread A changes the value of X to 1, writes it to the two-level Cache, and flushes it to the main memory. After thread A completes its operation, the value of X in the CPU’s two-level Cache and in main memory is l.
  • Thread B gets the value of X, first the level 1 cache is not hit, then the level 2 cache is hit, so it returns X=1; All right, so this is going to be fine, because this is also going to be X equals l in main memory. Thread B then changes the value of X to 2 and stores it in the first level Cache of Thread 2 and in the shared second level Cache of Thread 2. Finally, it updates the value of X in the main memory to 2.
  • Thread A needs to change the value of X this time, and when it gets it, it hits the first level cache, and X is equal to l and then there’s A problem. Why is thread A still getting l when thread B has changed the value of X to 2? This is the problem of memory invisibility of shared variables, which means that the value written by thread B is not visible to thread A.

Synchronized memory semantics:

This memory semantics solves the shared variable memory visibility problem. The memory semantics of entering a synchronized block are to remove a variable used in the synchronized block from the thread’s working memory so that the variable used in the synchronized block is not retrieved from the thread’s working memory, but directly from the main memory. The memory semantics of exiting a synchronized block is to flush changes to shared variables within the synchronized block to main memory. This causes the overhead of context switching, exclusive locks, and reduced concurrency

Volatile is Volatile.

This key ensures that updates to a variable are immediately visible to other threads. When a variable is declared volatile, the thread does not cache the value in registers or elsewhere when writing to the variable. Instead, it flushes the value back to main memory. When another thread reads the shared variable -, it fetches the latest value from main memory instead of using the current thread’s working memory. The memory semantics of volatile are similar to those of synchronized in that a thread writing a volatile variable value is equivalent to a thread exiting the synchronized block (synchronizing a variable value written to working memory into main memory). Reading the value of a volatile variable is equivalent to entering a synchronized block (clearing the value of the local memory variable and fetching the latest value from main memory). There is no guarantee of atomicity

Create a thread

1. Inherit Thread class

Override the run method: The advantage of using inheritance is that the currentThread can be retrieved from within the run () method using this instead of Thread.currentThread (). The downside is that Java doesn’t support multiple inheritance, so if you inherit Thread, you can’t inherit any other class. In addition, the task is not separated from the code. When multiple threads perform the same task, multiple task codes are required.

Public class threadRuning extends Thread{public threadRuning (String name){super(name); } @Override public void run() { while(true){ System.out.println("good time"); // In the run method, this represents the current thread System.out.println(this); } } public static void main(String[] args){ ThreadRuning threadRuning = new ThreadRuning("1111"); threadRuning.start(); }}

2. Implement the Runable interface

Implement the Run method: Solves the drawback of inheriting Thread by returning no value

public class RunableTest implements Runnable { @Override public void run() { while (true) { System.out.println("good time"); } } public static void main(String[] args) { RunableTest runableTest1 = new RunableTest(); RunableTest runableTest2 = new RunableTest(); new Thread(runableTest1).start(); new Thread(runableTest1).start(); new Thread(runableTest2).start(); }}

3. Implement the Callable interface

Implement the CALL method:

public class CallTest implements Callable { @Override public Object call() throws Exception { return "hello world"; } public static void main(String[] args){ FutureTask<String> futureTask = new FutureTask<String>(new CallTest()); new Thread(futureTask).start(); try { String result = futureTask.get(); System.out.println(result); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); }}}

The advantage of using inheritance is that it is easy to pass arguments. You can add member variables to a subclass, set parameters through a set method, or pass them through a constructor. If you use Runnable, you can only use variables declared final in the main thread. The downside is that Java doesn’t support multiple inheritance. If you inherit from Thread, then subclasses can’t inherit from other classes. Runable doesn’t have this restriction. Neither of the first two methods can get the result of the task, but the Callable method can

IV. Detailed explanation of THREAD class

Threading features:

A thread can be marked as a daemon thread or as a user thread

2, Each Thread is assigned a name, the default is a combination of (Thread- increment)

Each thread has a priority. High-priority threads have precedence over low-priority threads. 1-10, default is 5

4, The thread group of main is main. There is no actual specified thread group when the thread is constructed. By default, the thread group is the same as the parent thread

5. When a new thread object is created in the code of the thread’s run() method, the priority of the newly created thread is the same as that of the parent thread.

6. A newly created thread is a daemon thread if and only if the parent thread is a daemon thread.

7. When the JVM starts, there is usually a single non-daemon thread (this thread is used to call the main() method of the specified class).

The JVM will continue to execute threads until one of the following occurs:

1) The class runtime exit() method is called and the security mechanism allows this exit() method to be called.

2) All non-daemon threads have terminated, and the or run() method call returns or threw a propagable exception outside of the run() method.

The Init method:

/** * Initializes a Thread. * @Param g Thread group * @Param target execution object * @Param name Thread name * @Param stackSize new Thread stackSize, A value of 0 ignores * @Param ACC access control context for inheritance * @Param InheritThreadLocals inherits the initial value of an inheritable thread local variable from the constructor thread if the value is true */ private void init(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc, boolean inheritThreadLocals) { if (name == null) { throw new NullPointerException("name cannot be null"); } this.name = name; Thread parent = currentThread(); SecurityManager security = System.getSecurityManager(); If (g == null) {/* Determine if it's an applet or not */ * if there is a security manager, */ if (Security! = Security! = Security! = Security! = Security! = Security! = null) { g = security.getThreadGroup(); } /* If the security doesn't have a strong opinion of the matter use the parent thread group. */ If (g == null) {g = parent.getThreadGroup(); if (g == null) {g = parent.getThreadGroup(); }} /* CheckAccess regardless of whether or not ThreadGroup is explicitly passed in. */ * CheckAccess regardless of whether or not ThreadGroup is explicitly passed in. g.checkAccess(); /* * Do we have the required permissions? */ if (security ! = null) { if (isCCLOverridden(getClass())) { security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION); } } g.addUnstarted(); this.group = g; this.daemon = parent.isDaemon(); This.priority = Parent.getPriority (); this.priority = Parent.getPriority (); / / get the parent process priority if (security = = null | | isCCLOverridden (parent. GetClass ())) enclosing contextClassLoader = parent.getContextClassLoader(); else this.contextClassLoader = parent.contextClassLoader; this.inheritedAccessControlContext = acc ! = null ? acc : AccessController.getContext(); this.target = target; setPriority(priority); if (inheritThreadLocals && parent.inheritableThreadLocals ! = null) this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals); /* Stash the specified stack size in case the VM cares */ this.stackSize = stackSize; /* Set thread ID */ tid = nextThreadId (); }

Constructors: All constructors call the init() method

public Thread() {
    init(null, null, "Thread-" + nextThreadNum(), 0);
}
 
public Thread(Runnable target) {
    init(null, target, "Thread-" + nextThreadNum(), 0);
}
 
 
public Thread(Runnable target, AccessControlContext acc) {
    init(null, target, "Thread-" + nextThreadNum(), 0, acc, false);
}
 
public Thread(ThreadGroup group, Runnable target) {
    init(group, target, "Thread-" + nextThreadNum(), 0);
}
 
 
public Thread(String name) {
    init(null, null, name, 0);
}
 
 
public Thread(ThreadGroup group, String name) {
    init(group, null, name, 0);
}
 
 
public Thread(Runnable target, String name) {
    init(null, target, name, 0);
}
 
 
public Thread(ThreadGroup group, Runnable target, String name,
              long stackSize) {
    init(group, target, name, stackSize);
}

Thread state:

public enum State {
        NEW,
        RUNNABLE,
        BLOCKED,
        WAITING,
        TIMED_WAITING,
        TERMINATED;
    }

  

New: The thread has just been created and has not been started

Runnable: If the thread is running normally, there may be some kind of computation /IO wait operation /CPU time slice switch, etc. In this state, the wait is usually other system resources, not locks, sleeps, etc

Blocked: This state is used when multiple threads are engaged in a synchronous operation, such as waiting for the execution release of another thread’s synchronized block, or when a wait() method is called from within a reentrant synchronized block, in which case the thread is waiting to enter a critical section

WAITING: When a thread owns a lock, it calls its wait method and waits for another thread/lock owner to call notify/notifyAll so that the thread can proceed. It is BLOCKED and WATING that makes the difference. A “wait” in a “understand point” waits for notify. When a thread calls a join method and joins another thread, it will also enter the WAITING state and wait for the end of the thread being joined

TIMED_WAITING: This state is finite (time limited) WAITING, usually when wait(long) is called, join(long) is called, etc., and after another thread has slept, it will also be in TIMED_WAITING

TERMINATED: This state indicates that the thread’s run method has finished executing, essentially dead (at the time, the thread may not be retrieved if it is persisted).

When you write a thread while it is in execution state, then use the JConsole tool to check the state of the thread. It is a Runable state.

Here’s what the API documentation says:

In fact, we can understand it as two states, one is running, which means it is executing, the other is runable, which means it is ready, but it is waiting for other system resources. Then we can understand the following picture

The Start method:

Public synchronized void start() {/** * This method is not called by the primary method thread or the system group thread created by the virtual machine. * Any NEW function methods added to this method will be added to the virtual machine in the future. * 0 status values represent the state of NEW.  */ if (threadStatus ! = 0) / / thread cannot be repeated start throw new IllegalThreadStateException (); /* Notify the group that this thread is about to be started * so that it can be added to the group's list of threads * and the group's unstarted count can be decremented. */ group.add(this); boolean started = false; try { start0(); // local method started = true; } finally { try { if (! started) { group.threadStartFailed(this); } } catch (Throwable ignore) { /* do nothing. If start0 threw a Throwable then it will be passed up the call stack */ } } } private native void start0();

Is a local method that prompts the thread scheduler that the current thread is willing to forgo the current CPU. If the resource is not currently tight, the scheduler can ignore this hint. Essentially the thread state is always RUNNABLE, but I can interpret it as a transition from RUNNABLE to RUNNING

Methods sleep:

/** * This method will not cause the current thread to abdicate the ownership of any monitor. */ public static native void will not abdicate the ownership of any monitor sleep(long millis) throws InterruptedException; public static void sleep(long millis, int nanos) throws InterruptedException { if (millis < 0) { throw new IllegalArgumentException("timeout value is negative"); } if (nanos < 0 || nanos > 999999) { throw new IllegalArgumentException( "nanosecond timeout value out of range"); } if (nanos >= 500000 || (nanos ! = 0 && millis == 0)) { millis++; } sleep(millis); }

The sleep method frees the CPU’s time slice, but does not release the lock. After calling sleep(), the RUNNABLE state is transferred to the TIMED_WAITING state

The join method

/** * The current thread will die if it waits millis(ms) at most. NotifyAll () is called when the thread terminates. The notifyAll() method is called when the thread terminates. The notifyAll() method is called */ public final synchronized void join(Long Millis) throws InterruptedException  { long base = System.currentTimeMillis(); long now = 0; If (Millis <0) {throw new IllegalArgumentException("timeout value is negative"); } if (millis == 0) {while (isAlive()) {wait(0); } } else { while (isAlive()) { long delay = millis - now; if (delay <= 0) { break; } wait(delay); now = System.currentTimeMillis() - base; }}} public final synchronized void join(long millis, long millis) int nanos) throws InterruptedException { if (millis < 0) { throw new IllegalArgumentException("timeout value is negative"); } if (nanos < 0 || nanos > 999999) { throw new IllegalArgumentException( "nanosecond timeout value out of range"); } if (nanos >= 500000 || (nanos ! = 0 && millis == 0)) { millis++; } join(millis); Public final void join() throws InterruptedException {join(0); }

Join thread A, but not thread A, to cause thread B to be BLOCKED until it terminates or arrives at A given time

V. Other methods

Let’s talk about the wait, notify, and notifyAll methods of the Object class

Wait method

public final native void wait(long timeout) throws InterruptedException; Public final void wait(long timeout) public final void wait(long timeout) Int nanos) throws InterruptedException {if (timeout < 0) {throw new IllegalArgumentException("timeout value ") is negative"); } if (nanos < 0 || nanos > 999999) { throw new IllegalArgumentException( "nanosecond timeout value out of range"); } if (nanos > 0) { timeout++; } wait(timeout); } public final void wait() throws InterruptedException { wait(0); }

You can see that wait() and wait(long timeout, int nanos) both call wait(long timeout) internally.

The following focuses on the wait(long timeout) method

The wait method causes the current thread to block until another thread calls notify or notifyAll() on the corresponding object, or until the time specified in the method parameter has been reached.

The current thread calling wait must have the monitor lock on the object.

The wait method places the current thread T in the queue on the corresponding object. All synchronous requests on that object will not be responded to. Thread scheduling does not call thread T, which is the thread that called wait in its code, but wakes up before four things happen

1. When other threads call notify on the corresponding object, a thread will be randomly selected to wake up in the corresponding waiting queue of this object.

2. A notifyAll method is called on this object by another thread

3. Thread T is interrupted by an interrupt called by another thread

4. The wait time has exceeded the specified time in the wait. If the value of timeout is 0, it does not mean that the actual wait time is 0. It means that the thread waits until it is woken up by another thread.

The awakened thread T is removed from the object’s wait queue and can be scheduled again by the thread scheduler. Then, as usual, thread T will compete with other threads to acquire the lock on the object; Once thread T acquires the lock on this object, all synchronous requests on this object revert to their previous state, that is, to the situation in which wait was invoked. Thread T then returns from the call to the wait method. Therefore, when returning from the wait method, the state of the object and the state of thread T is the same as when the wait method was called.

Threads that can be woken without being woken, interrupted, or running out of time are called pseudo-awakenings. Although this rarely happens in practice, the program must test the condition that wakes the thread, and if the condition is not met, the thread continues to wait. In other words, the wait operation always appears inside the loop, as follows:

Synchronized (object){while(condition not met){object.wait (); } corresponding logical processing}

If the current thread is interrupted by another thread calling interrupt() before or while the current thread is waiting, then the InterruptedExCaption exception is thrown. This exception will not be thrown until the lock state on the object is restored to the state described above.

     

Note that the wait method places the current thread in the wait queue for this object, and unlocks only on this object; The current thread holds a lock on another object while the current thread is waiting.

   

This method should only be called by the thread that holds the object monitor.

   

The wait(long timeout, int nanos) method is implemented by adding one millisecond to the timeout as long as the nanos is greater than 0. This is mainly for more precise control of the timeout. Otherwise, wait(long timeout) is the same

Notify method

public final native void notify(); // local method

Notifies other threads that might be waiting on an object lock for that object. A thread in wait state is randomly selected by the JVM(regardless of priority). Before calling notify(), the thread must acquire an object-level lock for the object, and does not release the notify() lock immediately after executing the notify() method. The current thread does not release notify() until it exits the synchronized block, and only randomly notifies one thread to wake up at a time

NotifyAll () method

public final native void notifyAll(); // local method

It is similar to notify(), but makes all threads in the waiting pool waiting for the same shared resource exit from the wait state and enter the runnable state so that they compete for the lock of the object. Only the thread that acquired the lock can enter the ready state. Each lock object has two queues: ready queue and blocking queue

  • Ready queue: Stores the thread that is about to acquire the lock
  • Blocking Queue: Stores blocked threads

Six, the instance,

1, sleep.

public class ThreadDemo1 { public static void main(String[] args) { MyThread mt = new MyThread(); // Recommends myRunnable MR = new MyRunnable(); Thread t2 = new Thread(mr); mt.start(); // Start the thread t2. Start (); for (int i = 0; i < 100; i++) { System.out.println(Thread.currentThread().getName() + "-" + i); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); }}}} /** ** Class MyThread extends Thread {@Override public void run() {for (int I = 0; i < 100; i++) { if (this.isInterrupted()) { break; } System.out.println(Thread.currentThread().getName() + "-" + i); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); this.interrupt(); }}}} /** * Second way to implement threads: */ class MyRunnable implements Runnable {@Override public void run() {for (int I = 0; i < 100; i++) { System.out.println(Thread.currentThread().getName() + "-" + i); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); }}}}

2. Join and interrupt (tag interrupt is recommended)

public class ThreadDemo2 { public static void main(String[] args){ MyRunable2 mr2 = new MyRunable2(); Thread t = new Thread(mr2); // t.start(); MyRunable3 mr3 = new MyRunable3(); Thread t2 = new Thread(mr3); t2.start(); for (int i = 0; i < 50; i++) { System.out.println(Thread.currentThread().getName()+"--"+i); try { Thread.sleep(300); } catch (InterruptedException e) { e.printStackTrace(); } if(I ==20){// try {// These open to test join // t.Join (); // let t thread finish //} catch (interruptedException e) {// e.printStacktrace (); // } // t.interrupt(); // interrupt the thread. The interrupt flag is used to test the interrupt method mr3.flag = false; }}}} class MyRunable2 implements Runnable{@Override public void run() {for (int I = 0; i < 50; I++) {if (Thread interrupted ()) {/ / test interrupt state, this method will remove the interrupted status / /... break; } System.out.println(Thread.currentThread().getName()+"--"+i); try { Thread.sleep(300); } catch (InterruptedException e) { e.printStackTrace(); Thread.currentThread().interrupt(); }}}} <br>// Flag interrupt class MyRunable3 implements Runnable{public Boolean Flag = true; public MyRunable3(){ flag = true; } @Override public void run() { int i=0; while(flag){ System.out.println(Thread.currentThread().getName()+"==="+(i++)); try { Thread.sleep(300); } catch (InterruptedException e) { e.printStackTrace(); }}}}

Priorities and daemons

public class ThreadDemo3 { public static void main(String[] args){ MyRunnable4 mr4 = new MyRunnable4(); Thread t = new Thread(mr4); t.setName("Thread-t"); // Thread.max_priority (); // Thread.max_priority (); // Threads can be divided into daemon threads and user threads. The JVM will exit t.setDaemon(true) if there are no user threads in the process; Println (t.isAlive()); // Set the thread to the daemon thread System.out.println(t.isAlive()); t.start(); System.out.println(t.isAlive()); for (int i = 0; i < 50; i++) { System.out.println("main--"+i); try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } if (i==5){ Thread.yield(); }}}} class MyRunnable4 implements Runnable{@Override public void run() {for (int I = 0; i < 50; i++) { System.out.println("--"+i); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); }}}}

4, Producers and Consumers (forget which article I read). I’m sorry)

Define an interface:

package threadtest.procon;
 
public interface AbstractStorage {
    void consume(int num);
    void product(int num);
}

Define a class implementation interface to hold production stuff

package threadtest.procon; import java.util.LinkedList; /** * @author: LUGH1 * @date: 2019-7-4 * @description: */ public class Storage implements AbstractStorage{ private final int MAX_SIZE = 100; private LinkedList list = new LinkedList(); Public public void consume(int num){synchronized (list){while (list.size()<num){system.out.println (" ") :"; + num + "\t [Inventory] :"+ list.size() + "\t cannot perform consumption task!" ); try { list.wait(2000); } catch (InterruptedException e) { e.printStackTrace(); } } for(int i=0; i<num; i++){ list.remove(); } System.out.println(" [number of consumer items] :" + num + "\t ":" + list.size()); list.notifyAll(); } } @Override public void product(int num) { synchronized (list){ while(list.size()+num > MAX_SIZE){ Println (" [Number of items to produce] :" + num + "\t [Inventory] :" + list.size() + "\t Can't build yet!") ); try { list.wait(2000); } catch (InterruptedException e) { e.printStackTrace(); } } for(int i=0; i<num; i++){ list.add(new Object()); } System.out.println(" [number of items] :" + num + "\t [number of items] :" + list.size()); list.notifyAll(); }}}

Producer class:

package threadtest.procon; /** * @author: LUGH1 * @date: 2019-7-4 * @description: */ public class Producer extends Thread { private int num; public AbstractStorage abstractStorage; public Producer(AbstractStorage abstractStorage){ this.abstractStorage = abstractStorage; } public void setNum(int num) { this.num = num; } public void produce(int num){ abstractStorage.product(num); } @Override public void run() { produce(num); }}

Consumer category:

package threadtest.procon; /** * @author: LUGH1 * @date: 2019-7-4 * @description: */ public class Consumer extends Thread { private int num; public AbstractStorage abstractStorage; public Consumer(AbstractStorage abstractStorage){ this.abstractStorage = abstractStorage; } public void setNum(int num){ this.num = num; } public void consume(int num){ this.abstractStorage.consume(num); } @Override public void run() { consume(num); }}

The test class:

package threadtest.procon; /** * @author: LUGH1 * @date: 2019-7-4 * @description: */ public class Test { public static void main(String[] args){ AbstractStorage abstractStorage = new Storage(); // Producer P1 = new Producer(abstractStorage); Producer p2 = new Producer(abstractStorage); Producer p3 = new Producer(abstractStorage); Producer p4 = new Producer(abstractStorage); Producer p5 = new Producer(abstractStorage); Producer p6 = new Producer(abstractStorage); Producer p7 = new Producer(abstractStorage); // Consumer = new Consumer(abstractStorage); Consumer c2 = new Consumer(abstractStorage); Consumer c3 = new Consumer(abstractStorage); P1. SetNum (10); p2.setNum(20); p3.setNum(30); p4.setNum(40); p5.setNum(30); p6.setNum(20); p7.setNum(80); C1. SetNum (50); c2.setNum(70); c3.setNum(20); c1.start(); c2.start(); c3.start(); p1.start(); p2.start(); p3.start(); p4.start(); p5.start(); p6.start(); p7.start(); }}