玩转多线程--入门

目录

什么是多线程?

概念:

优点:

线程和进程区别:(面试常考题)

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

多线程创建

方法1继承Thread类

方法2实现Runnable接口

star()和run()的区别:(经典面试题)

其他形变

匿名内部类创建Thread子类对象

匿名内部类创建Runnable子类对象

lambda表达式创建Runnable子类对象

Thread类及其常见方法

Thread的常见构造方法:

Thread常见属性

启动一个线程--start()

中断线程

使用共享标记来中断

调用interrupt()方法来通知

总结:

等待线程--join()

获取当前线程的引用

休眠当前线程

线程的状态

线程安全(重点)

线程不安全:

造成线程不安全的原因:

线程调度是随机的

多个线程修改了同一变量

无法保证原子性

无法保证可见性

观于JMM面试题:

为什么要整这么多内存?

为啥要这么麻烦地拷来拷去?

指令重排序


什么是多线程?

概念:

一个线程就是一个执行流,每个线程之间都可以按照自己的顺序执行自己的代码,多个线程之间"同时"执行多份代码。多线程编程其实也可以称作为"并发编程"。

**并发编程:**对于进程也可以实现并发编程,但是和线程相比,线程更轻量

  • 创建线程比创建进程更快

  • 销毁线程比销毁进程更快

  • 调度线程比调度进程更快

优点:

  • 多线程可以充分利用CPU资源去处理一些复杂业务,从而提升业务的效率

  • 一定程度上可以提高程序处理任务效率,创建线程的个数,根据CPU逻辑处理器的数量作为参考

    • 线程个数<逻辑处理器个数:会提升程序处理任务效率

    • 线程个数>逻辑处理器个数:由于过多的线程,导致有较多线程处于阻塞状态,并且线程创建和销毁也会一定程度加重系统开销,可能会降低程序处理任务效率

线程和进程区别:(面试常考题)

  • 进程包含线程,每一个进程至少有一个线程,即主线程

  • 进程和进程之间不共享内存空间,同一个进程之间的线程共享内存空间

  • 进程是系统分配资源的最小单位,线程是系统调度的最小单位

  • 一个进程挂了一般不会影响其它进程,但是一个线程挂了,可能导致整个进程的崩溃

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

  • 线程是操作系统中的概念。操作系统内核实现了线程这样的机制,并且对用户层提供了一些API供用户使用

  • Java标准库中的Thread类可以视为是对操作系统提供的API进一步的抽象和封装

多线程创建

  • Java中线程参与调度执行的步骤:Java中创建一个线程对象->JVM调用系统的API->创建系统中的线程->最终参与CPU调度

  • 线程的执行顺序并没有什么规律,这和CPU的调度有关,由于CPU的调度是"抢占式"执行的,所以哪个线程当前占用CPU资源是不确定的

方法1继承Thread类

  • Thread用来描述一个线程,创建的每一个线程都是Thread的对象

  • 继承Thread类,直接使用this就表示当前对象的引用

java 复制代码
 class MyThread extends Thread{
     //必须要重写Thread类中的run()方法,run()内可以更具业务需求,进行调整
     @Override
     public void run() {
         System.out.println("Mythread");
     }
 }
 public class Test1 {
     public static void main(String[] args) {
         //创建MyThread的实例
         MyThread t1 = new MyThread();
         //调用start()启动线程,线程真正开始运行
         t1.start();
     }
 }

方法2实现Runnable接口

  • 其中只有一个run()方法,面对多个线程时和Thread类相比方法更方便,多个线程执行同一个任务就使用Runnable()接口

  • 实现Runnable接口,this表示的是MyRunnable()的引用,需要使用Thread.currentThread()

java 复制代码
 class MyRunnable implements Runnable{
     @Override
     //重写run()方法
     public void run() {
         System.out.println("MyRunnable");
     }
 }
 public class Test2 {
     public static void main(String[] args) {
         //创建Thread实例,调用Thread构造方法时将Runnable对象作为参数传入
         Thread t1 = new Thread(new MyRunnable());//根据线程需要,传入对应参数
         //调用start启动线程
         t1.start();
     }
 }

star()和run()的区别:(经典面试题)

  • start()真实申请系统线程的PCB,从而启动一个线程,参与CPU调度

  • run()定义线程的时候指定线程要执行的任务,如果直接调用,就是Java一个对象中普通的方法

其他形变

匿名内部类创建Thread子类对象
java 复制代码
Thread t1 = new Thread(){
     @Override
     public void run() {
         System.out.println("匿名内部类创建线程");
     }
 };
匿名内部类创建Runnable子类对象
java 复制代码
 Thread t2 = new Thread(new Runnable() {
     @Override
     public void run() {
         System.out.println("匿名内部类创建线程");
     }
 });
lambda表达式创建Runnable子类对象
  • 最推荐的编码方式

  • Runnable接口是一个函数式接口,可以通过Lambda表达式创建,本质上就是实现了Runnable接口

  • 函数接口:接口中只定义了一个方法

java 复制代码
Thread t3 = new Thread(()->{
             System.out.println("Lambda表达式创建线程");
 });

Thread类及其常见方法

  • Thread类是JVM用来管理线程的一个类,每一个线程都有唯一的Thread对象与之相关联

  • JVM会将这些Thread对象组织起来,用于线程调度,线程管理

Thread的常见构造方法:

方法 说明
Thread() 创建线程对象
Thread(Runnable target) 使用Runnable对象创建线程对象
Thread(String name) 创建线程对象,并命名
Thread(Runnable target, String name) 使用Runnable对象创建线程对象,并命名

Thread常见属性

属性 获取方法
ID getId()
名称 getName()
状态 getState()
优先级 getPriority()
是否后台线程 isDaemon()
是否存活 isAlive()
是否被中断 isInterrupted()
  • ID:线程的唯一标识,不同线程不会重复,JVM默认为Thread对象生成的一个编号,是Java层面的,与PCB区分开

    • Thread是Java中的一个类:创建的Thread对象->调用start方法->JVM调用系统API生成一个PCB->PCB与Thread对象一一对应
  • 名称:线程的名称

  • 状态:表示线程所处的情况

  • 优先级高的线程理论上来说更容易被调度到

  • 关于后台线程:JVM会在一个进程的所有非后台进程结束后,才会停止运行,前台线程可以阻止线程的退出

  • 是否存活,可以简单理解为run()方法是否执行结束

启动一个线程--start()

  • 覆写run()方法仅仅是提供了线程的任务清单

  • 调用start方法,才真正申请系统线程PCB,从而启动一个线程,参与CPU调度

中断线程

  • 线程执行到一半需要停止,通过一个信号使线程退出

  • 方案:

    • 通过共享标记来进行沟通

    • 调用interrupt()方法来通知

使用共享标记来中断
java 复制代码
public class Test {
     //设置标志位,变量用volatile修饰,保证内存可见性,后续再线程安全解决会提到
     public static volatile boolean isQuit = false;
     public static void main(String[] args) {
         //线程中断演示
         Thread t1 = new Thread(()->{
             while(!isQuit){
                 System.out.println(Thread.currentThread().getName()+"正常工作,没有被中断!");
                 try {
                     Thread.sleep(1000);
                 } catch (InterruptedException e) {
                     throw new RuntimeException(e);
                 }
             }
             System.out.println(Thread.currentThread().getName()+"被中断,停止任务进行!");
         });
         System.out.println(Thread.currentThread().getName()+"发出中断指令!");
         isQuit = true;
         System.out.println(Thread.currentThread().getName()+"结束发出中断命令!");
         t1.start();
     }
 }
调用interrupt()方法来通知
java 复制代码
 public class Test2 {
     public static void main(String[] args) throws InterruptedException {
         //线程中断演示
         Thread t1 = new Thread(() -> {
             while (!Thread.interrupted()) {
                 System.out.println(Thread.currentThread().getName() + "正常工作,没有被中断!");
                 try {
                     Thread.sleep(1000);
                 } catch (InterruptedException e) {
                     throw new RuntimeException(e);
                 }
             }
             System.out.println(Thread.currentThread().getName() + "被中断,停止任务进行!");
         });
         //启动线程
         t1.start();
         //线程休眠
         Thread.sleep(10000);
         //发出中断信号
         t1.interrupt();
     }
 }

注意 :

  • 如果线程处于运行状态,直接中断线程,不会报异常,符合程序预期

  • 如果线程处于等待状态,就会报一个中断异常

  • 下图所示第一张为上述代码所报异常

  • 修改catch处理逻辑后代码
总结:
  • 调用interrupt()方法来通知,如果线程因为调用wait/join/sleep等方法而阻塞挂起,则以InerruptedException异常的形式通知

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

等待线程--join()

  • 等待一个线程执行完毕
方法 说明
public void join() 等待线程到结束
public void join(long millis) 最多等待millis毫秒
public void join(long millis,int nanos) 更高精度

获取当前线程的引用

java 复制代码
 public static void main(String[] args) {
         Thread t1 = Thread.currentThread();//获取当前线程对象的引用
         System.out.println(t1.getName());
 }
 //一般可以连起来使用Thread.currentThread()+方法
 Thread.currentThread().getName()

休眠当前线程

  • 因为线程的调度是不可控的,所以,这个方法只能保证实际休眠时间是大于等于参数设置的休眠时间的
java 复制代码
public static void main(String[] args) throws InterruptedException {
    //获取当前毫秒
    System.out.println(System.currentTimeMillis());
    Thread.sleep(3*1000);
    System.out.println(System.currentTimeMillis());
}

线程的状态

  • 线程的状态是一个枚举类型Thread.State
java 复制代码
public static void main(String[] args) {
    for (Thread.State state:Thread.State.values()) {
        System.out.println(state);
    }
}

**面试题:**共六种线程状态

  • **NEW:**安排了工作,但是还未执行,创建好了一个线程对象,没有调用start()方法之前是不会创建PCB的

  • RUNNABLE:可工作的,包含正在工作中和即将开始工作->运行+就绪,此时系统中有很多PCB

  • BLOCKED:等待锁的状态,阻塞的一种

  • WAITING:没有等待时间,处于一直死等的状态

  • TIMED_WATING:设置等待时间的等待状态,过时不候

  • **TERMINATED:**线程执行完成,PCB已经销毁,但是Java对象还在

**注意:**对于isAlive()方法,可以认为处于不是NEW和TERMINATED状态的都是活着的

线程安全(重点)

线程不安全:

  • 如果多线程状态下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说明这个程序是线程安全的

造成线程不安全的原因:

线程调度是随机的
  • 这是线程安全的罪魁祸首

  • 随机调度使一个线程在多线程环境下,执行顺序存在很多变数

  • 代码编写人员必须保证在任意执行顺序之下,代码都能正常运行

多个线程修改了同一变量
  • 多个线程修改不同变量,不会出现线程安全问题,一个线程修改一个变量,也不会出现线程安全问题
无法保证原子性
  • 原子性:要么全部执行,要么全部不执行

    • 例如:count++这个操作,对应几条CPU指令

      • LOAD:从内存或者寄存器中读取count的值

      • ADD:执行自增

      • STORE:把计算结果写回寄存器或内存

    • 如果能保证原子性,当执行count++代码的时候,上述三条指令连续执行,不会被打断

  • 无法保证原子性可能带来的问题:可能会发生覆盖现象

    • 如果一个线程正在对一个变量操作,这时中途插入其他线程,这个操作会被打断,结果就可能产生覆盖

      • 这点也和线程抢占式调度密切相关,如果线程不是抢占式的,就算没有原子性,也问题不大
  • 一条Java语句不一定使原子的,也不一定使一条指令

无法保证可见性
  • 可见性:一个线程对共享变量值的修改,能够及时地被其它线程看到,可以通过某种方式,让线程之间相互通信

  • **Java内存模型(JMM):**Java虚拟机规范中定义了Java内存模型

    • 线程之间的共享变量存储在主内存中

    • 每一个线程都有自己的"工作内存"

    • 当线程要读取一个共享变量的时候,会先把变量从主内存中读取到自己的工作内存中,再从工作内存中读取数据

    • 当线程要修改一个共享变量的时候,会先修改工作内存中的副本,再同步回主内存

观于JMM面试题:
  • 所有线程不可以直接修改内存中的变量

  • 如果要修改,需要把这个变量从主内存中复制到自己的工作内存中

  • 各个线程之间无法相互通信,做到了内存级别的线程隔离

为什么要整这么多内存?
  • 实际上没有这么内存,这只是Java规范中的一个术语,是属于"抽象"的叫法

  • 所谓"主内存"才是真正硬件角度的"内存",而所谓"工作内存",则是指CPU的寄存器和高速缓存

为啥要这么麻烦地拷来拷去?
  • 因为CPU访问自身寄存器速度以及高速缓存速度,远远超过访问内存地速度(快了3-4个数量级,也就是几千倍,上万倍)

  • 那访问寄存器这么好,要啥内存?--因为太贵

    • 价格排序:CPU寄存器>内存>硬盘

    • 访问速度:CPU寄存器>内存>硬盘

指令重排序
  • 由于一条Java语句可能对应多条机器指令

  • 我们写的代码在编译之后可能会与代码对应的指令顺序不同,这个过程就是指令的重排序

  • 编译器对于指令重排序的前提是"保持逻辑不发生变化",这一点在单线程下比较容易判断,但是在多线程环境下就没有那么容易,多线程代码执行复杂程度高,编译器很难在编译阶段对代码的执行效果进行预测,因此激进的重排序很容易导致优化后的逻辑和之前不等价

xdm多线程需要解决的内容非常多,一篇文章不足已掌握,关于线程安全的解决请听下回分解!!

相关推荐
程序员buddha9 分钟前
2025年华为OD上机考试真题(Java)——数组连续和
java·开发语言·华为od
ac-er888813 分钟前
Go语言如何实现高性能缓存服务
开发语言·缓存·golang
SyntaxSage17 分钟前
Scala语言的面向对象编程
开发语言·后端·golang
Linux520小飞鱼23 分钟前
Go语言的循环实现
开发语言·后端·golang
CyberScriptor23 分钟前
PHP语言的字符串处理
开发语言·后端·golang
wfsm29 分钟前
uniapp中h5使用地图
开发语言·javascript·uni-app
还是车万大佬36 分钟前
C语言与ASCII码应用之简单加密
c语言·开发语言·算法
Eiceblue1 小时前
Python 在PDF中添加数字签名
开发语言·vscode·python·pycharm·pdf
DevOpsDojo1 小时前
Perl语言的数据结构
开发语言·后端·golang
浪前1 小时前
【算法】移除元素
开发语言·数据结构·算法