【多线程-从零开始-壹】线程的五种创建方法

简介: 【多线程-从零开始-壹】线程的五种创建方法

线程

  • 可以理解成更轻量的进程,也能解决[[01 计算机是如何工作的#^87b85a|并发编程]]的问题,但是创建/销毁的开销,要比进程更低
  • 因此多线程编程就成了当下主流的并发编程方式
  • 这个基本上是我们以后工作中天天用到,面试中考查的重点
    #高频面试
  • 在系统中,线程同样是通过 [[01 计算机是如何工作的#^7eb7b0|PCB]] 来描述的(Linux)
  • 一个进程,是一组 PCB
  • 一个线程,是一个 PCB
  • 一个进程中可以包含多个线程
  • 此时每个线程都可以独立的去 CPU 上调度执行
  • 线程是系统“调度执行”的基本单位
  • 进程是系统“资源分配”的基本单位
  • 一个可执行程序运行的时候(双击)
  • 操作系统会创建进程,给这个程序分配各种系统资源(CPU,内存,硬盘,网络带宽…)
  • 同时也会在这个进程中创建一个或多个线程,这些线程再去 CPU 上调度执行
  • 同一个进程中的线程,共用同一份资源
  • 线程比进程更轻量主要体现在可以资源共用
  1. 创建线程,省去了“分配资源”的过程
  2. 销毁进程,省去了“释放资源”的过程
  • 一旦创建进程,同时也会创建第一个线程==>分配资源,时间相对来说较慢
  • 一旦后续创建第二个、三个线程,就不必再重新分配资源了,用创建第一个线程时分配的资源
  • 当线程数目越来越多之后,此时效率也没办法进一步提升了(桌子的空间是有限的),当滑稽老铁数目达到一定程度后,有些人就够不到桌子了,就吃不到了
  • 能够提升效率,关键是充分利用多核心进行“并行执行
  • 如果只是“微观并发”,速度是没有提升的,真正能提升速度的是“并行
  • 如果线程数目太多,比如超出了 CPU 核心数目,此时就无法在微观上完成所有线程的“并行执行”,势必会存在严重的竞争
  • 当线程多了之后,此时就容易发生“冲突”
  • 由于多个线程,使用的是同一份资源(内存资源
  • 若多个线程针对同一个变量进行读写操作(尤其是写操作),就容易发生冲突
  • 一旦发生冲突,就可能使程序出现问题==>“线程安全问题
  • 一旦某个线程抛出异常,这个时候,如果不能妥善处理,就可能导致整个进程都崩溃,因此其他线程就会随之崩溃


  • 进程和线程的概念与区别(高频面试题,操作系统话题下出场频率最高的问题)#高频面试
  1. 进程包含线程
  • 一个进程里面可以有多个线程
  1. 进程是系统资源分配的基本单位
    线程是系统调度执行的基本单位
  2. 同一个进程的线程之间,共用一份系统资源(内存,硬盘,网络带宽等)
  • 尤其是“内存资源”,就是代码中定义的变量/对象…
  • 编程中,多个线程,是可以共用一份变量的
  1. 线程是当下实现并发编程的主流方式,通过多线程,就可以充分利用好多核 CPU
  • 但也不是线程数越多就一定越好,当线程数达到一定的程度,把多个核心都利用充分之后,再增加线程,就无法再提高效率了,甚至可能会影响效率(线程调度也是有开销的)
  1. 多个线程之间可能会相互影响
  • 线程安全问题:一个线程抛出异常,也可能会把其他线程也一起带走
  1. 多个进程之间一般不会相互影响
  • 进程的隔离型:一个进程崩溃了,不会影响其他进程

在 Java 代码中编写多线程程序

  • 线程本身是操作系统提供的概念,操作系统提供 API 供程序猿调用
  • 但不同的系统,提供的 API 是不同的(Windows 创建线程的 API 和 Linux 的差别非常大)
  • Java(JVM)把这些系统 API 封装好了,咱们不需要关注系统原生 API,只需要了解好 Java 提供的这一套 API 就好了

Thread 标准库

  • 这个类负责完成多线程的相关开发

创建线程的写法

1 . 继承 Thread 类

代码

package thread;  
  
class MyThread extends Thread {  
    //继承Thread类的目的是重写里面的run()方法  
    @Override  
    public void run() {  
        //这里写的代码就是即将创建的线程所需要执行的逻辑  
        while (true) {  
        System.out.println("hello thread");  
        //休眠操作,避免CPU消耗过大  
        try {  
            Thread.sleep(1000);  
        } catch (InterruptedException e) {  
            throw new RuntimeException(e);  
        }  
      }
    }
}  
  
public class Demo1 {  
    public static void main(String[] args) {  
        MyThread t = new MyThread();  
        //创建线程,与主线程各自独立,并发执行  
        t.start();  
        //t.run(); 不会创建新线程,在主线程中执行,但执行不到
        //因为由于不是线程,所以不会并发执行,所以一直执行创建的MyThread线程,死循环
        
        while(true) {  
        System.out.println("hello main");  
        try {  
            Thread.sleep(1000);  
        } catch (InterruptedException e) {  
            throw new RuntimeException(e);  
        }  
    }
    }  
}

这个代码运行起来是一个进程,但包含了两个线程

主线程——>main 方法(每个进程必有)

自创建的新线程——>t.start()

随后主线程和新线程就会并发/并行的在 CPU 上执行

  1. 创建一个类,继承于标准库的Thread,并重写run()方法
  • Thread 类可以直接被继承,因为它来自 java. lang 这个包,而这个包是默认导入的,里面所有的类都可以直接使用
  • 继承 Thread 类的主要目的是重写 run() 方法
  • run() 中的方法就是即将创建出的线程要执行的逻辑
  1. main方法里创建刚才那个类的实例,再使用提供的start()方法来创建线程
  • 调用 run() 就会在进程内部创建出一个新的线程,新的线程就会执行刚才 run 里面的代码
  • 线程明细:
  1. 主线程:调用 main 函数的方法需要一个专门的线程来执行,称为主线程
  2. t1.start();:这是创建的一个新进程,与主线程之间是并发/并行关系在 CPU 上执行
  • 这里调用 start() 是创建了一个新的线程,随后执行新线程里面的逻辑,而直接调用 run() 方法的话不会创建新的线程
  1. 运行结果

回调函数

非常重要的概念

  • run() 方法并没有被手动调用,但是最终也执行了,
  • 这种被用户定义了,但没手动调用的方法,最终是被系统/库/框架调用了,此时这样的方法就叫“回调函数(callback)
  • Java 数据结构中,优先级队列(堆),必须先定义好对象的“比较规则”
  • Comparable==>compareTo
    comparator==>compare
    都是自己定义了,但没有调用,此时都是由标准库本身内部的逻辑负责调用的

休眠操作:sleep()

  • 可以让循环每循环一次就休息一下,避免 CPU 消耗过大。单位是 ms(毫秒),1000 ms = 1 s
  • 这是一个静态方法类方法
  • 可以直接通过类名进行访问 类名. 方法名,不需要实例化对象,通过对象来访问
  • 这里会报错,使用Alt+Enter
  • IDEA 自动生成 try/catch,catch 中默认的代码有两种风格
  1. 再次抛出一个异常:throw new RuntimeException(e);
  2. 只是打印异常信息:e.printStackTrace();
  • 但实际开发中不止这俩
  • 可能进行“重试
  • 可能进行“回滚
  • 可能会通过短信/邮件/微信/电话向程序猿报警

抢占式执行

  • 多个线程之间,谁先去 CPU 上调度执行,这个过程是“不确定的”,这个调度顺序取决于内核里面的“调度器”的实现
  • 调度器里面有一套规则,但是我们作为程序开发没法进行干预,也感受不到,只能把这个过程近似于随机

观察线程

jconsole
  • 可以借助第三方工具来看这两个进程的情况
  • JDK 中的 bin 目录(binary 二进制,里面放的都是可执行程序)
  • 通过这个可以看到 Java 中进程运行情况
  • 远程进程:其他机器上的进程,需要通过网络连接
  • 本地进程:正在运行的进程
  • 一个 Java 线程中,不仅仅只有一个线程,其实有很多
  • 代码中自己创建的线程命名的规律是 Thread-数字
  • 主要的线程(主线程和自己创建的线程)
  • 调用栈(非常有用)
    当代码出现问题,抛出异常,进程终止时,可以查看对应的调用栈找到出现问题的语句,以及这个代码是如何一层一层被调用过去的
  • 其他进程:主要起到辅助作用
  1. 垃圾回收:在合适的时机,释放不使用的对象
  2. 统计信息/调试信息:比如现在通过 jconsole 能查看到一个 Java 进程的详情
IDEA 内置调试器
  • 通过 IDEA 内置的调试器,也能看到类似的信息

2 . 实现 Runnable 接口

代码

package thread;  
  
//通过Runnable的方式来创建线程  
class MyRunnable implements Runnable {  
    
    @Override  
    public void run() {  
        //描述线程要完成的逻辑  
        while (true) {  
            System.out.println("hello thread");  
            try {  
                Thread.sleep(1000);  
            } catch (InterruptedException e) {  
                throw new RuntimeException(e);  
            }        
        }    
    }
}  
   
public class Demo2 {  
    public static void main(String[] args) throws InterruptedException {  
        MyRunnable runnable = new MyRunnable();  
        Thread thread = new Thread(runnable);  
    //通过Thread创建线程,线程要执行的任务是通过Runnable来描述的,不是通过Thread自己
        
        thread.start();  
      
        while (true) {  
            System.out.println("hello main");  
            Thread.sleep(1000);  
        }
  }
}

因为 Runnable 是一个 interface(接口),所以要用 implements(实现)

Runnable 是用来描述“要执行的任务”是什么

通过 Thread 创建线程,线程要执行的任务是通过 Runnable 来描述的,不是通过 Thread 自己来描述的

两种本质上差别不大,第二种更利于“解耦和

这个 Runnable 只是一个任务,并不与“线程”这样的概念强相关

后续执行这个任务的载体可以是线程,也可以是其他的东西


[!quote] 协程纤程

  • 线程是轻量级进程,但进程很重
  • 随着对性能要求的提高,开始嫌弃线程,引入协程
  • 也叫做虚拟线程

3. 匿名内部类创建 Thread ⼦类对象

本质是继承 Thread,和 1 一样

代码

package thread;  
  
public class Demo3 {  
    public static void main(String[] args) throws InterruptedException {  
        Thread thread = new Thread() {  
        //这里就是在定义匿名内部类,这个类是Thread的子类  
            public void run() {  
                //在类内部重写run方法  
                while (true) {  
                    System.out.println("hello thread");  
                    try {  
                        Thread.sleep(1000);  
                    } catch (InterruptedException e) {  
                        throw new RuntimeException(e);  
                    }                
                }           
            }        
        };        
        thread.start();  
          
        while (true) {  
            System.out.println("hello main");  
            
            Thread.sleep(1000);  
        }    
    }
}

匿名内部类

[!quote] 匿名内部类

  • 一般是一次性的,用完就丢了
  • 内聚性更好一些
  • 相关联的代码放的越集中,内聚性越好
Thread thread = new Thread() {  //这里就是在定义匿名内部类,这个类是Thread的子类  
    public void run() {  
        //在类内部重写run方法   
    }
};
  • 这一段代码的解释
  1. 定义匿名内部类,这个类是 Thread 的子类
  2. 类的内部,重写了父类的 run 方法
  3. 创建了一个子类的实例,并且把实例的引用复制给了 thread

4.匿名内部类创建Runnable⼦类对象

本质是通过匿名内部类实现

代码

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

5.lambda 表达式创建 Runnable ⼦类对象

  • 本质上就是匿名内部类,是一个更简化的写法
  • 很多时候,写“匿名内部类”,目的不是写“类”,而是为了写那个 run() 方法
  • lambda 可以直接表示我们要写的 run() 方法,省去了一些不需要的部分
public static void main(String[] args) {  
    Thread thread = new Thread(() -> {  
        while(true){  
            System.out.println("hello thread");  
            try{  
                Thread.sleep(1000);  
            }catch(InterruptedException e){  
                throw new RuntimeException(e);  
            }        
        }    
    });    
    
    thread.start();  
    while(true){  
        System.out.println("hello main");  
        try{  
            Thread.sleep(1000);  
        }catch(InterruptedException e){  
            throw new RuntimeException(e);  
        }  
    }
}


相关文章
|
4天前
|
NoSQL Redis
单线程传奇Redis,为何引入多线程?
Redis 4.0 引入多线程支持,主要用于后台对象删除、处理阻塞命令和网络 I/O 等操作,以提高并发性和性能。尽管如此,Redis 仍保留单线程执行模型处理客户端请求,确保高效性和简单性。多线程仅用于优化后台任务,如异步删除过期对象和分担读写操作,从而提升整体性能。
15 1
|
1天前
|
缓存 安全 Java
【JavaEE】——单例模式引起的多线程安全问题:“饿汉/懒汉”模式,及解决思路和方法(面试高频)
单例模式下,“饿汉模式”,“懒汉模式”,单例模式下引起的线程安全问题,解锁思路和解决方法
|
1天前
|
Java 程序员 调度
【JavaEE】线程创建和终止,Thread类方法,变量捕获(7000字长文)
创建线程的五种方式,Thread常见方法(守护进程.setDaemon() ,isAlive),start和run方法的区别,如何提前终止一个线程,标志位,isinterrupted,变量捕获
|
2月前
|
Java 开发者
在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口
【10月更文挑战第20天】在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口。本文揭示了这两种方式的微妙差异和潜在陷阱,帮助你更好地理解和选择适合项目需求的线程创建方式。
32 3
|
2月前
|
Java 开发者
在Java多线程编程中,选择合适的线程创建方法至关重要
【10月更文挑战第20天】在Java多线程编程中,选择合适的线程创建方法至关重要。本文通过案例分析,探讨了继承Thread类和实现Runnable接口两种方法的优缺点及适用场景,帮助开发者做出明智的选择。
25 2
|
2月前
|
Java
Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口
【10月更文挑战第20天】《JAVA多线程深度解析:线程的创建之路》介绍了Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口。文章详细讲解了每种方式的实现方法、优缺点及适用场景,帮助读者更好地理解和掌握多线程编程技术,为复杂任务的高效处理奠定基础。
41 2
|
2月前
|
安全 Java
Java多线程通信新解:本文通过生产者-消费者模型案例,深入解析wait()、notify()、notifyAll()方法的实用技巧
【10月更文挑战第20天】Java多线程通信新解:本文通过生产者-消费者模型案例,深入解析wait()、notify()、notifyAll()方法的实用技巧,包括避免在循环外调用wait()、优先使用notifyAll()、确保线程安全及处理InterruptedException等,帮助读者更好地掌握这些方法的应用。
25 1
|
2月前
|
Java 开发者
Java多线程初学者指南:介绍通过继承Thread类与实现Runnable接口两种方式创建线程的方法及其优缺点
【10月更文挑战第20天】Java多线程初学者指南:介绍通过继承Thread类与实现Runnable接口两种方式创建线程的方法及其优缺点,重点解析为何实现Runnable接口更具灵活性、资源共享及易于管理的优势。
47 1
|
2月前
|
Java
在Java多线程编程中,`wait()`和`notify()`方法的相遇如同一场奇妙的邂逅
在Java多线程编程中,`wait()`和`notify()`方法的相遇如同一场奇妙的邂逅。它们用于线程间通信,使线程能够协作完成任务。通过这些方法,生产者和消费者线程可以高效地管理共享资源,确保程序的有序运行。正确使用这些方法需要遵循同步规则,避免虚假唤醒等问题。示例代码展示了如何在生产者-消费者模型中使用`wait()`和`notify()`。
34 1
|
2月前
|
安全 Java 开发者
Java多线程中的`wait()`、`notify()`和`notifyAll()`方法,探讨了它们在实现线程间通信和同步中的关键作用
本文深入解析了Java多线程中的`wait()`、`notify()`和`notifyAll()`方法,探讨了它们在实现线程间通信和同步中的关键作用。通过示例代码展示了如何正确使用这些方法,并分享了最佳实践,帮助开发者避免常见陷阱,提高多线程程序的稳定性和效率。
54 1