初识 线程

简介: 初识 线程

引言



什么是线程


线程是一个执行流,一个进程由一个或多个线程构成,简单来说:线程是进程的子集,或者可以将线程视为轻量级进程。它是 CPU 调度执行的基本单位。


  • 从 OS (操作系统) 的角度看,线程是调度的基本单位。
  • 从应用开发者角度看,线程是一个分担任务的角色。


82fea927573f442bb4687085520fe0ca.png


一、为什么引入线程



引入进程的目的,就是为了能够 " 并发编程 ",虽然多进程已经能够解决并发的问题了,但是我们认为还不够理想。


串行:单车道运输

并行:多车道运输


创建进程 / 销毁进程 / 调度进程,开销有较大。


因为进程是系统资源分配的基本单位,所以创建进程,需要分配资源,销毁进程,就需要释放资源。因此,如果频繁创建销毁,这样所带来的开销就比较大了。


于是就有了 " 线程 "(Thread) 这一概念,而线程在有些系统上也叫做 “ 轻量级进程 "。

为什么呢?


因为创建线程比创建进程更高效,销毁线程比销毁进程更高效,调度线程比调度进程更高效。


创建线程,并没有去申请资源,而销毁线程,也不需要释放资源。让线程产生在进程内部,那么线程就能够共用之前的资源。


我们必须明确:进程和线程之间是包含关系,一个进程可以包含一个线程或者多个线程。当我们先把进程创建出来之后,这时候就相当于资源都分配好了,接着,我们再在这个进程里面,创建线程。这样一来,线程就可以和之前的进程共用一样的资源了。


二、进程和线程之间的区别和联系( 经典面试题 )



① 一个进程至少包含一个线程,线程不能独立于进程而存在。也就是说,进程是包含线程的,一个进程里可以有一个线程,也可以有多个线程。

② 进程是操作系统资源分配的基本单位,线程是操作系统调度执行的基本单位。

③ 每个进程都有独立的内存空间( 虚拟地址空间 ),同一个进程的多个线程之间,共用这个虚拟地址空间。也就是说,多个进程间不能共享资源,而线程可以共享当前的进程资源文件。

④ 从第③点来看,虽然线程更加轻量,但并不是所有场景都适用的,当我们需要一个项目执行起来稳定,并做到各个用户使用稳定,就需要用到进程。


举例说明: 如下图所示,我们可以发现,进程可以看作是计算机正在运行的软件,CPU 和内存为这些进程提高硬件资源,而线程就是某个软件正在执行的多个任务。例如,百度网盘正在执行的有三个任务,而这些任务下载的时候都共享着百度网盘这个软件的所有资源。如果其中一个任务出现了问题,就可能导致整百度网盘停止运行了,但即使百度网盘停止运行了,也不会影响 迅雷和 QQ 音乐,因为 CPU 好好的在那呢。所以说,软件与软件之间没有影响,但一个软件的内部可能受一部分的线程所影响。即进程之间更加稳定,线程虽然更轻量、开销小,但相对不稳定。


d69d4443dbbe424593360226d0a2562c.png


三、图解进程与线程之间的区别



举个例子,辅导员现在让班长做一份表格,统计班级100 个同学的成绩,而这都要让班长一个人做,班长肯定不乐意了,所以班长想了一个办法,多叫几个人帮他统计,这样效率就能提高很多。


5fa332460b9b43fda26865e80ba42a7a.png


1. 多进程



多进程如下图所示:


班长叫来了一个同学,让他在房间B 中统计表格,而班长在原来的房间A 统计表格,他们两人之间互不干扰,这体现了进程的隔离性。


另外,两个房间,两张桌子,说明每次创建进程,都需要给其分配一些资源。


aebd5cfec5aa4926a73c4a72d481485c.png


2. 多线程


多线程如下图所示:


班长起初叫来了同学A,之后为了提高统计表格的效率,又叫来了同学B…他们都在一间屋子进行统计表格,他们互相之间都能够看到彼此,这体现了:一个进程的多个线程之间,共用了一个虚拟地址空间。


另外,多了几个同学,多了几张桌子,并没有创建房间,这体现了:创建线程的成本比创建进程的成本更低。


d1307b5771e641e2a15c9476fc8648c4.png


3. 注意多线程的使用


① 这里我们需要注意创建线程时的数目,刚开始,我们为了提高效率,多叫几个同学来统计表格,这当然没有问题,当同学非常多的时候,是不是整个屋子就装不下那么多人了?也就是说,线程的数目不是越多越好,如果线程数目多到一定程度,线程之间就会更频繁地进行调度,而这时候,调度所实现的开销就比较大了 !!! 于是,执行速度反而会变慢。


② 这么多同学在一个房间中统计表格,如果事先没有约定好谁负责哪一部分的内容,是不是同学之间容易重复地统计了某项信息?就比如说:同学A 和同学B 同时统计了同一个同学的成绩,之后又互相修改了,这就乱了套了。这种冲突体现了:线程不安全。【 多线程编程的重点问题 】


③ 如果其中一个同学在使用电脑的时候,不小心脚踩到了拖线板的开关,这就造成了整个房间的电脑都熄屏了,那么之前所有统计的同学成绩都因此而作废,这是不是也带来了一个问题呢?而这就这体现了:一个进程里面,如果某个线程抛出了异常,并没有合理 catch 到的话,可能导致整个进程都异常退出 !!!


四、站在系统内核的角度,再来看待进程和线程



在 Linux 系统中,线程同样是使用 PCB 来描述的。从操作系统内核的角度来看,它不分进程还是线程,两者都用 PCB 来代替。


当创建一个进程的时候,就是创建了一个 PCB 出来了,同时这个 PCB,也可以视为是当前进程中已经包含了一个线程了。( 一个进程至少有一个线程 )


在下图中,进程1 对应了一个 PCB,在这个进程1 中创建了一个线程,也就是再加了一个 PCB。


844bd5f52cfd4a6389f8a797ad7eb0eb.png


五、线程与代码之间有什么联系?



我们可以认为,一个线程就是代码中的一个执行流。

执行流:按照一定的顺序,来执行一组指令。


六、使用 Java 来操作线程



理解 Java 线程的三个概念


主线程:当一个程序启动时,一个线程立刻运行,该线程通常叫做程序的主线程。在 Java 中,主线程就是执行 main 方法的线程。


单线程程序:一个 Java 程序中只有一个线程,执行从 main 方法开始,从上到下依次执行。


多线程程序:不仅仅有执行 main 方法的线程,也有其他线程。


1. 程序清单1


// Thread 是Java 标准库中描述的一个关于线程的类
// 常用的方法就是自己定义一个类继承 Thread
// 重写 Thread 中的 run 方法,而run 方法就表示线程需要执行的具体任务
class MyThread extends Thread{
    @Override
    public void run() {
        System.out.println("hello thread !");
    }
}
public class ThreadDemo1 {
    public static void main(String[] args) {
        Thread thread = new MyThread();
        //start 方法会在操作系统中真正地创建一个线程出来(内核中创建一个 PCB,加入到双向链表中)
        //这个新的线程,就会执行 run 方法中的代码
        thread.start(); //方式一
        //thread.run(); //方式二
    }
}


输出结果:


08fe7bce886a458caac162fb5eb09890.png


start 和 run 方法之间的区别


在程序清单1中,start 方法和 run 方法有什么本质上的区别呢?


① 使用 start 方法,真正意义上创建了一个新的线程出来,能够使得线程之间并发执行。此时,执行 main 方法的作为主线程,创建出来新线程的为子线程。


② 使用 run 方法,并没有创建一个新的线程出来,此时依旧是单线程的状态。

结论:使用 start 方法,真正意义上创建了一个新的线程出来,能够使得线程之间并发执行。使用 run 方法,并没有创建一个新的线程出来。


举个例子,大学辅导员让班长统计表格,班长觉得任务量太重,于是就叫来了副班长,他们两个一起干活,这就对应到 start 方法;而 run 方法就表示只是班长一个人在干活。


图解分析:


e6f62d8fb7804395950a87f80b0f585c.png


2. 程序清单2


class MyThread2 extends Thread{
    @Override
    public void run() {
        while (true){
            System.out.println("hello thread !");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}
public class ThreadDemo2 {
    public static void main(String[] args) {
        Thread thread = new MyThread2();
        thread.start();
        while (true){
            System.out.println("hello main !");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}


输出结果:


下面的结果是死循环,我只截图了一部分,可以发现 main 方法中的代码和重写的 run 方法中 的代码之间,交替运行,即给我们呈现出来【并发式运行】的逻辑。


6279d5c4840b48b2b9a9984454c620b0.png


3. 程序清单3


在下面的程序清单中,我们把程序清单2的 start方法改变成 run方法,看看其对应的效果。


class MyThread2 extends Thread{
    @Override
    public void run() {
        while (true){
            System.out.println("hello thread !");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}
public class ThreadDemo3 {
    public static void main(String[] args) {
        Thread thread = new MyThread2();
        thread.run(); //方式二
        while (true){
            System.out.println("hello main !");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}



输出结果:


下面的结果是死循环,我只截图了一部分,可以发现,我们只调用了子类中重写的 run 方法。


这很好理解,run 方法一直在死循环,这就是按照顺序执行的,所以程序并没有走到 main方法后面的代码。


23e184c134fb4bd99932bf6e181e5143.png


至此,我们通过程序清单2 和 程序清单3,就能够更好地理解 start 方法和 run 方法了。


七、多线程的优势



多线程的优势在于:同一情况下,能够提升程序的运行速度


串行:一个线程执行了 20亿 次循环

并行:两个线程各自执行了10亿 次循环


程序清单4:


public class ThreadDemo4 {
    private static final long count = 10_0000_0000;// 10亿
    /**
     * 串行来针对 x 和 y 自增
     */
    public static void serial() {
        //System.currentTimeMillis 这个方法可以获取到当前系统的时间戳
        long begin = System.currentTimeMillis();
        int x = 0;
        for (long i = 0; i < count; i++) {
            x++;
        }
        int y = 0;
        for (long i = 0; i < count; i++) {
            y++;
        }
        long end = System.currentTimeMillis();
        System.out.println("串行时间(毫秒): " + (end - begin) );
    }
    /**
     * 并行来针对 x 和 y 自增
     */
    public static void concurrency() throws InterruptedException {
        long begin = System.currentTimeMillis();
        Thread t1 = new Thread() {
            @Override
            public void run() {
                int x = 0;
                for (long i = 0; i < count; i++) {
                    x++;
                }
            }
        };
        t1.start();
        Thread t2 = new Thread() {
            @Override
            public void run() {
                int y = 0;
                for (long i = 0; i < count; i++) {
                    y++;
                }
            }
        };
        t2.start();
        //保证 t1 和 t2 都执行完了之后,再结束计时
        //join 方法就是等待对应的线程结束
        //t1.join(); 就表示 t1 对应的 run 方法没执行完之前,会阻塞等待
        //t2.join(); 就表示 t2 对应的 run 方法没执行完之前,会阻塞等待
        t1.join();
        t2.join();
        long end = System.currentTimeMillis();
        System.out.println("并行时间(毫秒): " + (end - begin) );
    }
    public static void main(String[] args) throws InterruptedException {
        serial();
        concurrency();
    }
}


输出结果:


下面是我通过 IDEA 编译器测试输出的三次结果,我们发现,并行时间总是比串行时间快一倍左右,那么,并行总是比串行的速度快一倍吗?


不一定,因为对于线程调度来说,自身也是有开销的,而这两组数据调度的时间完全不确定,所以我们无法得出时间上的数据,但一般来说,运行某个程序时,并行确实比串行速度快。


314305ad138a42d990c1ebea8b0128ea.png


八、创建线程的几种写法



方法一:通过自定义类继承 Thread 类


class MyThread extends Thread{
    @Override
    public void run() {
        System.out.println("hello thread !");
    }
}
public class ThreadDemo1 {
    public static void main(String[] args) {
        Thread thread = new MyThread();
        thread.start();
    }
}


变形1:匿名内部类创建 Thread 子类的对象


这个方法和方法一没有区别,只是简化了一下而已,不过这样写方便不少。

下面的写法相当于创建了一个匿名的类,这个类继承了 Thread 类


【 new Thread 】这个代码实际上不是 new 了 Thread 类本身,而是 new 了一个 Thread类 的子类的实例。这里第一次看确实有点混人,它给你一种创建了父类的错觉,不理解的小伙伴可以去我的博客看看 《Java 中的内部类》,其中,有提到内部类。


public class Test1 {
    public static void main(String[] args) {
        Thread thread = new Thread() {
            @Override
            public void run() {
                System.out.println("hello thread !");
            }
        };
        thread.start();
    }
}


方法二:通过自定义类实现 Runnable 接口


class MyRunnable implements Runnable{
    @Override
    public void run() {
        System.out.println("hello thread !");
    }
}
public class ThreadDemo2 {
    public static void main(String[] args) {
        Thread thread = new Thread(new MyRunnable());
        thread.start();
    }
}



变形1:匿名内部类创建 Runnable 子类对象


同样地,在这里我们创建了一个 Runnable类 的子类,之后将这个子类再作为 Thread 构造方法的参数,这和方法一中的匿名内部类有一点点区别,不过思想是一样的。


public class ThreadDemo3 {
    public static void main(String[] args) {
        Thread thread = new Thread( new Runnable() {
            @Override
            public void run() {
                System.out.println("hello thread !");
            }
        } );
        thread.start();
    }
}


方法三:lambda 表达式


public class Test2 {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> System.out.println("hello thread !"));
        thread.start();
    }
}


十、Thread 的常见构造方法



fd7434bf31b04295bb8e1e56e32e928f.png


十一、Thread 的几个常见属性



d3ee0b3af00441f59ac5522431369561.png


程序清单5:


public class ThreadDemo5 {
    public static void main(String[] args) {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    //打印当前线程的名字
                    //Thread.currentThread 的这个方法,可以获取到当前线程的实例
                    //哪个线程调用这个方法,就能获取到对应的实例
                    System.out.println(Thread.currentThread().getName());
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        }, "我的线程名字叫十七");
        //thread.start();
        System.out.println("ID: " + thread.getId());
        System.out.println("name: " + thread.getName());
        System.out.println("state: " + thread.getState());
        System.out.println("priority: " + thread.getPriority());
        System.out.println("isDaemon: " + thread.isDaemon());
        System.out.println("isAlive: " + thread.isAlive());
        System.out.println("isInterrupted: " + thread.isInterrupted());
    }
}


输出结果:

我们可以看到在使用 start 方法的前后,线程的状态和存活这两个属性发生了改变。


51e6e6312e5a4d6bab86596bd8b74c92.png


十二、再谈线程与进程



1. 线程之间能够共享的资源


线程之间能够共享的资源主要分为两方面:


① 内存

线程1 和线程 2 都可以共享同一份内存( 同一个变量 )


② 文件

线程1 打开的文件,线程 2 也能去使用。

比方说:我们创建一个 .java 文件,处于同一个进程的两个线程就能够使用里面的各个变量、函数…而创建两个 .java 文件,当某个文件执行的时候,另一个文件不受干扰,这就相当于进程之间互不干扰。


2. 多进程的优势


前面对比了线程与进程,说的最多的就是线程更加轻量,使用多线程能够达到更高效的并发编程。


问:难道进程相比于线程,是一无是处的吗?


答:其实并不是,进程更加具有 " 独立性 "。


例如:在一个操作系统上,同一时刻运行着很多个进程,如果某个进程挂了,那么它并不会影响到其他进程,因为每个进程有各自的地址空间。相比之下,由于多个线程之间共用着同一个进程的地址空间,若某个线程挂了,就很可能会把整个进程也带走。


这很好理解,比方说,我们打开电脑的 QQ 和 浏览器,如果在我们使用浏览器的时候,一个页面卡住了,通常情况下,电脑就会问你:" A. 是否继续等待? B. 是否关闭当前程序? " 这时候,如果你点击关闭程序,你之前在浏览器上访问的其他页面也会随之关闭,然而,我们后台的 QQ 并不会受到影响。


目录
相关文章
|
3月前
线程18
线程18
39 4
|
3月前
|
监控 安全 Java
线程(一)
线程(一)
|
6月前
|
NoSQL Java 应用服务中间件
线程不够用怎么办?
### 并发编程挑战与解决方案概览 - 多线程导致线程爆炸,浪费CPU及可能导致JVM崩溃。线程池缓解问题,但仍有阻塞IO的效率低下。 - 非阻塞IO(如servlet3.1/Tomcat)和事件驱动(Reactive/Future)减少线程使用,但学习曲线陡峭。 - 轻量级线程如Netty、Spring Flux和虚拟线程(Java Loom)提升性能,但普及尚需时日。Java21引入虚拟线程,有望成未来性能关键。
232 10
|
Java C语言 Python
线程那些事
线程那些事
53 0
|
7月前
|
存储 安全 Java
C++线程浅谈
C++线程浅谈
|
算法 NoSQL Java
02.关于线程你必须知道的8个问题(上)
大家好,我是王有志,欢迎来到《Java面试都问啥?》。 今天我们来一起看看在面试中,关于线程各大公司大都喜欢问哪些问题。
110 1
02.关于线程你必须知道的8个问题(上)
|
Java Linux 调度
03.关于线程你必须知道的8个问题(中)
大家好,我是王有志,欢迎来到《Java面试都问啥?》。我们书接上回,继续聊Java面试中关于线程的问题。
84 1
03.关于线程你必须知道的8个问题(中)
|
算法 安全 程序员
线程小练习
线程小练习
|
传感器 存储 自动驾驶
(6)线程
(6)线程
106 0
|
Java 调度
线程小记
线程小记
104 0