Java并发编程的核心概念--线程与进程

  • 原子性‌:‌操作或多个操作要么全部执行且不被打断,‌要么都不执行。‌这保证了线程在执行操作时不会被其他线程干扰。‌
  • 可见性‌:‌当多个线程访问同一个变量时,‌一个线程修改了这个变量的值,‌其他线程能够立即看到修改的值。‌这通过volatile关键字、 synchronized和Lock等机制实现。
  • 有序性‌:‌程序执行的顺序按照代码的先后顺序执行。‌Java内存模型允许指令重排序,但提供了volatile等机制来保证一定的有序性。

进程

进程的概念

进程是计算机中的程序关于某数据集合上的一次运行活动,‌是系统进行资源分配和调度的基本单位,‌是操作系统结构的基础‌。‌进程是一个具有独立功能的程序关于某个数据集合的一次运行活动,‌它可以申请和拥有系统资源,‌是一个动态的概念,‌是一个活动的实体。‌进程不只是程序的代码,‌还包括当前的活动,‌通过程序计数器的值和处理寄存器的内容来表示。‌

在早期面向进程设计的计算机结构中,‌进程是程序的基本执行实体;‌在当代面向线程设计的计算机结构中,‌进程是线程的容器。‌程序是指令、‌数据及其组织形式的描述,‌进程是程序的实体‌。

进程的状态:‌

  • 运行状态‌:‌进程正在CPU上运行,‌占用处理器资源。‌
  • 就绪状态‌:‌进程已具备运行条件,‌等待系统分配处理器资源以便运行。‌
  • 等待状态‌(‌也称为阻塞状态或睡眠状态)‌:‌进程因等待某个事件(‌如I/O操作完成)‌而无法运行,‌即使获得CPU控制权也无法执行。‌

此外,‌根据不同的系统和描述,‌进程还可能包括新建状态终止状态等。‌新建状态表示进程刚被创建但尚未就绪,‌终止状态则表示进程已结束运行但系统尚未回收其资源‌。‌

进程状态之间的转换:‌

  • 就绪到运行‌:‌当进程调度程序为处于就绪状态的进程分配了处理机后,‌该进程便由就绪状态转变为执行状态。‌
  • 运行到就绪‌:‌处于执行状态的进程在其执行过程中,‌因分配给它的时间片已用完或更高优先级的进程需要运行,‌而不得不让出处理机,‌于是进程从执行状态转变为就绪状态。‌
  • 运行到阻塞‌:‌正在执行的进程因等待某种事件发生(‌如I/O操作完成)‌而无法继续执行时,‌便从执行状态转变为阻塞状态。‌
  • 阻塞到就绪‌:‌处于阻塞状态的进程,‌若其等待的事件已经发生(‌如I/O操作完成)‌,‌则进程由阻塞状态转变为就绪状态。‌

进程转换中会遇到问题:‌

  1. 进程切换开销大‌:‌进程切换需要反复进入内核态,‌置换状态信息,‌当进程数较多时,‌大部分系统资源可能被进程切换所消耗。‌‌
  2. 死锁问题‌:‌多个进程因竞争资源而造成一种僵局,‌若无外力作用,‌这些进程都将无法向前推进。‌‌
  3. 同步与互斥问题‌:‌多个进程在访问共享资源时,‌需要确保数据的一致性和防止数据被破坏,‌需要采用同步机制如互斥锁、‌信号量等。‌‌
  4. 进程通信问题‌:‌进程间需要通信和共享数据时,‌需要采用进程间通信(‌IPC)‌机制,‌如管道、‌消息队列、‌共享内存等。‌‌

进程示例代码:

以下是一个简单的Java进程示例代码,‌它创建了一个echo进程来执行一个外部命令cmd /c echo(‌比如命令在Unix/Linux系统上,‌或者命令在Windows系统上)‌:‌

public class ProcessExample {
    public static void main(String[] args) {
        try {
            // 在Unix/Linux系统上使用
            // Process process = Runtime.getRuntime().exec("echo Hello, World!");
            
            // 在Windows系统上使用
            Process process = Runtime.getRuntime().exec("cmd /c echo Hello, World!");
            
            // 读取进程的输出
            java.io.BufferedReader reader = 
                new java.io.BufferedReader(new java.io.InputStreamReader(process.getInputStream()));
            String line;
            while ((line = reader.readLine()) != null) {
                System.out.println(line);
            }
            
            // 等待进程结束
            int exitVal = process.waitFor();
            if (exitVal == 0) {
                System.out.println("Success!");
            } else {
                // 有错误发生
                System.out.println("Something went wrong");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

这段代码首先通过方法创建了一个进程来执行一个外部命令Runtime.getRuntime().exec()。‌然后,‌它读取了这个进程的输出,‌并将其打印到控制台上。‌最后,‌它等待进程结束,‌并检查进程的退出值来确定命令是否成功执行。‌

请注意,‌在实际应用中,‌你可能需要处理更多的错误情况,‌并且确保正确地关闭所有打开的资源,‌比如。‌此外,‌当执行外部命令时,‌一定要小心,‌避免安全风险。

进程与线程的区别:‌

  • 资源拥有‌:‌进程是资源分配的基本单位,‌拥有独立的内存空间和系统资源;‌线程是CPU调度的基本单位,‌基本上不拥有系统资源,‌只拥有一点运行中必不可少的资源,‌且共享其所属进程的资源‌。‌
  • 并发性‌:‌进程间并发执行需要较大的开销,‌而同一进程内的多个线程间并发执行开销较小,‌因此线程能更高效地提高并发性‌。‌
  • 系统开销‌:‌创建或撤销进程时,‌系统需要分配或回收资源,‌开销较大;‌而线程切换只需保存和设置少量寄存器的内容,‌开销较小‌。‌
  • 独立性‌:‌进程间相互独立,‌一个进程崩溃不会影响其他进程;‌而一个线程崩溃可能导致整个进程崩溃‌。‌
  • 执行过程‌:‌进程拥有独立的执行序列,‌而线程必须依存于应用程序中,‌由应用程序提供多个线程执行控制‌。‌

线程

线程的基本概念

线程是操作系统调度的最小单位,‌也是程序执行的最小单元。‌在Java中, 线程是Thread类的实例, 每个线程都有自己的堆栈和程序计数器。‌Java语言支持多线程, 允许程序同时执行多个任务, 从而提高程序的并发性和响应性。

线程的创建方式

Java中创建线程主要有以下几种方式:

1. 继承Thread :

  • 创建一个继承自Thread类的子类。‌
  • 重写run()方法,‌在其中定义线程要执行的任务。‌
  • 创建线程对象,‌并调用start()方法启动线程。‌注意,‌不能直接调用run()方法,‌因为这样会使得run()方法在当前线程中执行,‌而不是在新的线程中执行。‌
示例代码:‌
public class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("Thread is running...");
    }

    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start(); // 启动线程
    }
}

2. 实现Runnable接口 :

  • 创建一个实现了Runnable接口的类。‌
  • 实现run()方法,‌在其中定义线程要执行的任务。‌
  • 创建Thread对象,‌并将实现了Runnable接口的对象作为参数传递给Thread类的构造函数。‌
  • 调用方法启动线程start()。‌
示例代码:‌
public class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("Thread is running...");
    }

    public static void main(String[] args) {
        Thread thread = new Thread(new MyRunnable());
        thread.start(); // 启动线程
    }
}

3. 实现Callable接口 ( Java 5及以上版本) :

在Java 5及以上版本中, Callable接口类似于Runnable接口, 但它可以返回一个结果, 并且可以抛出异常。与Runnable不同,‌Callable接口通常与FutureTask类一起使用,‌FutureTask类实现了Future接口,‌并提供了启动和取消计算、‌查询计算是否完成以及获取计算结果的方法。‌

以下是一个简单的例子,‌展示了如何实现Callable接口,‌并使用FutureTask来启动和管理线程:‌​​​​​​​

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

// 实现Callable接口
public class CallableTask implements Callable {
    private int number;

    public CallableTask(int number) {
        this.number = number;
    }

    @Override
    public Integer call() throws Exception {
        // 模拟一些计算
        Thread.sleep(1000);
        return number * 2;
    }
}

// 创建并启动线程,‌使用Callable和FutureTask
public class CallableExample {
    public static void main(String[] args) {
        // 创建CallableTask对象
        CallableTask callableTask = new CallableTask(10);
        // 将CallableTask对象包装在FutureTask对象中
        FutureTask futureTask = new FutureTask<>(callableTask);
        // 创建并启动线程
        Thread thread = new Thread(futureTask);
        thread.start();

        try {
            // 等待线程执行完成,‌并获取结果
            Integer result = futureTask.get();
            System.out.println("线程返回的结果: " + result);
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
    }
}

在这个例子中,‌CallableTask类实现了Callable接口,‌并重写了call方法。‌call方法包含了线程执行的任务代码,‌并返回一个整数结果。‌在CallableExample类的main方法中,‌我们创建了一个CallableTask对象,‌并将其包装在对象中。‌然后,‌我们创建了一个FutureTask线程,‌将对象作为参数传递给FutureTask线程的构造函数,‌并启动​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​FutureTask线程。‌最后,‌我们通过调用对象的get方法来等待线程执行完成,‌并获取结果。‌​​​​​​​

4. 使用匿名内部类 :

  • 可以通过匿名内部类的方式简化线程创建的代码,‌适用于线程执行的任务较为简单的情况。‌
使用匿名内部类创建并启动线程的示例:‌
public class AnonymousInnerClassThreadExample {
    public static void main(String[] args) {
        // 使用匿名内部类创建并启动线程
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                // 在这里编写线程的任务代码
                System.out.println("匿名内部类线程正在运行...");
            }
        });
        
        // 启动线程
        thread.start();
    }
}

在这个例子中,‌我们没有创建一个单独的类来实现Runnable接口。‌相反,‌我们在创建Thread对象时,‌直接提供了一个​​​​​​​​​​​​​​Runnable接口的匿名实现。‌这种方式使得代码更加简洁,‌特别适用于线程任务较为简单的情况。‌

5. 使用Lambda表达式 ( Java 8及以上版本) :

  • Java 8引入了Lambda表达式, 可以进一步简化线程创建的代码。‌
示例代码:

public class LambdaThreadExample {
    public static void main(String[] args) {
        // 使用Lambda表达式创建并启动线程
        new Thread(() -> {
            // 在这里编写线程的任务代码
            System.out.println("Lambda表达式线程正在运行...");
        }).start();
    }
}

在这个例子中, 我们直接使用Lambda表达式() -> { /* 任务代码 */ }作为Thread类的构造函数的参数。Lambda表达式是Runnable接口的一个实现, 它覆盖了run方法。这种方式使得代码更加简洁,‌并且避免了创建额外的类或匿名内部类。‌

线程的运行

线程被创建并调用start()方法后,‌并不立即执行run()方法中的代码。‌线程会进入就绪状态,‌等待CPU的调度。‌当线程获得CPU时间片后,‌它才会开始执行run()方法中的代码,‌进入运行状态。‌线程在执行过程中可能会因为各种原因(‌如等待I/O操作完成、‌等待锁等)‌进入阻塞状态,‌等待条件满足后再次进入就绪状态,‌等待CPU调度执行。‌最终,‌线程执行完​​​​​​​run()方法中的代码或因为异常而终止,‌进入死亡状态​​​​​​​​​​​​​​。‌

使用接口创建和启动线程

下面是一个简单的Java线程示例代码, 使用Runnable接口来创建和启动线程:

// 实现Runnable接口
class MyThread implements Runnable {
    public void run() {
        // 在这里编写线程要执行的任务
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + " 正在运行,‌i=" + i);
            try {
                // 线程休眠一段时间
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

public class ThreadExample {
    public static void main(String[] args) {
        // 创建MyThread对象
        MyThread myThread = new MyThread();
        
        // 创建线程对象,‌将MyThread作为目标对象传递给Thread对象
        Thread thread = new Thread(myThread);
        
        // 启动线程
        thread.start();
        
        // 也可以这样创建并启动线程(‌匿名内部类方式)‌
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 5; i++) {
                    System.out.println(Thread.currentThread().getName() + " 匿名内部类方式正在运行,‌i=" + i);
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();
    }
}

在这个示例中,‌MyThread类实现了Runnable接口,‌并重写了run方法以定义线程要执行的任务。‌在main方法中,‌我们创建了​​​​​​​​​​​​​​​​​​​​​MyThread的实例,‌并将其作为目标对象传递给Thread对象。‌然后,‌我们调用​​​​​​​​​​​​​​Thread对象的​​​​​​​start方法来启动线程。‌

此外,‌示例中还展示了如何使用匿名内部类的方式来创建并启动线程,‌这种方式不需要显式地创建一个实现Runnable接口的类。‌

注意事项

  • 线程一旦启动,‌就不能再次启动。‌尝试对已经启动的线程调用start()方法会抛出异常IllegalThreadStateException。‌
  • 直接调用线程的run()方法并不会创建新的线程,‌而是在当前线程中同步执行run()方法中的代码​​​​​​​。‌
  • 在多线程编程中,‌需要注意线程同步和互斥问题,‌以避免数据不一致和竞态条件的发生。‌

你可能感兴趣的:(java,开发语言,学习,线程与进程)