认识线程、Java多线程编程、Thread类及常见方法

一、认识线程

上篇我们介绍了操作系统基础知识,对进程有了一定理解,接下来我们主要学习线程!

1.1 线程是什么

一个线程就是一个 “执行流”,每个线程之间都可以按照顺序执行自己的代码,多个线程之间 “同时” 执行着多份代码。

还是回到我们之前的银行的例子中。之前我们主要描述的是个人业务,即一个人完全处理自己的业务。我们进一步设想如下场景:
一家公司要去银行办理业务,既要进行财务转账,又要进行福利发放,还得进行缴社保。如果只有张三一个会计就会忙不过来,耗费的时间特别长。为了让业务更快的办理好,张三又找来两位同事李四、王五一起来帮助他,三个人分别负责一个事情,分别申请一个号码进行排队,自此就有了三个执行流共同完成任务,但本质上他们都是为了办理一家公司的业务。
此时,我们就把这种情况称为多线程,将一个大任务分解成不同小任务,交给不同执行流就分别排队执行。其中李四、王五都是张三叫来的,所以张三一般被称为主线程(Main Thread)。

1.2 为什么要有线程

首先, “并发编程” 成为 “刚需”

  • 单核CPU的发展遇到了瓶颈。要想提高算力,就需要多核 CPU。而并发编程能更充分利用多核CPU资源。(CPU再往小了做就比较困难了)
  • 有些任务场景需要"等待 IO",为了让等待IO的时间能够去做一些其他的工作,也需要用到并发编程。

其次,虽然多进程也能实现并发编程,但是线程比进程更轻量
线程为啥更"轻"?把申请资源/释放资源的操作给省下了~~

  • 创建线程比创建进程更快;
  • 销毁线程比销毁进程更快;
  • 调度线程比调度进程更快。

增加线程数量并不是可以一直提高速度,CPU核心数量是有限的!线程太多,而核心数量有限,不少的开销反而浪费在线程调度上了!!!

最后,线程虽然比进程轻量,但是人们还不满足,于是又有了 “线程池”(ThreadPool) 和 “协程”(Coroutine)。

1.3 进程和线程的区别

  • 进程是包含线程的,每个进程至少有一个线程存在,即主线程
  • 进程和进程之间不共享内存空间;同一个进程的线程之间共享同一个内存空间。

比如之前的多进程例子中,每个客户来银行办理各自的业务,但他们之间的票据肯定是不想让别人知道的,否则钱不就被其他人取走了么。而上面我们的公司业务中,张三、李四、王五虽然是不同的执行流,但因为办理的都是一家公司的业务,所以票据是共享着的。这个就是多线程和多进程的最大区别。

在这里插入图片描述

  • 进程是系统分配资源的最小单位,线程是系统调度的最小单位。
    谈到"调度"已经和进程没啥关系了,进程专门负责资源分配,线程来接管和调度相关的一切内容。上篇讲的进程调度的过程和当前线程调度完全一致!

在这里插入图片描述
在这里插入图片描述

1.4 线程PCB

一个线程也是通过一个PCB来描述的;一个进程里面可能是对应一个也可能是对应多个PCB了。
之前介绍的PCB里的状态、上下文、优先级、记账信息都是每个线程有自己的,各自记录各自。但是同一个进程里的PCB之间,pid是一样的,内存指针和文件描述符表也是一样的
操作系统对于调度,只认pcb。(系统内核里看不到"进程","线程"这样的概念)!一个进程可以包含一个线程(一个pcb)或者是多个线程(多个pcb),同一时刻操作系统上有很多的进程,内核里就管理着几十号、上百号pcb,这些pcb轮番去系统上调度执行,但操作系统并不关心这些pcb哪些是一伙(哪些是一个进程的)!!!

1.5 线程安全问题

什么时候会有安全问题?多个执行流访问同一个共享资源的时候。
线程模型天然就是资源共享的,多个线程争抢同一个资源 (同一个变量)非常容易触发线程安全问题;进程模型天然是资源隔离的,不容易触发,但进行进程间通信的时候,多个进程访问同一个资源可能会出问题~~

不同线程可能会去争抢同一份资源,这就导致了线程安全问题。(而在多进程中不会出现这样的情况,因为进程已经把资源分配好了,每个进程各自利用各自的资源~)

并且系统创建线程也是要消耗资源的(虽然比进程轻量,但也不是0),如果你创建太多线程会使资源耗尽,导致别的进程用不了!这里耗尽的资源是指CPU、内存、带宽这样的资源…

多线程还可能有一种情况:一个线程抛异常,如果处理不好,很可能把整个进程都给带走了,其他线程也就挂了~~

1.6 Java的线程 和 操作系统线程 的关系

线程是操作系统中的概念,操作系统内核实现了线程这样的机制,并且对用户层提供了一些API供用户使用 (例如Linux的pthread库);Java标准库中Thread类可以视为是对操作系统提供的API进行了进一步的抽象和封装。

二、Java多线程编程

感受多线程程序和普通程序的区别:

  • 每个线程都是一个独立的执行流;
  • 多个线程之间是 “并发” 执行的。

Java操作多线程,最核心的类:Thread(java.lang 包中,自动导入,同String、StringBuilder、StringBuffer)

2.1 第一个多线程程序

代码示例:

class MyThread extends Thread{
    @Override
    public void run(){
        while(true){
            System.out.println("hello thread");
            // 为了让这里的打印慢点方便看,加个sleep,休眠1s
            try{
                Thread.sleep(1000);
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        }
    }
}

public class ThreadDemo1 {
    public static void main(String[] args){
        Thread t = new MyThread();
        t.start();        // 如果调用t.run(),依然是单线程,会在run方法的循环里出不来!

        while(true){
            System.out.println("hello main");
            // 为了让这里的打印慢点方便看,加个sleep,休眠1s
            try{
                Thread.sleep(1000);
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        }
    }
}

执行结果:
在这里插入图片描述

在这里插入图片描述
new Thread对象操作不创建线程。(说的线程指的是系统内核里的PCB) 调用start才是创建PCB,才是货真价实的线程!
PCB只是操作系统书里说的概念,实际上Linux对应结构体名字叫做task_struct~~
如果run方法执行完毕,新的这个线程自然销毁!!!
在这里插入图片描述
start和run之间的区别:start是真正创建了一个线程 (从系统这里创建的),线程是独立的执行流;run只是描述了线程要干的活儿,如果直接在main中调用run,此时没有创建新线程,全是main线程一个人干活!!!

2.2 jconsole工具观察线程

在这里插入图片描述
在这里插入图片描述
补充:
1)main结束销毁后,thread-0会销毁吗?
这要看守护线程/非守护线程,默认是不会的~
2)调用栈(堆栈跟踪):描述了当前方法之间的调用关系
在这里插入图片描述

2.3 创建线程

Java中大致有五种创建线程的写法:
1)继承 Thread,重写run

class MyThread extends Thread{
    @Override
    public void run(){
        while(true){
            System.out.println("hello thread");
            // 为了让这里的打印慢点方便看,加个sleep,休眠1s
            try{
                Thread.sleep(1000);
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        }
    }
}

public class ThreadDemo1 {
    public static void main(String[] args){
        Thread t = new MyThread();
        t.start();        // 如果调用t.run(),依然是单线程,会在run方法的循环里出不来!

        while(true){
            System.out.println("hello main");
            // 为了让这里的打印慢点方便看,加个sleep,休眠1s
            try{
                Thread.sleep(1000);
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        }
    }
}

2)实现Runnable接口

// Runnable作用,是描述一个“要执行的任务”,run方法就是任务的执行细节
class MyRunnable implements Runnable{
    @Override
    public void run() {
        System.out.println("hello thread");
    }
}

public class ThreadDemo2 {
    public static void main(String[] args){
        // 这只是描述了一个任务
        Runnable runnable = new MyRunnable();
        // 把任务交给线程来执行
        Thread t = new Thread(runnable);
        t.start();
    }
}

这里的Runnable只是为了描述线程干的活儿是啥。

这种写法就涉及到了"解耦合",目的就是为了让线程与线程要干的活儿分离开!
未来如果要改代码,不用多线程了,而使用多进程、或者线程池、或者协程… 此时代码改动比较小~~

3)使用匿名内部类,继承 Thread

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

过程:
1.创建了一个Thread的子类 (子类没有名字,所以"匿名");
2.创建了子类的实例,并且让 t引用指向该实例。

4)使用匿名内部类,实现Runnable

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

此处是创建了一个类,实现Runnable,同时创建了该类的实例,并且传给Thread的构造方法。

5)使用Lambda表达式

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

最简单,推荐写法~
Lambda表达式详解!

三、Thread 类及常见方法

Thread 类是 JVM 用来管理线程的一个类,换句话说,每个线程都有一个唯一的 Thread 对象与之关联。
用我们上面的例子来看,每个执行流,也需要有一个对象来描述,类似下图所示,而Thread 类的对象就是用来描述一个线程执行流的,JVM 会将这些 Thread 对象组织起来,用于线程调度,线程管理。
在这里插入图片描述

时刻牢记,线程之间是并发执行的,并且是抢占式调度的!
两个线程在微观上可能是并行 (两个核心),也可能是并发的 (一个核心)。应用程序这里宏观上感知不到,咱们看到的始终是随机执行、抢占式调度…
以前咱们写代码,只要读懂代码的顺序,即就是固定按照从上到下的顺序来执行的…但是现在并不是了,理解多线程代码要考虑无数种顺序~~

3.1 构造方法

方法说明
Thread()创建线程对象
Thread(Runnable target)使用 Runnable 对象创建线程对象
Thread(String name)创建线程对象,并命名 (方便调试)
Thread(Runnable target, String name)使用 Runnable 对象创建线程对象,并命名
【了解】Thread(ThreadGroup group,Runnable target)线程可以被用来分组管理,分好的组即为线程组,目前我们了解即可
Thread t1 = new Thread();
Thread t2 = new Thread(new MyRunnable());
Thread t3 = new Thread("这是我的名字");
Thread t4 = new Thread(new MyRunnable(), "这是我的名字");

t1、t2…是代码里的变量名,thread-0…“这是我的名字”…是系统里的线程名!

3.2 Thread 的几个常见属性

属性获取方法
IDgetId()
名称getName()
状态getState()
优先级getPriority()
是否后台线程 (守护线程)isDaemon()
是否存活isAlive()
是否被中断isInterrupted()
  • ID 是线程的唯一标识,不同线程不会重复
  • 名称是各种调试工具用到
  • 状态表示线程当前所处的一个情况,下面我们会进一步说明
  • 优先级高的线程理论上来说更容易被调度到
  • 关于后台线程,需要记住一点:JVM会在一个进程的所有非后台线程结束后,才会结束运行。
  • 是否存活,即简单的理解,为 run 方法是否运行结束了
  • 线程的中断问题,下面我们进一步说明

在这里插入图片描述
isDaemon():
前台线程,会阻止进程结束。前台线程的工作没做完,进程是完不了的;
后台线程,不会阻止进程结束。后台线程工作没做完,进程也是可以结束的。
代码里手动创建的线程,默认都是前台的,包括main默认也是前台的;
其他的 JVM 自带的线程都是后台的,也可以手动使用setDaemon()设置成后台线程。后台线程即守护线程。
在这里插入图片描述
把t设置成守护线程/后台线程,此时进程的结束就和t无关了~

isAlive():
在真正调用start之前,调用 t.isAlive() 就是false;调用start之后,isAlive() 就是true。
另外,如果内核里线程把run干完了,此时线程销毁,PCB随之释放。但是 Thread t 这个对象还不一定被释放,此时 t.isAlive() 也是false。
即isAlive()是在判断当前系统里面的这个线程是不是真的有了!
如果t的run还没跑,isAlive就是false;如果t的run正在跑,isAlive就是true;如果t的run跑完了,isAlive就是false!

在这里插入图片描述
注意:

System.out.println(Thread.currentThread());

这里得到的是thread实例,直接打印thread实例得到的结果是Thread类toString方法得到的结果。(具体怎么写,点进Thread源码实现);而一般情况下只需要获得线程名:即Thread.currentThread().getName();

3.3 启动一个线程-start()

之前我们已经看到了如何通过覆写 run 方法创建一个线程对象,但线程对象被创建出来并不意味着线程就开始运行了,覆写 run 方法是提供给线程要做的事情的指令清单。
调用 start 方法,才真的在操作系统的底层创建出一个线程。

在这里插入图片描述

在这里插入图片描述

3.4 中断一个线程-interrupt()

中断的意思是:不是让线程立即就停止,而是通知线程你应该要停止了。但是否真的停止取决于线程这里具体的代码写法!

目前常见的有以下两种方式:

  1. 通过共享的标记 / 标志位来进行沟通
private static boolean flag = true;

在这里插入图片描述

  1. 调用 interrupt() 方法来通知 / 使用Thread自带的标志位来进行判定

Thread 内部包含了一个 boolean 类型的变量作为线程是否被中断的标记。

方法说明
public void interrupt()中断对象关联的线程,如果线程正在阻塞,则以异常方式通知,否则设置标志位
public boolean isInterrupted()判断对象关联的线程的标志位是否设置,调用后不清除标志位
public static boolean interrupted()判断当前线程的中断标志位是否设置,调用后清除标志位

在这里插入图片描述
main线程调用t.interrupt()相当于main通知t你要终止了~~
在这里插入图片描述
在这里插入图片描述
调用interrupt()只是告诉线程,你应该终止了,但是它是不是真的要终止,这是它自己的事情!!!
sleep()被唤醒是因为触发了InterruptedException异常
在这里插入图片描述
为啥sleep()要清除标志位?唤醒之后,线程到底要不要终止、是立即终止还是稍后,就把选择权交给了程序猿自己!
举个例子,有一天我正在打游戏,娘亲突然喊我下楼去买瓶酱油,此时我有三个选择:
1.放下游戏立即就去;2.打完这把再去,稍后处理;3.假装没听见,就完全不处理~~

thread 收到通知的方式有两种:
1)如果线程因为调用 wait/join/sleep 等方法而阻塞挂起,则以 InterruptedException 异常的形式通
知,清除中断标志

  • 当出现 InterruptedException 的时候, 要不要结束线程取决于 catch 中代码的写法. 可以选择忽略这个异常, 也可以跳出循环结束线程~

2)否则,只是内部的一个中断标志被设置,thread 可以通过

  • Thread.interrupted() 判断当前线程的中断标志被设置,清除中断标志
  • Thread.currentThread().isInterrupted() 判断指定线程的中断标志被设置,不清除中断标志

这种方式通知收到的更及时,即使线程正在 sleep 也可以马上收到。

为啥不设计成A让B终止,B就立即终止呢?:AB之间是并发执行、随机调度的。因此B这个线程执行到哪里了A是不清楚的,立即终止可能会导致一些bug。

3.5 等待一个线程-join()

有时,我们需要等待一个线程完成它的工作后,才能进行自己的下一步工作。例如,张三只有等李四转账成功,才决定是否存钱,这时我们需要一个方法明确等待线程的结束。
线程是一个随机调度的过程所以不容易控制,而等待线程做的事情就是在控制两个线程的结束顺序
在这里插入图片描述
如果是执行 t.join() 的时候,t 已经结束了。join不会阻塞,就会立即返回~

方法说明
public void join()等待线程结束
public void join(long millis)等待线程结束,最多等 millis 毫秒
public void join(long millis, int nanos)同理,但可以更高精度

无参数:“死等”,不见不散;有参数:指定一个超时时间(最大等待时间),这种操作方式是更常见的~ 死等很容易出问题!
join()需要对InterruptedException异常进行处理!

3.6 获取当前线程引用-currentThread()

在哪个线程中调用,就能获取到哪个线程的实例!

方法说明
public static Thread currentThread();返回当前线程对象的引用
public class ThreadDemo {
    public static void main(String[] args) {
        Thread thread = Thread.currentThread();
        System.out.println(thread.getName());
   }
}

3.7 休眠当前线程-sleep()

方法说明
public static void sleep(long millis) throws InterruptedException休眠当前线程 millis毫秒
public static void sleep(long millis, int nanos) throws InterruptedException可以更高精度的休眠

让线程休眠,本质上就是让这个线程不参与调度了 (不去CPU上执行了)。

在这里插入图片描述
PCB是使用链表来组织的 (并不具体),实际情况并不是一个简单的链表,其实这是一系列以链表为核心的数据结构。
一旦线程进入阻塞状态,对应的PCB就进入阻塞队列了,此时就暂时无法参与调度了。
比如调用sleep(1000),对应的线程PCB就要在阻塞队列中待1000ms这么久,但是当这个PCB回到了就绪队列就会被立即调度吗?当然不!虽然是sleep (1000),但是实际上考虑到调度的开销,对应的线程是无法在被唤醒之后立即就执行的,所以实际上的时间间隔大概率要大于1000ms。即因为线程的调度是不可控的,所以,这个方法只能保证实际休眠时间是大于等于参数设置的休眠时间的!
挂起(hung up)就是阻塞(block) ,一个意思~~
sleep()需要对InterruptedException异常进行处理!

3.8 例题

例一:有20个线程,需要同时启动,每个线程按0-19的序号打印,如第一个线程需要打印0。请设计代码,在main主线程中,等待所有子线程执行完后,再打印 ok。

    public static void main(String[] args) throws InterruptedException {
        Thread[] threads = new Thread[20];
        for(int i=0; i<20; i++){
            final int n = i;
            threads[i] = new Thread(new Runnable() {
                @Override
                public void run() {//内部类使用外部的变量,必须是final修饰
                    System.out.println(n);
                }
            });
        }
        for(Thread t : threads){
            t.start();
        }
        for(Thread t : threads){//同时执行20个线程,再等待所有线程执行完毕
            t.join();
        }
        System.out.println("ok");
    }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

yyhgo_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值