什么是进程和线程?

  • 进程(Process) 是指计算机中正在运行的一个程序实例。举例:你打开的微信就是一个进程。
  • 线程(Thread) 也被称为轻量级进程,更加轻量。多个线程可以在同一个进程中同时执行,并且共享进程的资源比如内存空间、文件句柄、网络连接等。举例:你打开的微信里就有一个线程专门用来拉取别人发你的最新的消息。
  • 一个进程中可以有多个线程,多个线程共享进程的堆和方法区 (JDK1.8 之后的元空间)资源,但是每个线程有自己的程序计数器、虚拟机栈 和 本地方法栈。
  • 进程是分配资源的最小单位,线程是执行调度的最小单位。

进程间通信

  • 定义:进程间通信是指在不同进程之间传递数据或信号的能力。由于进程通常拥有独立的内存空间,它们不能直接访问对方的内存,因此需要操作系统提供特殊的机制来实现数据交换。

  • 目的:允许运行在不同进程中的应用程序部分能够协作。

  • **机制:**包括但不限于以下几种:

    • 匿名管道(Unnamed Pipes):允许具有亲缘关系的进程(如父子进程)进行通信。
    • 命名管道(FIFOs):允许任意两个进程通信,不要求它们具有亲缘关系。
    • 消息队列(Message Queues):允许进程以消息的形式发送和接收数据。
    • 信号(Signals):用于通知接收进程某个事件已经发生。
    • 共享内存(Shared Memory):允许多个进程访问同一块内存区域,是最快的IPC方式,但需要同步机制来避免冲突。
    • 套接字(Sockets):提供了在不同机器上运行的进程之间进行数据交换的机制。
    • 远程过程调用(RPC):允许一个进程调用另一个地址空间(通常是远程计算机上)的进程中的函数或过程。
    • 内存映射文件(Memory-mapped Files):将文件或其他对象映射到内存,以便可以像访问内存一样访问这些对象。

线程间通信

  • 定义:线程间通信是指在同一个进程中不同线程之间的数据交换。由于它们共享相同的内存空间,线程间通信通常更简单直接。
  • 目的:允许同一应用程序的不同执行线程能够协作和同步。
  • **机制:**包括但不限于以下几种:
    • 共享变量(需要同步机制,如锁或信号量)
    • 同步原语,如互斥锁(Mutexes)、信号量(Semaphores)、事件(Events)、条件变量(Condition Variables)
    • 线程安全的数据结构
    • 线程特定的数据(Thread-Specific Data, TSD)
    • 管道和套接字也可以用于线程间通信,但它们更常用于进程间通信

单工,半双工,全双工

全双工(Full-Duplex): 全双工通信允许两个设备或进程在通信过程中同时发送和接收数据。这意味着通信的双方可以不间断地进行双向数据交换,类似于电话通话。

半双工(Half-Duplex): 半双工通信只允许数据在一个方向上流动,发送方和接收方不能同时发送和接收数据。在任何给定的时刻,通信只能在一个方向上进行。例如,传统的对讲机就是半双工通信的典型例子,你只能说话或者听,不能同时进行。

单工(Simplex): 单工通信只允许数据在一个方向上流动,发送方发送数据,接收方接收数据,不允许反向通信。例如,无线电广播就是单工通信,广播台发送信息,听众接收信息,但听众无法通过广播台反馈信息。

在传统的Unix和类Unix系统中,匿名管道(Unnamed Pipes)通常是半双工(Half-Duplex)的,这意味着数据只能在一个方向上流动。在这种模式下,一个进程可以创建两个管道文件,一个用于写入,另一个用于读取,但不能同时进行读写操作。

命名管道(Named Pipes)或者FIFO(First In, First Out)文件则可以实现全双工(Full-Duplex)通信,这是因为它们允许数据在两个方向上独立流动,而不需要特定的读写顺序。在全双工模式下,两个进程可以同时发送和接收数据,就像打电话时双方可以同时听和说一样。

进程的生命周期

创建状态(new):进程正在被创建,尚未到就绪状态。

就绪状态(ready):进程已处于准备运行状态,即进程获得了除了处理器之外的一切所需资源,一旦得到处理器资源(处理器分配的时间片)即可运行。

运行状态(running):进程正在处理器上运行(单核 CPU 下任意时刻只有一个进程处于运行状态)。

阻塞状态(waiting):又称为等待状态,进程正在等待某一事件而暂停运行如等待某资源为可用或等待 IO 操作完成。即使处理器空闲,该进程也不能运行。

结束状态(terminated):进程正在从系统中消失。可能是进程正常结束或其他原因中断退出运行。

java线程的生命周期

在Java中,线程(Thread)有五种主要的状态,这些状态定义在java.lang.Thread.State枚举中。以下是这些状态的简要描述:

  1. NEW(新建):

    • 线程对象已被创建,但尚未启动。在这个阶段,线程还没有开始执行任何代码。
  2. RUNNABLE(可运行):

    • 线程已启动,并且已调用start()方法,但尚未运行run()方法的run()子程序。线程在运行时,可能正在执行run()方法的代码,也可能正在操作系统内核态中执行,或者正在等待CPU分配时间片。
  3. BLOCKED(阻塞):

    • 线程正在等待获取一个监视器锁(即同步锁),而该锁被其他的线程持有,则该线程进入Blocked状态。当该线程持有锁时,该线程将自动变成RUNNABLE状态。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      public class CustomLock {
      // 私有静态对象,用作锁
      private static final Object lock = new Object();

      // 提供一个公共的静态方法来访问锁对象
      public static Object getLock() {
      return lock;
      }
      }

      // 使用CustomLock类作为锁对象
      public class Main {
      public static void main(String[] args) {
      // 创建线程
      Thread thread = new Thread(new Runnable() {
      @Override
      public void run() {
      synchronized (CustomLock.getLock()) {
      // 同步代码
      }
      }
      });
      thread.start();

      // 主线程中的同步代码块
      synchronized (CustomLock.getLock()) {
      // 同步代码
      }
      }
      }
    • synchronized关键字,可以有以下几种用法(参考):

      • 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号synchronized(this){}括起来的代码,作用的对象是调用这个代码块的对象。 一个线程访问一个对象中的synchronized(this)同步代码块时,其他试图访问该对象的线程将被阻塞。

      • 修饰一个方法,被修饰的方法称为同步方法public synchronized void method(){},其作用的范围是整个方法,作用的对象是调用这个方法的对象。

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        // 写法二:

        public synchronized void method()
        {
        // todo
        }

        // 写法二:

        public void method()
        {
        synchronized(this) {

        }
        }
      • 修饰一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象。静态方法是属于类的而不属于对象的。同样的,synchronized修饰的静态方法锁定的是这个类的所有对象。

        1
        2
        3
        public synchronized static void method() {

        }
      • 修饰一个类,其作用的范围是synchronized(clazz.class){}后面括号括起来的部分,作用主的对象是这个类的所有对象。

    • 不会释放已经持有的资源和锁。

  4. WAITING(等待):

    • 线程正在无限期地等待另一个线程执行特定操作(显式唤醒)。这种状态通常发生在线程调用了Object.wait()Thread.join()LockSupport.park()方法后,并且没有设置超时时间。线程在等待状态下,直到其他线程调用Object.notify()Object.notifyAll()方法,或者当前线程所在的线程组被中断。

    • Object.notify()

      • 作用notify()方法用于唤醒在此对象监视器上等待的单个线程。如果多个线程在等待,它会唤醒其中一个线程,具体是哪一个线程是不确定的(由JVM的线程调度器决定)。
      • 使用场景:当你想要唤醒一个等待的线程,并且对唤醒哪个线程没有特别的要求时,可以使用notify()
    • Object.notifyAll()

      • 作用notifyAll()方法用于唤醒在此对象监视器上等待的所有线程。这将导致所有等待这个锁的线程都会被唤醒,并尝试重新获取锁。

      • 使用场景:当你想要唤醒所有等待的线程,可能是为了让他们重新评估自己的等待条件,或者因为所有等待的线程现在都可以继续执行了。

        共同点

      • 调用条件:这两个方法都必须在同步代码块或同步方法中调用,因为它们需要当前线程持有调用对象的锁。

      • 唤醒机制:被唤醒的线程不会立即执行,它们会进入锁的争夺队列,等待重新获取锁。

      • 返回到运行状态:被唤醒的线程只有在重新获得锁之后才能继续执行。

    • 不会释放已经持有的资源和锁。

  5. TIMED_WAITING(超时等待):

    • 线程正在等待另一个线程执行操作,但是这种等待有一个超时限制。这种状态可能发生在调用了带有超时参数的Object.wait(long timeout)Thread.join(long millis)LockSupport.parkNanos()LockSupport.parkUntil()方法后。超时时间到达后,线程将进入可运行状态。
  6. TERMINATED(终止):

    • 线程已完成执行其run()方法的代码,或者因其他原因终止了执行。在终止状态下,线程不再可运行,也不再可被恢复。

要获取或检查线程的状态,可以使用Thread类的getState()方法。了解线程状态对于调试多线程程序、处理并发问题以及确保资源正确管理非常重要。

创建线程并执行的实例

通过实现 Runnable 接口来创建线程:创建一个线程,最简单的方法是创建一个实现 Runnable 接口的类。

为了实现 Runnable,一个类只需要执行一个方法调用 run(),声明如下:

1
public void run()

你可以重写该方法,重要的是理解的 run() 可以调用其他方法,使用其他类,并声明变量,就像主线程一样。

在创建一个实现 Runnable 接口的类之后,你可以在类中实例化一个线程对象。

Thread 定义了几个构造方法,下面的这个是我们经常使用的:

1
Thread(Runnable threadOb,String threadName);

这里,threadOb 是一个实现 Runnable 接口的类的实例,并且 threadName 指定新线程的名字。

新线程创建之后,你调用它的 start() 方法它才会运行。

1
void start();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
class RunnableDemo implements Runnable {
private Thread t;
private String threadName;

RunnableDemo( String name) {
threadName = name;
System.out.println("Creating " + threadName );
}

public void run() {
System.out.println("Running " + threadName );
try {
for(int i = 4; i > 0; i--) {
System.out.println("Thread: " + threadName + ", " + i);
// 让线程睡眠一会
Thread.sleep(50);
}
}catch (InterruptedException e) {
System.out.println("Thread " + threadName + " interrupted.");
}
System.out.println("Thread " + threadName + " exiting.");
}

public void start () {
System.out.println("Starting " + threadName );
if (t == null) {
t = new Thread (this, threadName);
t.start ();
}
}
}

public class TestThread {

public static void main(String args[]) {
RunnableDemo R1 = new RunnableDemo( "Thread-1");
R1.start();

RunnableDemo R2 = new RunnableDemo( "Thread-2");
R2.start();
}
}