Java多线程基础

1.什么是线程?多线程?

线程可以看做是程序里面其中一条代码执行路径,例如QQ跟人消息聊天时对方向你发送了一个文件,此时你可以接收下载该文件,同时继续跟朋友聊天,因为接收下载文件是一条代码执行路径(线程),跟朋友聊天发消息是一条代码执行路径(线程),这两个功能互不干扰并且可以同时进行,这也是多线程的应用场景之一。

线程是cpu执行的最小单位(其实现在这么说是不太准确的,因为现在多数的cpu都是多核心多线程,变成了CPU的最小执行单位是核心,核心的最小执行单位是线程)。那么cpu是什么呢?它是一个特别精细的物理仪器,无论我们在电脑上干什么,底层都是由这个小小的仪器来帮帮我们实现的。CPU 是中央处理器(Central Processing Unit)的缩写,我们可以把它理解为计算机的"大脑"。

线程是操作系统进行调度的最小执行单位,调度可以理解为指挥,操作系统就是交警,根据自己的一套规则策略指挥调度这些线程能正常高效的调往cpu执行。为什么需要调度呢?我们知道cpu资源是有限的,有时候电脑上打开了多个网页或者打开了多个软件,那么是不是这些网页和软件都占用了cpu呢?答案是否定的,因为前面我们讲了cpu的执行能力是有限的,也就是说同一时刻执行的线程数是有限的。所以我们的操作系统就会帮助我们调度这些线程去给cpu执行,哪些是我们正在使用的线程,哪些我们已经闲置了,或者哪些我们后面又要使用了,我们的操作系统都能迅速的做出反应调度我们需要使用的线程交给cpu执行。

为了方便理解,我们打开任务管理器,发现占用CPU高的是QQ和酷狗,因为QQ一直都在接收消息,而酷狗是我正在听歌,所以这两个程序都是有我正在使用且需要执行的线程,所以会占用cpu。

而下图中的Typora我也有使用该程序查看markdown文件,但是cpu的使用率是0,因为我们现在并没有使用它,也就是没有使用它的任何功能,所以没有什么需要执行的线程。

但是当我们在Typora编辑文字的时候,Typora就有需要被立刻执行的线程了,因为我们正在使用该程序的编辑功能。所以此时我们的CPU占用也不是为0了。这也印证了我们的操作系统为了提高cpu的利用率和不浪费cpu资源,会对线程进行调度。

多线程就是指的多个线程,在我们的程序中实现的五花八门的功能跟我们的多线程是密切相关的。

2.多线程的并发和并行

并发:多个线程交替执行,交替的时间很快到可以忽略,看起来就像多个线程在同时执行。

在计算机发展之初,我们的cpu还是单核的,也就意味着在同一时刻cpu只能执行一个线程,此时我们的多线程程序就是通过并发的方式在cpu上执行的。

并行:多个线程在同一时刻在cpu上执行。

3.程序和进程

程序:为了完成特定任务,用某种语言编写的一组指令的集合。即指一段静态的代码,静态的对象。

实现软件的编程语言不止Java语言一种,流行的还有Python,c++,go等。这些编程语言编写的代码运行后,通过操作系统加载到内存中,被分解成了一个个指令让cpu执行。那为什么说程序是静态的呢?只要程序没有运行起来那么就是一个静态的对象,只是一段静态的代码。

进程:动态的程序,也就是执行中的程序就是进程。

每个进程都有一个独立的内存空间,系统运行一个程序即是一个进程从创建、运行到消亡的过程。

一个进程里面至少有一条线程,在同一个进程中的线程可以共享进程中的资源。这就使得线程间通信更简便、高效。但多个线程操作共享的系统资源可能就会带来安全隐患。

进程是操作系统调度和分配资源的最小单位,系统在运行时会为每个进程分配不同的内存空间。

下图中,红框的蓝色区域为线程独享,黄色区域为线程共享。

4.创建线程的4种方法

4.1 继承Thread类

Thread类中有run()方法,这是我们利用线程执行任务的方法,所以我们需要重写该方法,在方法中实现我们想要的逻辑。如下我在代码中实现:在Thread的子类Mythread中设置任务为打印0到9的数字。

创建该类的实例对象,然后调用start()方法开启线程,调用该方法之后才意味着我们真正开启了一条新的线程,调用该方法后会自动调用run()方法。

java 复制代码
public class MyThread extends Thread{
    @Override
    public void run() {
        System.out.println("该线程开始执行");
        for (int i = 0; i < 10; i++) {
            System.out.println("MyThread:" + i );
        }
    }

    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start();
        for (int i = 0; i < 10; i++) {
            System.out.println("main:" + i);
        }

    }
}

代码运行图:

由图我们可以知道在主线程中开启运行一个新的线程并不会堵塞主线程的代码,而是从myThread.start()开始,两个线程各执行各的,所以看到的效果是交替打印的效果。当然如果不是交替执行的效果也是正常的,因为线程对cpu的使用权本身就是不确定的。有可能主线程先打印完,分线程才打印,因为分线程创建线程是需要一定时间的。

注意:

不能用t1.run()代替t1.start()

不能让已经start()的线程再执行start()否则报线程状态异常

总结:

创建一个继承于Thread类的子类

重写Thread类的run()-----> 将线程要执行的操作,声明在此方法体中

创建当前Thread的子类的对象

通过对象调用start():1.启动线程 2.调用当前线程的run()

4.2 实现Runnable接口

1.创建实现Runnable接口的类

  1. 实现接口中的run方法

  2. 创建当前实现类的对象

4.将此对象作为参数传递到Thread类的构造器中,创建Thread类的实例

java 复制代码
public class MyRunnable implements Runnable{
    @Override
    public void run() {
        System.out.println("执行分线程");
    }

    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();
        Thread thread = new Thread(myRunnable);
        thread.start();
    }
}

其实实现Runnable接口与继承Thread类本质上都是对Runnable接口中的run方法进行了重写。

那么原本Thread类中的run方法写了什么呢?查看Thread类原代码:

Thread类中run方法中是执行了target这个对象的run方法,通过对target的追踪发现它是Thread类中声明的Runnable类的属性,在构造方法中我们找到了携带该属性的有参构造方法,在该构造方法中将我们自己实现的Runnable实例化对象赋值给target。

这也是我们用实现Runnable接口创建线程的原理,实际上就是利用了Thread的有参构造,可以将我们的Runnable实例化对象传入赋值,线程启动时调用run方法,而Thread类中的run方法又是调用的target的run方法也就是我们传入对象的run方法。

而继承Thread类创建线程则是重写了原本的run方法,利用了多态性,执行重写的方法以达到目的。

4.3 线程池创建线程

现有问题:

如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。

那么有没有一种办法使得线程可以复用,即执行完一个任务,并不被销毁,而是可以继续执行其他的任务?

**思路:**提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具。

代码实现:

java 复制代码
class NumberThread implements Runnable{

    @Override
    public void run() {
        for(int i = 0;i <= 100;i++){
            if(i % 2 == 0){
                System.out.println(Thread.currentThread().getName() + ": " + i);
            }
        }
    }
}

class NumberThread1 implements Runnable{

    @Override
    public void run() {
        for(int i = 0;i <= 100;i++){
            if(i % 2 != 0){
                System.out.println(Thread.currentThread().getName() + ": " + i);
            }
        }
    }
}

class NumberThread2 implements Callable {
    @Override
    public Object call() throws Exception {
        int evenSum = 0;//记录偶数的和
        for(int i = 0;i <= 100;i++){
            if(i % 2 == 0){
                evenSum += i;
            }
        }
        return evenSum;
    }

}

public class ThreadPoolTest {

    public static void main(String[] args) {
        //1. 提供指定线程数量的线程池
        ExecutorService service = Executors.newFixedThreadPool(10);
        ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;
//        //设置线程池的属性
//        System.out.println(service.getClass());//ThreadPoolExecutor
        service1.setMaximumPoolSize(50); //设置线程池中线程数的上限

        //2.执行指定的线程的操作。需要提供实现Runnable接口或Callable接口实现类的对象
        service.execute(new NumberThread());//适合适用于Runnable
        service.execute(new NumberThread1());//适合适用于Runnable

        try {
            Future future = service.submit(new NumberThread2());//适合使用于Callable
            System.out.println("总和为:" + future.get());
        } catch (Exception e) {
            e.printStackTrace();
        }
        //3.关闭连接池
        service.shutdown();
    }

}

5.线程的生命周期

.start()方法开启线程 开始调用run()方法执行任务 执行完run方法之后线程销毁。

其他多线程相关的博客:【java多线程】线程不安全原因及解决办法总结_java 线程不安全-CSDN博客

造成死锁的成因以及解决方案-CSDN博客

总结并发编程中的锁策略、CAS及synchronized是如何进行优化的-CSDN博客

相关推荐
ᅠᅠᅠ@1 分钟前
异常枚举;
开发语言·javascript·ecmascript
编程版小新7 分钟前
C++初阶:STL详解(四)——vector迭代器失效问题
开发语言·c++·迭代器·vector·迭代器失效
陈大爷(有低保)21 分钟前
UDP Socket聊天室(Java)
java·网络协议·udp
c4fx26 分钟前
Delphi5利用DLL实现窗体的重用
开发语言·delphi·dll
kinlon.liu34 分钟前
零信任安全架构--持续验证
java·安全·安全架构·mfa·持续验证
鸽芷咕1 小时前
【Python报错已解决】ModuleNotFoundError: No module named ‘paddle‘
开发语言·python·机器学习·bug·paddle
王哲晓1 小时前
Linux通过yum安装Docker
java·linux·docker
Jhxbdks1 小时前
C语言中的一些小知识(二)
c语言·开发语言·笔记
java6666688881 小时前
如何在Java中实现高效的对象映射:Dozer与MapStruct的比较与优化
java·开发语言
Violet永存1 小时前
源码分析:LinkedList
java·开发语言