preface

Multithreading is a technique that enables concurrent execution of multiple threads from software or hardware. Multithreaded computers have hardware that allows them to execute more than one thread at a time, improving overall processing performance. Systems with this capability include symmetric multiprocessors, multicore processors, and chip-level multiprocessors or simultaneous multithreading processors.

The principle is not complex, but the realization can not be so simple, see two pictures on the Internet, I think to describe multithreading is very vivid, let everyone see

Ideal multithreading:

Reality multithreading:

Do you think it looks good?

Space is limited, this article we first talk about process and thread, concurrency and parallel core principle and the creation of thread, if you want to have a deeper understanding of multithreading, I also sorted out some multithreading learning materials and interview materials, you can click here to get: multithreading data portal

Processes and threads

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

Thread: the 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 allocates resources to processes, cpus are specifically 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 heap and method area resources of the process, but each thread has its own program counter and stack area.

Program counter: An area of memory that records the address of the instruction currently being executed by the thread.

Stack: Used to store local variables of the thread, which are private to the thread, as well as the call 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: is used to store NM loaded classes, constants, static variables and other information, is also shared by threads.

Differences between the two:

A process has an independent address space. The crash of one process in protected mode does not affect other processes.

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, and the death of one thread equals the death of the entire process.

  • In short, a program has at least one process, and a process has at least one thread.

  • The scale of thread partition is smaller than that of process, which makes the concurrency of multithreaded program high.

  • In addition, the process has an independent memory unit during execution, while multiple threads share the memory, thus greatly improving the efficiency of the program.

  • Each independent thread has an entry point for program execution, a sequential execution sequence, and an exit point for the program. However, threads cannot execute independently and must depend on the application, which provides multiple thread execution control.

  • From a logical point of view, the meaning of multithreading is that multiple parts of an application can be executed simultaneously. However, the operating system does not treat multiple threads as multiple independent applications to achieve process scheduling and management and resource allocation. This is an important difference between a process and a thread

Concurrency and parallelism

Concurrent: Indicates that multiple tasks are being executed at the same time in the same period. Concurrent tasks emphasize that they are executed simultaneously in a time period, and a time period is accumulated by multiple units of time. Therefore, concurrent tasks may not be executed simultaneously in a unit of time.

Parallelism: Refers to the simultaneous execution of multiple tasks in a unit of time.

In multithreaded programming practice, the number of threads often exceeds the number of cpus, so it is generally called multithreaded concurrent programming rather than multithreaded parallel programming.

Common problems with concurrency:

1. Thread safety:

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 was updated. It can lead to data inaccuracies.

2. Shared memory is invisible

Java Memory model (handling shared variables)

According to the Java memory model, all variables are stored in main memory. When a thread uses variables, it copies the variables in main memory to its own workspace, or working memory. When a thread reads or writes variables, it manipulates the variables in its own working memory. (As shown in the picture above)

(Actual working Java memory model)

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

  • Thread A first fetches the value of shared variable X, and since both levels of Cache miss, loads the value of X in main memory, assuming it is 0. It then caches the value of X=0 to the two-level Cache. Thread A modifies X to 1, writes it to the two-level Cache, and flushes it to main memory. After thread A completes the operation, the value of X in the Cache level and main memory of the CPU where thread A resides is L.

  • Thread B gets the value of X, first level 1 cache missed, then look at level 2 cache, level 2 cache hit, so return X=1; Everything is fine up here, because we have X is equal to L in main memory. Thread B then changes the value of X to 2, stores it in thread 2’s level 1 Cache and shared level 2 Cache, and finally updates the value of X in main memory to 2, so everything is fine.

  • This time thread A needs to change the value of X again. The cache hit level 1 and the value of X is equal to L, so thread A needs to change the value of X to 2. This is the memory invisibility of shared variables, where values written by thread B are not visible to thread A.

Synchronized memory semantics:

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

Volatile:

This keyword 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, the value is flushed back to main memory. When another thread reads the shared variable -, it retrieves the latest value from main memory instead of using the value in the current thread’s working memory. The memory semantics of volatile are similar to synchronized in that writing a volatile variable value is equivalent to exiting a synchronized block. Reading volatile values is equivalent to entering a synchronized block (emptying local memory values before fetching the latest values from main memory). Atomicity is not guaranteed

Create a thread

Inherit the Thread class

Overriding the run method: the advantage of using inheritance is that we can get the currentThread from the run () method using this instead of thread.currentthread (); The downside is that Java does not support multiple inheritance. If you inherit from Thread, you cannot inherit from other classes. In addition, there is no separation between the task and the code. When multiple threads perform the same task, multiple copies of the task code are required.

Public class ThreadRuning extends Thread{public ThreadRuning(String name){public class ThreadRuning extends Thread{public ThreadRuning(String 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(); }}Copy the code

2. Implement Runable interface

Implement the run method: To solve the problem of inheriting Thread, there is no return 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(); }}Copy the code

3. Implement 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(); }}}Copy the code

The advantage of using inheritance is that it is easy to pass parameters. You can add variables to subclasses, set parameters through the set method, or pass them through constructors. With Runnable, you can only use variables declared final in the main thread. The downside is that Java does not support multiple inheritance. If Thread is inherited, subclasses cannot inherit from other classes, whereas Runable does not. The first two methods do not get the result of the task, but Callable does

“Thread”; “Thread”

Thread features:

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

2. Each Thread is assigned a name. The default name is a combination of Thread and increment number

3. Each thread has a priority. High-priority threads execute before low-priority threads. 1-10, default is 5

There is no actual thread group specified when constructing a thread. The default thread group is the same as the parent thread

5. When a new thread object is created in the run() method, the priority of the new 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 that calls the main() method of the specified class.

The JVM continues executing threads until one of the following occurs:

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

2) All non-daemon threads have terminated, and the or run() method call returns or throws some propagable exceptions outside the run() method.

The Init method:

/** * initializing a Thread. * @param g Thread group * @param target That executes the object * @param name Name of the Thread * @param stackSize The size of the new Thread stack, A value of 0 means that the access control context used for inheritance is ignored * @param acc * @param inheritThreadLocals if true inherits the initial value of the inheritable thread-local variable from the constructor thread */ 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, Ask the security manager what to do. */ if (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(); }} /* checkAccess regardless of whether or not threadgroup is passed in. */ / checkAccess is performed regardless of whether the owning threadgroup shows incoming. 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(); // If the parent thread is a daemone. 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 Set thread ID */ tid = nextThreadID(); }Copy the code

Constructor: 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);
}
Copy the code

Thread status:

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

  

NEW: Indicates that the thread has just been created and has not been started

RUNNABLE: Indicates that the thread is running normally, but there may be some operation that takes time to compute/wait for I/O /CPU time slices, etc. Waits in this state are usually for other system resources, but not for locks, sleeps, etc

BLOCKED: This state is used when multiple threads have synchronized operations, such as a synchronized block that is awaiting release from another thread, or a reentrant synchronized block that calls wait(), where the thread is waiting to enter a critical section

WAITING: When a thread holds a lock, it calls its wait method and waits for notify/notifyAll to be called by another thread/lock owner. The thread is BLOCKED and WATING. One is WAITING for entry outside the critical point, the other is WAITING for notify inside the understanding point. When the thread calls the join method and joins another thread, it will also enter the WAITING state, WAITING for the completion of the thread joined by it

TIMED_WAITING: This state is a finite (time-limited) WAITING state. It usually occurs when wait(long), join(long) is called, and another thread is in TIMED_WAITING state after sleep

TERMINATED: this state indicates that the thread’s run method is TERMINATED and is essentially dead (at the time the thread may not be recycled if it is persisted)

(In many articles write running state, in fact there are only six sources of source code, when write a thread to keep the state of execution while, then use the jConsole tool to check the thread state, it is Runable state.)

Here’s what the Api documentation says:

In fact, there are two states: running, indicating that the system is executing, and runable, indicating that the system is ready, but 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 main method thread or 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.  */ 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();Copy the code

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

Methods sleep:

/** * This method will cause the current thread to sleep for a specified number of milliseconds. */ This method will not cause the current thread to give up 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); }Copy the code

The sleep method, which is overloaded, frees the CPU’s timeslapped but does not release the lock, and goes from RUNNABLE to TIMED_WAITING after calling sleep()

* * * * the join method

/** * The thread will die if it waits for the millis(ms) parameter at most. When the thread terminates, a notifyAll() method is called. * notifyAll() is called when the thread terminates Applications are advised not to use wait,notify, and notifyAll methods on thread instances. */ 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, synchronized) public final synchronized void join(long millis, synchronized) 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); } // Wait until the thread dies. Public final void join() throws InterruptedException {join(0); }Copy the code

Joining thread A causes thread B to wait until thread A finishes or arrives at A given time, during which thread B is BLOCKED instead of thread A

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, 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); }Copy the code

Wait () and wait(long timeout, int nanos) both call the wait(long timeout) method internally.

Wait (long timeout

Wait 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 parameters is reached.

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

The wait method places the current thread T in a wait queue on the corresponding object, and all synchronous requests on this object are not responded to. Thread scheduling will not call thread T, and thread T will wake up until four things happen (thread T is the thread that called wait in its code).

1. When another thread calls notify on the corresponding object, any thread in the corresponding queue will be selected to wake up the object.

2. Other threads call notifyAll on this object

Other threads call the interrupt method to interrupt thread T

4. The wait time has exceeded the time specified in wait. If the timeout parameter is 0, it does not mean that the actual waiting time is 0, but 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. After that, thread T will compete with other threads to acquire the lock on the object as usual; Once thread T acquires the lock on this object, all synchronous requests on this object revert to their previous state, i.e., to when wait was called. Thread T then returns from the call of the wait method. Therefore, when the object is returned from the wait method, the state of the object and thread T is the same as when the wait method was called.

A thread can be woken up without being woken up, interrupted, or running out of time. This is called a pseudo-wake. Although in practice this rarely happens, the program must test for the condition that wakes the thread, and if the condition is not met, the thread continues to wait. In other words, a wait operation always occurs in a loop, as follows:

Synchronized (object){while(condition not satisfied){object.wait (); } The corresponding logical processing}Copy the code

If the current thread is interrupted by another thread calling interrupt() before or while the current thread is waiting, it raises InterruptedExcaption. 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 of the object and unlocks only the object. A lock on another object held by the current thread holds the lock on the other object while the current thread waits.

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

The implementation of wait(long timeout, int nanos) adds a millisecond to the timeout whenever nanos is greater than 0

Notify method

public final native void notify(); // Local methodCopy the code

Notifies other threads that may be waiting on the 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 the object-level lock of the object. The lock is not released immediately after notify() is executed, and the current thread does not release the lock until it exits the synchronized block, notifying only one thread at a time at random to wake up

NotifyAll () method

public final native void notifyAll(); // Local methodCopy the code

This is similar to notify(), except that all the 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 on 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 will 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(); MyRunnable Mr = new MyRunnable(); Thread t2 = new Thread(mr); mt.start(); // Start 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(); }}}} /** * 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: */ 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(); }}}}Copy the code

2. Join and interrupt (mark interrupt 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(); // catch (InterruptedException e) {// e.printStackTrace(); // } // t.interrupt(); 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>// 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(); }}}}Copy the code

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"); // A Thread with a higher priority has a higher probability of capturing CPU time slices. // Threads can be divided into daemons and user threads. When there are no user threads in the process, the JVM exits. 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(); }}}}Copy the code

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);
}
Copy the code

Define a class implementation interface for storing 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(); @override public void consume(int num){synchronized (list){while (list.size()<num){system.out.println (" + num + "\t [inventory] :"+ list.size() + "\t" ); try { list.wait(2000); } catch (InterruptedException e) { e.printStackTrace(); } } for(int i=0; i<num; i++){ list.remove(); } system.out. println(" num + "\t :" + list.size()); list.notifyAll(); } } @Override public void product(int num) { synchronized (list){ while(list.size()+num > MAX_SIZE){ System.out.println(" num + "\t [inventory] :" + list.size() + "\t" ); try { list.wait(2000); } catch (InterruptedException e) { e.printStackTrace(); } } for(int i=0; i<num; i++){ list.add(new Object()); } system.out. println(" num + "\t :" + list.size());} system.out. println(" num + "\t :" + list.size()); list.notifyAll(); }}}Copy the code

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); }}Copy the code

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); }}Copy the code

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 c1 = 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(); }}Copy the code