【JavaEE初阶】一文带你了解线程、多线程基础

简介: 【JavaEE初阶】一文带你了解线程、多线程基础

一、认识线程


1.1 线程是什么


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

我们设想如下场景:

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


1.2 为什么要有线程


首先, "并发编程" 成为 "刚需。

单核 CPU 的发展遇到了瓶颈. 要想提高算力, 就需要多核 CPU,而并发编程能更充分利用多核 CPU资源。

有些任务场景需要 "等待 IO", 为了让等待 IO 的时间能够去做一些其他的工作, 也需要用到并发编程。

其次, 虽然多进程也能实现并发编程, 但是线程比进程更轻量。


创建线程比创建进程更。

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

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

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

(Coroutine),关于线程池我后面会再介绍,此处关于协程的话题我就暂时不做过多讨论啦!


1.3 进程和线程的区别


进程是包含线程的,每个进程至少有一个线程存在,即主线程。

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

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

进程的最大区别。

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

11.png


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

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

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


二、第一个多线程程序


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


每个线程都是一个独立的执行流

多个线程之间是 "并发" 执行的

import java.util.Random;
public class ThreadDemo {
    private static class MyThread extends Thread {
        @Override
        public void run() {
            Random random = new Random();
            while (true) {
                // 打印线程名称
                System.out.println(Thread.currentThread().getName());
                try {
                    // 随机停止运行 0-9 秒
                  Thread.sleep(random.nextInt(10));
               } catch (InterruptedException e) {
                    e.printStackTrace();
               }
           }
       }
   }
 public static void main(String[] args) {
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        MyThread t3 = new MyThread();
        t1.start();
        t2.start();
        t3.start();
        Random random = new Random();
        while (true) {
            // 打印线程名称
            System.out.println(Thread.currentThread().getName());
            try {
                Thread.sleep(random.nextInt(10));
           } catch (InterruptedException e) {
                // 随机停止运行 0-9 秒
                e.printStackTrace();
           }
       }
   }
}

 使用 jconsole 命令观察线程


12.png


2.2 创建线程


2.2.1 方法1——继承Thread类


继承 Thread 来创建一个线程类 →创建 MyThread 类的实例调用 →start 方法启动线程


c

class MyThread extends Thread{
    @Override
    public void run() {
        System.out.println("这是线程运行线程的代码!!");
    }
}
public class TestDemo {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start();
    }
}

13.png


2.2.2 方法2——实现Runnable接口


实现 Runnable 接口→创建 Thread 类实例, 调用 Thread 的构造方法时将 Mythread2 对象作为 target 参数→调用 start 方法

class MyThread2 implements Runnable{
    @Override
    public void run() {
        System.out.println("这是线程执行的代码!!");
    }
}
public class Demo7 {
    public static void main(String[] args) {
        Thread t = new Thread(new MyThread2());
        t.start();
    }
}

14.png

对比上面两种方法:

  • 继承 Thread , 直接使用 this 就表示当前线程对象的引用
  • 实现 Runnable 接口, this 表示的是 MyThread2 的引用. 需要使用 Thread.currentThread()


2.2.3 方法3——匿名内部类创建Threa子类对象


public class Demo1 {
    public static void main(String[] args) {
        Thread t = new Thread(){//创建一个子类继承thread类,没有名字
            @Override
            public void run() {
                while (true){
                    System.out.println("hello thread!");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        };
        t.start();//调用系统API
        while (true){
            System.out.println("hell main!");
            try {
                Thread.sleep(1000);
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        }
    }
}


15.png



2.2.4 方法4——匿名内部类创建 Runnable 子类对象


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


2.2.5 方法5——lambda 表达式创建 Runnable 子类对象


public class Demo3 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(()->{
            while (true){
                System.out.println("hello thread!");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        t.start();
        while (true){
            System.out.println("hello main");
            Thread.sleep(1000);
        }
    }
}


三、Thread类及其常见方法


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

17.png


3.1 Thread类的常见构造方法


方法 说明

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(), "这是我的名字");


3.2 Thread的常见属性


属性 获取方法

ID

getId()

名称

getName()

状态

getState()

优先级

getPriority()

是否后台线程

isDaemon()

是否存活

isAlive()

是否被中断

isInterrupted()


ID 是线程的唯一标识,不同线程不会重复

名称是各种调试工具用到

状态表示线程当前所处的一个情况,下面我们会进一步说明

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

关于后台线程,需要记住一点:JVM会在一个进程的所有非后台线程结束后,才会结束运行。

是否存活,一般都是Thread对象先创建好,手动调用start,内核才真正创建出线程。消亡的时候,可能是Thread对象先结束生命周期,也可能是Threa对象还在,内核中的线程把run执行完了,就结束了。

线程的中断问题,下面我们进一步说明

public class ThreadDemo {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    System.out.println(Thread.currentThread().getName() + ": 我还
活着");
                    Thread.sleep(1 * 1000);
               } catch (InterruptedException e) {
                    e.printStackTrace();
               }
           }
            System.out.println(Thread.currentThread().getName() + ": 我即将死去");
       });
        System.out.println(Thread.currentThread().getName() 
                           + ": ID: " + thread.getId());
        System.out.println(Thread.currentThread().getName() 
                           + ": 名称: " + thread.getName());
        System.out.println(Thread.currentThread().getName() 
                           + ": 状态: " + thread.getState());
        System.out.println(Thread.currentThread().getName() 
                           + ": 优先级: " + thread.getPriority());
        System.out.println(Thread.currentThread().getName() 
                           + ": 后台线程: " + thread.isDaemon());
        System.out.println(Thread.currentThread().getName() 
                           + ": 活着: " + thread.isAlive());
        System.out.println(Thread.currentThread().getName() 
                           + ": 被中断: " + thread.isInterrupted());
        thread.start();
        while (thread.isAlive()) {}
        System.out.println(Thread.currentThread().getName() 
                           + ": 状态: " + thread.getState());
   }
}



3.3 启动一个线程


之前我们已经看到了如何通过覆写 run 方法创建一个线程对象,但线程对象被创建出来并不意味着线程就开始运行了。

覆写 run 方法是提供给线程要做的事情的指令清单

线程对象可以认为是把李四、王五叫过来了

而调用 start() 方法,就是喊一声:”行动起来!“,线程才真正独立去执行了。

18.png


调用 start 方法, 才真的在操作系统的底层创建出一个线程。



3.4 终止一个线程


李四一旦进到工作状态,他就会按照行动指南上的步骤去进行工作,不完成是不会结束的。但有时我们需要增加一些机制,例如老板突然来电话了,说转账的对方是个骗子,需要赶紧停止转账,那张三该如何通知李四停止呢?这就涉及到我们的停止线程的方式了。

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

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

调用 interrupt() 方法来通知


3.4.1 使用自定义的变量作为标志位


public class Demo6 {
    public static boolean isQuit = false;
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(()->{
            while (!isQuit){
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        t.start();
        //主线程执行其他逻辑,让t线程结束
        Thread.sleep(3000);
        isQuit = true;
        System.out.println("把t线程终止");
    }
}

19.png


3.4.2 使用Thread.currentThread().isInterrupted()


//线程终止,使用Thread自带的标志位
public class Demo8 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(()->{
            //Thread.currentThread()→获取当前线程的对象
            //isInterrupted()->Thread对象内部提供的标志位,true:线程要结束
            while (!Thread.currentThread().isInterrupted()){
                System.out.println("hello thread!!");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    //e.printStackTrace();
                    break;
                }
            }
        });
        t.start();
        Thread.sleep(3000);
        t.interrupt();//把上面的标志位设置成true,sleep被唤醒,清除标志位,会一直执行下去
//此时给我们留了更大的操作空间,此时选择break跳出即可
    }
}

16.png


thread 收到通知的方式有两种:


        如果线程因为调用 wait/join/sleep 等方法而阻塞挂起,则以 InterruptedException 异常的形式通 知, 清除中断标志, 当出现 InterruptedException 的时候 , 要不要结束线程取决于 catch 中代码的写法 . 可以选择忽略这个异常, 也可以跳出循环结束线程 .

       否则,只是内部的一个中断标志被设置,thread 可以通过 Thread.interrupted() 判断当前线程的中断标志被设置,清除中断标志 。Thread.currentThread().isInterrupted() 判断指定线程的中断标志被设置,不清除中断标志。这种方式通知收到的更及时,即使线程正在 sleep 也可以马上收到。



目录
相关文章
|
2月前
|
Java 开发者
在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口
【10月更文挑战第20天】在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口。本文揭示了这两种方式的微妙差异和潜在陷阱,帮助你更好地理解和选择适合项目需求的线程创建方式。
23 3
|
2月前
|
Java 开发者
在Java多线程编程中,选择合适的线程创建方法至关重要
【10月更文挑战第20天】在Java多线程编程中,选择合适的线程创建方法至关重要。本文通过案例分析,探讨了继承Thread类和实现Runnable接口两种方法的优缺点及适用场景,帮助开发者做出明智的选择。
20 2
|
2月前
|
Java
Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口
【10月更文挑战第20天】《JAVA多线程深度解析:线程的创建之路》介绍了Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口。文章详细讲解了每种方式的实现方法、优缺点及适用场景,帮助读者更好地理解和掌握多线程编程技术,为复杂任务的高效处理奠定基础。
34 2
|
2月前
|
Java 开发者
Java多线程初学者指南:介绍通过继承Thread类与实现Runnable接口两种方式创建线程的方法及其优缺点
【10月更文挑战第20天】Java多线程初学者指南:介绍通过继承Thread类与实现Runnable接口两种方式创建线程的方法及其优缺点,重点解析为何实现Runnable接口更具灵活性、资源共享及易于管理的优势。
38 1
|
2月前
|
安全 Java 开发者
Java多线程中的`wait()`、`notify()`和`notifyAll()`方法,探讨了它们在实现线程间通信和同步中的关键作用
本文深入解析了Java多线程中的`wait()`、`notify()`和`notifyAll()`方法,探讨了它们在实现线程间通信和同步中的关键作用。通过示例代码展示了如何正确使用这些方法,并分享了最佳实践,帮助开发者避免常见陷阱,提高多线程程序的稳定性和效率。
41 1
|
2月前
|
Java
在Java多线程编程中,`wait()` 和 `notify()/notifyAll()` 方法是线程间通信的核心机制。
在Java多线程编程中,`wait()` 和 `notify()/notifyAll()` 方法是线程间通信的核心机制。它们通过基于锁的方式,使线程在条件不满足时进入休眠状态,并在条件成立时被唤醒,从而有效解决数据一致性和同步问题。本文通过对比其他通信机制,展示了 `wait()` 和 `notify()` 的优势,并通过生产者-消费者模型的示例代码,详细说明了其使用方法和重要性。
28 1
|
21天前
|
数据采集 Java Python
爬取小说资源的Python实践:从单线程到多线程的效率飞跃
本文介绍了一种使用Python从笔趣阁网站爬取小说内容的方法,并通过引入多线程技术大幅提高了下载效率。文章首先概述了环境准备,包括所需安装的库,然后详细描述了爬虫程序的设计与实现过程,包括发送HTTP请求、解析HTML文档、提取章节链接及多线程下载等步骤。最后,强调了性能优化的重要性,并提醒读者遵守相关法律法规。
55 0
|
2月前
|
存储 前端开发 C++
C++ 多线程之带返回值的线程处理函数
这篇文章介绍了在C++中使用`async`函数、`packaged_task`和`promise`三种方法来创建带返回值的线程处理函数。
62 6
|
2月前
|
存储 运维 NoSQL
Redis为什么最开始被设计成单线程而不是多线程
总之,Redis采用单线程设计是基于对系统特性的深刻洞察和权衡的结果。这种设计不仅保持了Redis的高性能,还确保了其代码的简洁性、可维护性以及部署的便捷性,使之成为众多应用场景下的首选数据存储解决方案。
42 1
|
2月前
|
C++
C++ 多线程之线程管理函数
这篇文章介绍了C++中多线程编程的几个关键函数,包括获取线程ID的`get_id()`,延时函数`sleep_for()`,线程让步函数`yield()`,以及阻塞线程直到指定时间的`sleep_until()`。
26 0
C++ 多线程之线程管理函数