Java多线程基础篇|JUC并发编程基础篇

一、进程与线程基本概念

进程和线程不是一开始就有的,而是随着需要才出现的概念。

1、进程产生的背景

最初计算机只能接受一些特定的指令,用户输入一个指令,计算机就做一个操作。但是用户输入的速度是远远低于计算机运算的速度的,所以计算机有大量时间是在等待用户的输入,也就是 CPU 一直处于空闲状态,CPU 的利用率非常低。

批处理操作系统

后来有了批处理操作系统,把一些系列的指令写成一个清单,一次性把这个清单交给计算机,然后计算机会逐行读取指令,并把结果输出到另外一个磁盘中。

批处理操作系统的出现虽然提高了计算机的效率,但是由于批处理操作系统的指令运行方式仍然是串行的,内存中始终只有一个程序在运行。这种批处理操作系统并不理想,因为在同一时刻,CPU 只能执行一个程序的指令,也就是内存中只能运行一个程序。

进程的提出

由于内存中只能运行一个程序,于是计算机科学家们提出了进程的概念。

进程就是应用程序在内存中分配的空间,也就是在内存中可以同时存在多个程序在运行,并且各个应用程序(进程)之间互不干扰。每一个进程都保存程序运行的状态。

程序:指能完成某些功能的代码集合。

在计算机中的 CPU 采用时间片的方式运行每一个进程。CPU 为每一个进程会分配一个时间片,如果时间片结束时进程还在运行,则暂停这个进程的运行,并且 CPU 分配给另外一个进程(这个过程就是上下文切换)。如果进程在时间片结束前阻塞或结束,则 CPU 立即进行切换,不用等待时间片用完。

需要注意 CPU 进行上下文切换是非常耗时的操作,因为要在进程切换前会保存当前进程的状态(进程的标识,进程使用的资源等),方便在下次获取到 CPU 时间片之后根据之前保存的状态进行恢复,接着继续执行。

使用 CPU时间片 + 进程的方式,在宏观上觉得在同一时间段执行了多个任务,那是因为 CPU 的运算速度太快了,所以看上去像是在并发处理任务。其实在在单核 CPU 来说,任意时刻只有一个程序在执行。

并发:在同一时间段,处理多个任务。

并行:在同一时刻,处理多个任务。

对操作系统的要求进一步提高

虽然进程的出现,使得操作系统的性能大大提升,但是随着时间的推移,人们并不满足一个进程在一段时间只能做一件事情,如果一个进程有多个子任务时,只能逐个得执行这些子任务,很影响效率。

比如:我们电脑上的杀毒软件,我们在扫描病毒的同时不能进行垃圾扫描。我们必须要等待病毒扫描完成之后,才能进行垃圾扫描。

线程的提出

我们能不能让这些子任务同时执行呢?于是线程的概念就被提出来了。线程是比进程更小的单位,一个程序就是一个进程,而一个进程里面可以包含一个或多个线程。

image-20230405200232690

比如:我们电脑上的杀毒软件,可以一边进行病毒扫描,一边进行垃圾扫描。

进程让操作系统的并发性成为了可能,而线程让进程的内部并发成为了可能。

既然进程也可以实现并发,那为啥还要提出进程呢?

  • 进程的通信比较复杂,数据不容易共享,不同进程之间有内存屏障。而线程方便数据的共享,也方便线程之间的通信。
  • 进程是重量级的,切换进程的开销比较大,不但需要保存寄存器和栈信息,还要进行资源的分配回收以及页调度。而线程只需要保存寄存器和栈信息,开销比较小。

二、多线程

上面我们讲了进程和线程的来由。那么在 Java 中如何创建线程呢?

1、继承Thread类

class MyThread extends Thread {
    
    

    @Override
    public void run() {
    
    
        System.out.println(Thread.currentThread().getName());
    }
}
public class Demo04 {
    
    
    public static void main(String[] args) {
    
    
        MyThread thread1 = new MyThread();
        thread1.setName("子线程1");
        thread1.start();
        MyThread thread2 = new MyThread();
        thread2.setName("子线程2");
        thread2.start();
        System.out.println(Thread.currentThread().getName());
    }
}

注意:同一个线程不能多次调用start()方法,否则会报 IllegalThreadStateException异常。

start() 源码:

private volatile int threadStatus = 0;
public synchronized void start() {
    
    
    /**
         * This method is not invoked for the main method thread or "system"
         * group threads created/set up by the VM. Any new functionality added
         * to this method in the future may have to also be added to the VM.
         *
         * A zero status value corresponds to state "NEW".
         */
    if (threadStatus != 0)
        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();
        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();

分析源码发现threadStatus = 0,当调用本地方法start0()之后会改变threadStatus的值会改变,因此第二次调用start()方法时会报IllegalThreadStateException异常。

2、实现Runnable接口

在上面使用继承 Thread 类的方式来创建线程,但是我们知道在 Java 中是单继承,多实现。我们要是一个类继承了 Thread 类就不能显示的继承其它类了,那如果我们现在既要创建新的线程任务,也要继承其它类,那么我们要如何实现呢?

在 Thread 的构造器中,支持传递一个 Runnable 接口的实现类的方式创建线程。

image-20230405203241447

class MyCustomThread implements Runnable {
    
    

    @Override
    public void run() {
    
    
        System.out.println(Thread.currentThread().getName());
    }
}
public class Demo05 {
    
    
    public static void main(String[] args) {
    
    
        Thread thread = new Thread(new MyCustomThread());
        thread.start();
    }
}

我们还可以对以上的代码进行简化,比如写成匿名内部类对象的方式,还可以写成 lamdab 表达式的方式。

何为匿名内部类对象?

匿名内部类是指没有名字的内部类,它在声明时直接实现一个接口或继承自一个类,并且在使用时直接进行创建和实例化。匿名内部类在Java中非常常用,它可以简化代码的书写,使代码更为简洁,并且也可以使程序员在一定程度上隐藏代码的实现细节。

格式:

new 父类构造器<实参列表> implements 接口名<泛型参数列表>{ 外部类成员变量、方法;[内部类成员变量] [内部类方法]}

在 JDK 8 之后,如果不修改外部变量,可以直接访问,不用加 final 修饰。

public class OuterClass {
     
     
    public void myMethod() {
     
     
        final int x = 3; // 将x声明为final
        new Thread(new Runnable() {
     
     
            @Override
            public void run() {
     
     
                x++; // 编译错误,无法修改x的值
                System.out.println(x);
            }
        }).start();
    }
}

匿名内部类对象:

public class Demo05 {
    
    
    public static void main(String[] args) {
    
    
        Thread thread = new Thread(new Runnable {
    
    
            @Override
            public void run() {
    
    
                System.out.println(Thread.currentThread().getName());
            }
        });
        thread.start();
    }
}

要区分匿名对象,匿名内部类对象,内部类三者的区别。

匿名对象:没有给创建的对象起一个名字。

例如:new MyThread()

内部类:在一个类的内部定义的类。

例如:

class OutterClass {
     
     
    // 外部类成员
    class InnerClass {
     
     
        // 内部类成员
    }
}

匿名内部类对象:没有给内部类的实例起名字。

我们还可以进一步简化代码,写成 lamdab 表达式。

public class Demo05 {
    
    
    public static void main(String[] args) {
    
    
        new Thread(() -> System.out.println(Thread.currentThread().getName())).start();
    }
}

如果对 lamdab 不熟悉的同学,可以去网上搜索学习。

要使用 lamdab,使用的 JDK 版本要求大于等于8。


更多内容欢迎访问我的博客

猜你喜欢

转载自blog.csdn.net/qq_43907505/article/details/130054237