Detail java thread to thread process to process communication

  • 2020-06-19 10:22:52
  • OfStack

Thread to thread communication

1. Basic concepts and the differences between threads and processes:

The first thing to understand about processes and threads is by definition

1. What is a process?

It is a program with a definite independent function. It is an independent unit of the system for resource allocation and scheduling. The focus is on system scheduling and separate units.

2. What are threads?

The entity of the thread process is the basic unit of CPU scheduling and dispatching. It is a basic unit smaller than the process that can run independently. The thread itself basically has no system resources.

At run time, just temporarily use 1 counter, register and stack.

The relationship between them

1. A thread can only belong to one process, and a process can have multiple threads, but at least one thread (commonly referred to as the main thread).

2. Resources are allocated to the process, and all resources of the process are Shared with all threads of the process 1.

3, the thread in the execution process, need to cooperate with synchronization. Message communication is used to synchronize threads of different processes.

4, the processor allocated to the thread, that is, the real running in the processor is the thread.

5. Thread refers to a unit of execution within the process, which is also a schedulable entity within the process.

Analyze the differences between the two from three perspectives

1. Scheduling: The thread is the basic unit of scheduling and allocation, and the process is the basic unit of owning resources.

2. Concurrency: Concurrent execution can be performed not only between processes, but also between multiple threads of a process.

3. Resource ownership: The process is an independent unit that owns the resources. The thread does not own the system resources, but can access the resources belonging to the process. .

2. Communication mode between multiple threads:

1. Share variables

2. wait/notify mechanism

3. Lock/Condition mechanism

4, pipes,

3. Share variables

A simple way to send signals between threads is to set the signal value in a variable of the Shared object. Thread A sets the boolean member variable hasDataToProcess to true in one block, and thread B also reads the member variable hasDataToProcess in the block. This simple example USES an object holding a signal and provides the set and check methods:



public class MySignal{

 protected boolean hasDataToProcess = false;

 public synchronized boolean hasDataToProcess(){
  return this.hasDataToProcess;
 }

 public synchronized void setHasDataToProcess(boolean hasData){
  this.hasDataToProcess = hasData;
 }

}

Threads A and B must obtain a reference to a Shared instance of MySignal in order to communicate. If they hold references to different instances of MySingal, they will not be able to detect each other's signals. The data that needs to be processed can be stored in a Shared cache that is separate from the MySignal instance.

4. wait ()/notify mechanism

To achieve thread communication, we can use the wait(), notify(), and notifyAll() methods provided by the Object class. Calling the wait() method releases the lock on the synchronization monitor. These three methods must be called by the synchronization monitor object, which can be divided into two cases:

The & # 8226; For synchronization methods decorated with synchronized, because the default instance of the class (this) is the synchronization monitor, these three methods can be called directly.

The & # 8226; For the synchronization block decorated by synchronized, the synchronization monitor is an object in the synchronized parentheses, so you must use that object to call all three methods.

Suppose there are two threads in the system, representing the withdrawer and the depositor, respectively. The current system has a special requirement, which requires the depositor and the withdrawer to make deposits and withdrawals continuously, and requires the withdrawer to withdraw the money immediately after depositing the money into the designated account. The depositor is not allowed to make two deposits and the withdrawer is not allowed to make two withdrawals.

We set a flag to indicate whether there is any deposit in the account. If there is, it is true; if there is not, it is false. The specific code is as follows:

First, we define an Account class that has two methods for withdrawing and saving money. Since these methods may need to perform both withdrawals and saving operations concurrently, we modify both methods to be synchronous (using the synchronized keyword).


public class Account { 
  private String accountNo; 
  private double balance; 
  // A flag indicating whether there is money in an account  
  private boolean flag=false; 
   
  public Account() { 
    super(); 
  } 
 
  public Account(String accountNo, double balance) { 
    super(); 
    this.accountNo = accountNo; 
    this.balance = balance; 
  }  
   
  public synchronized void draw (double drawAmount){ 
     
    try { 
       if(!flag){ 
       this.wait(); 
       }else { 
         // To withdraw money  
         System.out.println(Thread.currentThread().getName()+"  To withdraw money :"+drawAmount); 
         balance=balance-drawAmount; 
         System.out.println(" The balance of  : "+balance); 
         // Set as the mark indicating whether the account has been deposited false 
         flag=false; 
         // Wake up other threads  
         this.notifyAll();     
       } 
      } catch (Exception e) { 
        e.printStackTrace(); 
    } 
  } 
   
   
  public synchronized void deposit(double depositAmount){ 
   try { 
       if(flag){ 
        this.wait(); 
       } 
       else{ 
         System.out.println(Thread.currentThread().getName()+" To save money "+depositAmount); 
         balance=balance+depositAmount; 
         System.out.println(" The account balance is: "+balance); 
         flag=true; 
         // Wake up other threads  
         this.notifyAll(); 
       } 
    } catch (Exception e) { 
      // TODO: handle exception 
      e.printStackTrace(); 
    } 
  } 
 
} 

Next, create two thread classes, one for the draw and one for the save thread!

Withdrawal thread class:


public class DrawThread implements Runnable { 
 
  private Account account; 
  private double drawAmount; 
   
   
  public DrawThread(Account account, double drawAmount) { 
    super(); 
    this.account = account; 
    this.drawAmount = drawAmount; 
  } 
 
  public void run() { 
    for(int i=0;i<100;i++){ 
     account.draw(drawAmount);   
    } 
  } 
} 

Saving thread class:


public class depositThread implements Runnable{ 
  private Account account; 
  private double depositAmount; 
    
  public depositThread(Account account, double depositAmount) { 
    super(); 
    this.account = account; 
    this.depositAmount = depositAmount; 
  } 
 
 
  public void run() { 
  for(int i=0;i<100;i++){ 
     account.deposit(depositAmount); 
   } 
  } 
 
} 

Finally, let's test 1 on this withdrawal and deposit operation


public class TestDraw { 
 
  public static void main(String[] args) { 
    // create 1 An account  
    Account account=new Account(); 
    new Thread(new DrawThread(account, 800)," Who take money ").start(); 
    new Thread(new depositThread(account, 800)," Depositors armour ").start(); 
    new Thread(new depositThread(account, 800)," Depositors b ").start(); 
    new Thread(new depositThread(account, 800)," Depositors c ").start(); 
 
  } 
 
} 

General output results:


 Depositor A deposits money 800.0 
 The account balance is: 800.0 
 Who take money   To withdraw money :800.0 
 The balance of  : 0.0 
 Depositor C deposits 800.0 
 The account balance is: 800.0 
 Who take money   To withdraw money :800.0 
 The balance of  : 0.0 
 Depositor A deposits money 800.0 
 The account balance is: 800.0 
 Who take money   To withdraw money :800.0 
 The balance of  : 0.0 
 Depositor C deposits 800.0 
 The account balance is: 800.0 
 Who take money   To withdraw money :800.0 
 The balance of  : 0.0 
 Depositor A deposits money 800.0 
 The account balance is: 800.0 
 Who take money   To withdraw money :800.0 
 The balance of  : 0.0 
 Depositor C deposits 800.0 
 The account balance is: 800.0 
 Who take money   To withdraw money :800.0 
 The balance of  : 0.0 
 Depositor A deposits money 800.0 
 The account balance is: 800.0 
 Who take money   To withdraw money :800.0 
 The balance of  : 0.0 
 Depositor C deposits 800.0 
 The account balance is: 800.0 
 Who take money   To withdraw money :800.0 
 The balance of  : 0.0 
 Depositor A deposits money 800.0 
 The account balance is: 800.0 
 Who take money   To withdraw money :800.0 
 The balance of  : 0.0 

5. Lock/Condition mechanism

If the program does not use the synchronized keyword to maintain synchronization, but directly applies the Lock image to maintain synchronization, then there is no implicit synchronization monitor object in the system and wait(), notify(), notifyAll() cannot be used to coordinate threads.

JAVA provides us with the Condition class to coordinate the running of threads while using LOCK objects for synchronization. The Condition class is explained in detail in the JDK documentation.

Let's take the Account class and make a slight change.


import java.util.concurrent.locks.Condition; 
import java.util.concurrent.locks.Lock; 
import java.util.concurrent.locks.ReentrantLock; 
 
public class Account { 
   
  // According to defined Lock object  
  private final Lock lock=new ReentrantLock(); 
  // Get the specified Lock The conditional variable corresponding to the  
  private final Condition con=lock.newCondition();   
 
  private String accountNo; 
  private double balance; 
  // A flag indicating whether there is money in an account  
  private boolean flag=false; 
   
  public Account() { 
    super(); 
  } 
 
  public Account(String accountNo, double balance) { 
    super(); 
    this.accountNo = accountNo; 
    this.balance = balance; 
  }  
   
  public void draw (double drawAmount){ 
     
    // lock  
    lock.lock(); 
    try { 
       if(!flag){ 
//      this.wait(); 
       con.await(); 
       }else { 
         // To withdraw money  
         System.out.println(Thread.currentThread().getName()+"  To withdraw money :"+drawAmount); 
         balance=balance-drawAmount; 
         System.out.println(" The balance of  : "+balance); 
         // Set as the mark indicating whether the account has been deposited false 
         flag=false; 
         // Wake up other threads  
//        this.notifyAll();  
         con.signalAll(); 
       } 
      } catch (Exception e) { 
        e.printStackTrace(); 
    } 
      finally{ 
        lock.unlock(); 
      } 
  } 
   
   
  public void deposit(double depositAmount){ 
    // lock  
    lock.lock(); 
    try { 
       if(flag){ 
//       this.wait(); 
         con.await(); 
       } 
       else{ 
         System.out.println(Thread.currentThread().getName()+" To save money "+depositAmount); 
         balance=balance+depositAmount; 
         System.out.println(" The account balance is: "+balance); 
         flag=true; 
         // Wake up other threads  
//        this.notifyAll(); 
         con.signalAll(); 
       } 
    } catch (Exception e) { 
      // TODO: handle exception 
      e.printStackTrace(); 
    }finally{ 
      lock.unlock(); 
    } 
  } 
 
} 

The output is exactly the same as above! It's just that the Lock image shown here ACTS as a synchronization monitor, using the Condition object to pause the specified thread and wake up the specified thread!

6. The pipe

Pipeline flow is one of the common methods of thread communication in JAVA. The basic process is as follows:

1) Create pipe output stream PipedOutputStream pos and pipe input stream PipedInputStream pis

2) Match pos and pis, ES158en. connect(pis);

3) Assign pos to the information input thread and pis to the information acquisition thread, and the communication between threads can be realized


import java.io.IOException;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;

public class testPipeConnection {

  public static void main(String[] args) {
    /**
     *  Create a pipe output stream 
     */
    PipedOutputStream pos = new PipedOutputStream();
    /**
     *  Create a pipe input stream 
     */
    PipedInputStream pis = new PipedInputStream();
    try {
      /**
       *  Connect the pipe input stream to the output stream   This can also be done with overloaded constructors 
       */
      pos.connect(pis);
    } catch (IOException e) {
      e.printStackTrace();
    }
    /**
     *  Create producer threads 
     */
    Producer p = new Producer(pos);
    /**
     *  Create a consumer thread 
     */
    Consumer1 c1 = new Consumer1(pis);
    /**
     *  Starting a thread 
     */
    p.start();
    c1.start();
  }
}

/**
 *  Producer thread ( with 1 Associated with a pipeline input stream )
 * 
 */
class Producer extends Thread {
  private PipedOutputStream pos;

  public Producer(PipedOutputStream pos) {
    this.pos = pos;
  }

  public void run() {
    int i = 0;
    try {
      while(true)
      {
      this.sleep(3000);
      pos.write(i);
      i++;
      }
    } catch (Exception e) {
      e.printStackTrace();
    }
  }
}

/**
 *  Consumer thread ( with 1 Associated with a pipeline input stream )
 * 
 */
class Consumer1 extends Thread {
  private PipedInputStream pis;

  public Consumer1(PipedInputStream pis) {
    this.pis = pis;
  }

  public void run() {
    try {
      while(true)
      {
      System.out.println("consumer1:"+pis.read());
      }
    } catch (IOException e) {
      e.printStackTrace();
    }
  }
}

Once the program starts, you can see the producer thread sending data to the consumer1 thread


consumer1:0
consumer1:1
consumer1:2
consumer1:3
......

Although the pipe flow is convenient to use, it also has some disadvantages

1) A pipe flow can only pass data between two threads

Threads consumer1 and consumer2 simultaneously obtain read data from pis. When thread producer writes a piece of data to the pipeline flow, only one thread can get the data at every moment. Not both threads can get the data sent by producer, so one pipeline flow can only be used for communication between two threads. Not only pipeline flow, other IO methods are one-to-one transmission.

2) Pipeline flow can only realize one-way sending. If communication between two threads is needed, two pipeline flows are needed

As you can see from the above example, thread producer sends data through the pipe to thread consumer. If thread consumer wants to send data to thread producer, it needs to create another pipe flow pos1 and pis1, and assign pos1 to consumer1 and pis1 to producer, which will not be discussed in this article.

II. Process to process communication

1. Inter-process communication mode

(1) Pipes (Pipe) : Pipes can be used for communication between related processes, allowing communication between a process and another process with which it shares a common ancestor.

(2) Named pipelines (named pipe) : Named pipelines overcome the limitation that pipelines have no names, and therefore, in addition to having the functions of pipelines, it allows communication between unrelated processes. Named pipes have filenames in the file system. Named pipes are created with the command mkfifo or the system call mkfifo.

(3) Signal (Signal) : Signal is a relatively complex communication mode, which is used to inform the receiving process of the occurrence of certain events. In addition to inter-process communication, the process can also send signals to the process itself. linux not only supports the early signal semantic function sigal of Unix, but also supports the signal function sigaction which conforms to the standard of ES218en.1 (actually, this function is based on BSD, BSD reimplements the signal function in order to realize the reliable signal mechanism and unify the external interface of 1).

(4) Message (Message) queue: A message queue is a linked table of messages, including Posix message queue system V message queue. A process with sufficient permissions can add messages to the queue, and a process with read permissions can read messages from the queue. Message queue overcomes the shortage of signal bearing information, channel bearing only unformatted byte stream and buffer size limitation

(5) Shared memory: Enables multiple processes to access the same block of memory space, which is the fastest available FORM of IPC. It is designed for the low efficiency of other communication mechanisms. Often used in conjunction with other communication mechanisms, such as semaphores, to achieve interprocess synchronization and mutual exclusion.

(6) Memory mapping (mapped memory) : Memory mapping allows any number of processes to communicate with each other, and each process using this mechanism realizes it by mapping a Shared file to its own process address space.

(7) Semaphore (semaphore) : Mainly used as a means of synchronization between processes and between different threads of the same process.

(8) Set of interfaces (Socket) : more 1-like inter-process communication mechanism, which can be used for inter-process communication between different machines. Originally developed by the BSD branch of Unix, it is now generally portable to other Unix classes: both Linux and System V variants support sockets.


Related articles: