Java 多线程基础

文章目录

  • 1. 认识线程
    • 1.1 概念
      • 1.1.1 线程是什么
      • 1.1.2 为什么要有线程
      • 1.1.3 进程和线程的区别
      • 1.1.4 Java的线程和操作系统线程的关系
    • 1.2 第一个多线程程序
    • 1.3 创建线程
    • 1.4 多线程的优势
  • 2. Thread 类及其常用的方法
    • 2.1 Thread 的常见构造方法
    • 2.2 Thread 的几个常见属性
    • 2.3 启动一个线程 - start()
    • 2.4 中断一个线程
    • 2.5 等待一个线程 - join()
    • 2.6 获取当前线程引用
    • 2.7 休眠当前线程

1. 认识线程

1.1 概念

1.1.1 线程是什么

线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
一个线程就是一个"执行流", 每个线程可以按照顺序执行自己的代码, 多个线程之间"同时"执行着多份代码

1.1.2 为什么要有线程

  • 单核CPU的发展到了瓶颈, 要再想提高计算机算力的话, 就需要多核CPU, 而并发编程便能充分利用多核CPU资源
  • 有些任务场景需要"等待IO"为了让"等待IO"的时间能去做其他的事情, 也需要用到并发编程

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

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

1.1.3 进程和线程的区别

  • 进程是包含线程的, 每个进程必须包含一个线程, 就是主线程
  • 进程和进程之间是不共享内存空间的, 同一个进程的线程之间共享内存空间
  • 进程是系统分配资源的最小单位, 线程是系统调度的最小单位

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

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

1.2 第一个多线程程序

下面我们写一个简单的多线程程序, 先不关心它是怎么写的, 就先简单的了解一下多线程,
感受多线程程序和单线程程序的区别

  • 每个线程是一个独立的执行流
  • 多个线程之间是"并发"执行的
package Thread;

import java.util.Random;

public class ThreadDemo1 {
    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) {
                    throw new RuntimeException(e);
                }
            }
        }
    }


    public static void main(String[] args) {

        MyThread thread1 = new MyThread();
        MyThread thread2 = new MyThread();
        MyThread thread3 = new MyThread();
        // 启动线程
        thread1.start();
        thread2.start();
        thread3.start();

        Random random = new Random();
        while (true) {
            System.out.println(Thread.currentThread().getName());
            try {
                // 随机停止运行 0 ~ 9 秒
                Thread.sleep(random.nextInt(10));
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

Java 多线程基础_第1张图片
下面我们用jconsole观察线程
首先我们打开jconsole
Java 多线程基础_第2张图片

Java 多线程基础_第3张图片

Java 多线程基础_第4张图片

1.3 创建线程

  1. 继承Thread 类来创建一个线程
public class ThreadDemo2 {
    // 1.继承一个Thread 类来创建一个线程类
    class MyThread extends Thread {
        @Override
        // 重写里面的run方法
        public void run() {
            System.out.println("这里是线程运行的代码");
        }
    }

    public static void main(String[] args) {
        // 2.创建一个 MyThread 实例
        MyThread thread1 = new MyThread();
        // 3. 调用 start 方法启动线程
        thread1.start();
    }
}
  1. 实现 Runnable 接口
public class ThreadDemo3 {
       // 1. 实现Runnable接口
       static class MyRunnable implements Runnable {
        @Override
        public void run() {
            System.out.println("这里是线程执行的代码");
        }
    }
    public static void main(String[] args) {
        // 2. 创建Thread类实例, 调用Thread类的构造方法时将Runnable对象作为target 参数
        Thread thread1 = new Thread(new MyRunnable());
        // 启动线程
        thread1.start();
    }
}

这里我们要注意的是:
实现Runnable接口,并不能直接启动或者说实现一个线程,Runnable接口和线程是两个不同的概念
换句话说,一个类,实现Runnable接口,这个类可以做很多事情,不仅仅只被用于线程,也可以用于其他功能!

  • 继承 Thread 类直接使用 this 就表示当前线程对象的引用
  • 而实现 Runnable 接口, this 表示的是 MyRunnable 的引用, 需要使用 Thread.currentThread() 表示当前线程对象的引用
  1. 使用匿名内部类创建 Thread 子类对象
public class ThreadDemo5 {
    public static void main(String[] args) {
        Thread thread1 = new Thread() {
            @Override
            public void run() {
                System.out.println("使用匿名内部类创建 Thread 子类对象");
            }
        };
        //启动线程
        thread1.start();
    }
}
  1. 匿名内部类创建 Runnable 子类对象
public class ThreadDemo6 {
    public static void main(String[] args) {
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("使用匿名内部类创建 Runnable 子类对象");
            }
        });
        // 启动线程
        thread1.start();
    }
}
  1. lambda 表达式创建 Runnable 子类对象
public class ThreadDemo7 {
    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            System.out.println("使用匿名内部类创建 Thread 对象");
        });
    // 启动线程
    thread1.start();
    }
}

注:
Thread 类中run()和start()方法的区别

作用功能不同:
run方法的作用是描述线程具体要执行的任务;
start方法的作用是真正的去申请系统线程
运行结果不同:
run方法是一个类中的普通方法,主动调用和调用普通方法一样,会顺序执行一次;
start调用方法后, start方法内部会调用Java 本地方法(封装了对系统底层的调用)真正的启动线程,并执行run方法中的代码,run 方法执行完成后线程进入销毁阶段。

1.4 多线程的优势

多线程的主要优势之一就是增加运行速度

下面我们举个例子具体看
我们使用并发和串行方式计算 a 和 b 的值, 分别让 a 和 b 自加20_0000_0000次,

/**
 * @describe
 * @author chenhongfei
 * @version 1.0
 * @date 2023/9/23
 */
package Thread;

import java.util.Random;
import java.util.concurrent.CountDownLatch;

public class ThreadDemo8 {
    private static final long count = 20_0000_0000;
    public static void main(String[] args) throws InterruptedException {
        // 使用并行方式
        concurrency();
        // 使用串行方式
        serial();
    }

    // 并行
    private static void concurrency() throws InterruptedException {
        long begin = System.nanoTime();
        // 利用一个线程计算 a 的值
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                int a = 0;
                for (int i = 0; i < count; i++) {
                    a++;
                }
            }
        });
        //启动线程
         thread1.start();
         // 主线程内计算b的值
        int b = 0;
        for (int i = 0; i < count; i++) {
            b++;
        }
        // 等待Thread线程结束
        thread1.join();
        long end = System.nanoTime();
        double ms =  (end - begin) * 1.0 / 1000 /1000; //1 纳秒=1e-6 毫秒
        System.out.printf("并发: %f 毫秒%n",ms);

    }

    // 串行
    private static void serial() {
        // 全在主线程内计算a, b 的值
        long begin = System.nanoTime();
        int a = 0;
        int b = 0;
        for (int i = 0; i < count; i++) {
            a++;
        }
        for (int i = 0; i < count; i++) {
            b++;
        }
        long end = System.nanoTime();
        double ms =  (end - begin) * 1.0 / 1000 /1000; //1 纳秒=1e-6 毫秒
        System.out.printf("串行: %f 毫秒%n",ms);
    }
}

Java 多线程基础_第5张图片

2. Thread 类及其常用的方法

Thread类是Java中用于创建线程的类。线程是程序执行的最小单元,它允许多个任务并发执行,提高了程序的效率。Thread类的实例表示一个独立的执行线程,可以通过继承Thread类或实现Runnable接口来创建线程。Thread类提供了一系列方法来管理线程的状态和行为,比如启动线程、暂停线程、恢复线程、等待线程完成等。在Java中,线程的使用非常普遍,比如在网络编程、多线程服务器、GUI应用程序等领域都会用到线程。

2.1 Thread 的常见构造方法

方法 说明
Thread() 创建线程对象
Thread(Runnable target) 使用 Runnable 对象创建线程对象
Thread(String name) 创建线程对象,并命名
Thread(Runnable target, String name) 使用 Runnable 对象创建线程对象,并命名
Thread t1 = new Thread();
Thread t2 = new Thread(new MyRunnable());
Thread t3 = new Thread("这是我的名字");
Thread t4 = new Thread(new MyRunnable(), "这是我的名字");

2.2 Thread 的几个常见属性

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

具体情况我们参考下面代码

/**
 * @describe
 * @author chenhongfei
 * @version 1.0
 * @date 2023/9/23
 */
package Thread;

public class ThreadDemo9 {
    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                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() + ": ID" + thread1.getId());
        System.out.println(Thread.currentThread().getName() + ": 名称" + thread1.getName());
        System.out.println(Thread.currentThread().getName() + ": 状态" + thread1.getState());
        System.out.println(Thread.currentThread().getName() + ": 优先级" + thread1.getPriority());
        System.out.println(Thread.currentThread().getName() + ": 后台线程" + thread1.isDaemon());
        System.out.println(Thread.currentThread().getName() + ": 活着" + thread1.isAlive());
        System.out.println(Thread.currentThread().getName() + ": 被中断" + thread1.isInterrupted());

        //启动线程
        thread1.start();

        while (thread1.isAlive()) {
            System.out.println(Thread.currentThread().getName() + ": 状态" + thread1.getState());
        }
    }
}

2.3 启动一个线程 - start()

在Java中,start()方法通常用于启动线程。它是定义在Thread类中的方法,用于启动线程。
当调用start()方法时,线程会从它的run()方法开始执行。run()方法是线程的执行体,它包含了线程要执行的代码。
当线程启动后,它将从run()方法开始执行,直到该方法结束或线程被中止。在执行期间,线程可以访问共享变量和对象,并且可以与其他线程并发执行。

  • 调用 start() 方法, 才真的在操作系统的底层创建出一个线程.
  • 重写run() 方法是给线程提供具体的指令清单
Thread thread = new Thread(new Runnable() {
    @Override
    public void run() {
        // 线程执行的代码(重写run 方法)
        System.out.println("Thread started.");
    }
});
thread.start(); // 启动线程

在上面的示例中,我们创建了一个新的线程对象,将一个实现了Runnable接口的对象传递给了构造函数。在run()方法中,我们打印了一条消息表示线程已经启动。然后,我们调用start()方法来启动线程。
需要注意的是,start()方法并不会阻塞调用它的线程。启动线程后,程序会继续执行其他代码。如果需要等待线程执行结束后再继续执行当前线程,可以使用join()方法。

2.4 中断一个线程

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

  • 1.通过共享的标记来进行沟通

通过设置一个标志位,线程在执行时可以检查这个标志位,如果发现标志位被设置了,那么线程就认为自己被中断了。

  • 2.调用interrupt() 方法来通知

下面是一个简单的使用标志位中断线程的实例:

public class ThreadDemo10 {
    private static volatile boolean isInterrupted = false;  //使用volatile 修饰, 保证线程间的可见性
    static Thread thread1 = new Thread(() -> {
        while (!isInterrupted) {
            System.out.println(Thread.currentThread().getName() + "开始转账");
            try {
                Thread.sleep(1000);  // 模拟任务耗时
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        System.out.println(Thread.currentThread().getName() + "取消转账, 差点误了大事");
    });

    public static void main(String[] args) {
        thread1.start();    // 线程启动, 开始转账
        isInterrupted = true;   // 取消转账
    }
}

我们也可以使用 Thread.interrupted() 或者 Thread.currentThread().isInterrupted() 代替自定义标志位.

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

下面先介绍几个方法

方法 说明
public void interrupt() 中断对象关联的线程,如果线程正在阻塞,则以异常方式通知,否则设置标志位
public static booleaninterrupted() 判断当前线程的中断标志位是否设置,调用后清除标志位
public booleanisInterrupted() 判断对象关联的线程的标志位是否设置,调用后不清除标志位
public class ThreadDemo11 {
    static Thread thread1 = new Thread(() -> {
        while (!Thread.currentThread().isInterrupted()) {    // Thread.currentThread().isInterrupted() 这就是标志位
            System.out.println(Thread.currentThread().getName() + "开始转账");
            try {
                Thread.sleep(1000);  // 模拟任务耗时
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        System.out.println(Thread.currentThread().getName() + "取消转账, 差点误了大事");
    });

    public static void main(String[] args) {
        thread1.start();    // 线程启动, 开始转账
        thread1.interrupt();   //设置标志位   取消转账
    }
}

线程收到通知的方法有两种:

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

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

  1. 否则, 只是内部的一个中断标志被设置,thread 可以通过:
    Thread.interrupted() 判断当前线程的中断标志被设置,清除中断标志
    Thread.currentThread().isInterrupted() 判断指定线程的中断标志被设置,不清除中断标志

标志位就相当于一个开关
Thread.isInterrupted() 相当于按下开关, 开关自动弹起来了, 这个称为"清楚标志位"
Thread.currentThread().isInterrupted() 相当于按下开关, 开关不弹起来, 这个称为"不清除标志位"

  • 使用Thread.isInterrupted() 线程中断, 会清除标志位
public class ThreadDemo12 {
    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                System.out.println(Thread.interrupted());
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        thread1.start();
        thread1.interrupt();   // 通知中断线程, 设置标志位
    }
}

只有第一个是 true , 后面都是 false 因为"开关弹回去了"
Java 多线程基础_第6张图片

  • 使用Thread.currentThread().Interrupted() 线程中断, 标志位不会清除

public class ThreadDemo13 {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                System.out.println(Thread.currentThread().isInterrupted());
            }
        });
        thread.start();
        thread.interrupt();
    }
}

全部都是 true , 因为标志位不会清除
Java 多线程基础_第7张图片

2.5 等待一个线程 - join()

有时候, 我们经常需要等待一个线程完成它的工作后, 才能进行下一步工作, 比如说, 这个月发工资我要买个手机, 只有等到了工资到账, 才能去买手机, 这时候我们就需要明确等待线程的结束.

每个Thread实例都有一个join()方法,该方法使得当前正在执行的线程暂停执行(阻塞),直到被join的线程执行结束(即从run()方法返回)。

public class ThreadDemo14 {
    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                System.out.println("还有" + (5-i) +"天发工资");
            }
            System.out.println("工资已到账");
        });

        Thread thread2 = new Thread(() -> {
            System.out.println("正在去买手机的路上");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("买到了手机");
        });

        thread1.start();
        thread1.join();   //直到 thread1 执行结束才执行后面内容
        thread2.start();
        thread2.join();
        System.out.println("此任务结束");
    }
}

Java 多线程基础_第8张图片
在这个例子中,主线程(Main Thread)会等待thread1线程执行结束,然后再执行自己的后续操作(让thread2执行)。join()方法的使用使得主线程能够同步地执行与子线程的结束。

如果把两个join() 注释掉, 效果如下:
Java 多线程基础_第9张图片
此外还可以使用wait(), notify() 和 notifyAll(), 这个后面我们细说.

2.6 获取当前线程引用

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

currentThread() 现在就是指向当前执行线程的一个引用。你可以用这个引用来获取当前线程的各种信息,例如它的名称、它的优先级、它是否是守护线程等等。

System.out.println("Current thread name: " + currentThread.getName());
System.out.println("Current thread priority: " + currentThread.getPriority());
System.out.println("Is current thread a daemon?: " + currentThread.isDaemon());

2.7 休眠当前线程

我们可以使用Thread.sleep()方法来使当前线程休眠(暂时停止执行)一段时间。这个方法接受一个以毫秒为单位的时间参数,指定线程应该休眠的时间。

  • 因为线程的调度是不可控的,所以,这个方法只能保证实际休眠时间是大于等于参数设置的休眠时间的。
public class ThreadDemo15 {
    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            System.out.println(System.currentTimeMillis());
            try {
                Thread.sleep(3 * 1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println(System.currentTimeMillis());
        });
        thread1.start();
    }
}

你可能感兴趣的:(JavaEE,java,开发语言)