【Java】多线程入门,看这篇就够了


有不清楚概念的可以点击阅读相关的知识
👇
操作系统,进程,线程的初步总结
☝️

1️⃣观察多线程现象

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

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

示例

public class TreadTest {
    static class SomeThead extends Thread{
        @Override
        public void run() {
            while(true){
                System.out.println("我是另一个线程(执行流B)");
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static void main(String[] args) {
        SomeThead thead = new SomeThead();
        thead.start();

        while(true){
            System.out.println("我是主线程(执行流A)");
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

运行结果:

如果按照之前的逻辑,一段代码的死循环在执行中,是不能执行其他代码的。

而现在有多个执行流,A、B两个执行流之间各自执行自己的,相互不影响。

使用Java自带的 jconsole 命令观察线程

默认路径:C:\Program Files\Java\jdk1.8.0_131\bin

2️⃣多线程的优势-增加运行速度

分别实现四个线程分别对各自的每一段数据进行排序,和一个线程线程中分别对四段进行排序。
省略了最后一步将四个部分的数据进行归并的步骤。

生成数组

public class ArrayHelper {
    public static long[] generateArray(int n){
        Random random = new Random(20220420);//随机种子
        long[] array = new long[n];
        for(int i = 0; i < n ; i++){
            array[i] = random.nextInt();
        }
        return array;
    }
}

实现并发的排序

public class ConcurrentSort {
    static class SortWorker extends Thread{
        private final long[] array;
        private final int fromIndex;
        private final int toIndex;

        // 利用构造方法,将待排序的数组区间情况,传入
        // 对 array 的 [fromIndex, toIndex) 进行排序
        public SortWorker(long[] array,int fromIndex,int toIndex){
            this.array = array;
            this.fromIndex = fromIndex;
            this.toIndex = toIndex;
        }
        @Override
        public void run() {
            Arrays.sort(array,fromIndex,toIndex);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        long[] array = ArrayHelper.generateArray(4_000_0000);

        long start = System.currentTimeMillis();

        SortWorker s1 = new SortWorker(array,0,1_000_0000);
        s1.start();// 让 s1 进入就绪队列
        SortWorker s2 = new SortWorker(array,1_000_0001,2_000_0000);
        s2.start();// 让 s2 进入就绪队列
        SortWorker s3 = new SortWorker(array,2_000_0000,3_000_0000);
        s3.start();// 让 s3 进入就绪队列
        SortWorker s4 = new SortWorker(array,3_000_0000,4_000_0000);
        s4.start();// 让 s4 进入就绪队列

        // 等待 4 个线程全部排序完毕,这 4 个 join 没有前后顺序
        s1.join();
        s2.join();
        s3.join();
        s4.join();

        // 4 个线程一定全部结束了
        // TODO:进行 4 路归并,将 4 个有序数组,归并成一个有序数组
        long end = System.currentTimeMillis();

        System.out.println("并发排序用时:" + (end - start) + "ms");
    }
}

并发排序用时:1401ms

单线程排序

public class SingleSort {
    public static void main(String[] args) {
        long[] array = ArrayHelper.generateArray(4_000_0000);

        long start = System.currentTimeMillis();

        Arrays.sort(array,0,1_000_0000);
        Arrays.sort(array,1_000_0001,2_000_0000);
        Arrays.sort(array,2_000_0001,3_000_0000);
        Arrays.sort(array,3_000_0001,4_000_0000);
        // TODO:进行 4 路归并,将 4 个有序数组,归并成一个有序数组
        long end = System.currentTimeMillis();

        System.out.println("非并发排序用时:" + (end - start) + "ms");
    }
}

非并发排序用时:3733ms

1.多核环境下,并发排序用时 < 串行排序的耗时
单线程一定可以能跑在一个CPU(核)上,多线程可能工作在多个核上(核亲和性)

2.单核环境下,并发的排序也可能较少!

本身,计算机下有很多线程在等待分配CPU,假如现在有100个线程。
在公平的情况下,排序主线程,只会被分配 1/100 的时间。

当并发时,使用四个线程进行排序,算上其他的99个线程,计算机中共有 99 + 4 = 103个线程
四个线程和单个线程,分给我们的时间占比 4 / 103 > 1 / 100。

OS进行资源调度的单位是线程,衡量耗时是以进程为单位的。
所以,即使单核情况下,一个进程中的线程越多,被分到的时间片越多。

3.那线程越多越好吗?

并不是,创建线程本身是有消耗的,线程调度也需要耗时。
CPU是公共资源,要合理分配使用。

4.并发排序的耗时一定小于串行吗?

不一定,在单核的情况下考虑:

串行的排序:t = t(排区间1) + t(排区间2) + t(排区间3) + t(排区间4)

并发的排序:t = 4*t(创建线程) + t(排区间1) + t(排区间2) + t(排区间3) + t(排区间4) + t(销毁)

并发额外需要创建线程和销毁线程的时间,如果排序用时越短,那么并发的效率越低。

为什么要多线程?

提升整个进程的执行速度,尤其是计算密集性的程序。

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

当一个执行流因故堵塞时,我们可以启用一个新的执行流进行处理(比如因网络堵塞)。

3️⃣创建线程

方法一:继承 Thread 类

1.继承 Thread 来创建一个线程类.

class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("这里是线程运行的代码");
    }
}

2.创建 MyThread 类的实例

MyThread t = new MyThread();

3.调用 start 方法启动线程

t.start(); // 线程开始运行

方法二:实现 Runnable 接口

1.实现 Runnable 接口

class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("这里是线程运行的代码");
    }
}

2.创建 Thread 类实例, 调用 Thread 的构造方法时将 Runnable 对象作为 target 参数.

Thread t = new Thread(new MyRunnable());

3.调用 start 方法

t.start(); // 线程开始运行

对比上面两种方法:

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

注意:

当有了 Thread 对象后,调用其 start() 方法。

  • 已经调用过 start() 后就不能再调用 start() ,否则会发生异常
  • 千万不要调用成 run() ,否则就跟调用普通方法运行一样,在主线程下运行的代码,和多线程就没有关系了。

理解start():

调用 start() 方法把线程的状态从新建态变成了就绪态。子线程进入线程调度器的就绪队列中,等待被调度器选中分配CPU,从子线程进入就绪队列后就和主线程在被调度时保持同等地位。

先执行子线程还是主线程呢?

当主线程执行 start() ,说明主线程还在占用资源运行中。但是先执行子线程还是主线程都是有可能的,如果发生线程调度选中子线程就执行子线程,但是调度顺序不可预知。不过大概率还是继续执行主线程,因为刚执行完 start() 就发生线程调度的可能性比较小。
只能保证在调用 start() 前的代码先执行。

什么情况下会发生线程调度(重新选择线程分配CPU)?

  1. CPU空闲
    1.当前运行的线程执行结束。运行 -> 结束
    2.当前运行的线程等待外部条件 。运行 -> 阻塞
    3.当前运行的线程主动放弃CPU。运行 -> 就绪
  2. 被调度器主动调度
    1.高优先级线程抢占
    2.时间片耗尽(最常见)

理解线程的随机性

在多线程的运行过程中,固定的代码会产生不一样的结果,主要原因就是因为在线程的运行过程中,调度的随机性。
只有一个线程就不会有问题。

其他变形

  • 匿名内部类创建 Thread 子类对象
// 使用匿名类创建 Thread 子类对象
Thread t1 = new Thread() {
    @Override
    public void run() {
        System.out.println("使用匿名类创建 Thread 子类对象");
    }
};
  • 匿名内部类创建 Runnable 子类对象
// 使用匿名类创建 Runnable 子类对象
Thread t2 = new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("使用匿名类创建 Runnable 子类对象");
    }
});
  • lambda 表达式创建 Runnable 子类对象
// 使用 lambda 表达式创建 Runnable 子类对象
Thread t3 = new Thread(() -> System.out.println("使用匿名类创建 Thread 子类对象"));
Thread t4 = new Thread(() -> {
    System.out.println("使用匿名类创建 Thread 子类对象");
});

4️⃣Thread 类及常见方法

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

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

2.Thread 的几个常见属性

属性获取方法
IDgetId()
名称getName()
状态getState()
优先级getPriority()
是否后台线程isDaemon()
是否存活isAlive()
是否被中断isInterrupted()
  1. ID 是线程的唯一标识,不同线程不会重复,只能 get 不能 set

  2. 名称是各种调试工具用到,为了方便给开发者看
    可以 get 和 set,也可以重复,可以通过setName()设置,也可以通过 Thead() 构造方法设置
    默认情况下,如果没有设置名称,线程名称遵守 Thread-num,num从0往后递增

  3. 状态表示线程当前所处的一个情况,只能 get 不能 set ,状态的变更由JVM控制
    在这里插入图片描述

  4. 线程可以 get 和 set 自己的优先级,优先级高的线程理论上来说更容易被调度到
    但是这个优先级设置,只是给JVM一些建议,不能强制让线程有限被调度

  5. 后台线程,又叫守护线程/精灵线程,可以 get 和 set
    后台线程一般是做一些支持工作的线程,前台线程一般是做一些有交互工作的。
    比如:写了一个月播放器
    1.线程响应用户点击工作(前台)
    2.线程取网络上下载歌曲(后台)
    注意:
    1.JVM会在一个进程的所有非后台线程结束后,才会结束运行,和主线程没有关系
    2.和后台线程没有关系,即使后台线程还在工作,也正常退出
    在这里插入图片描述

  6. 是否存活,即简单的理解,为 run 方法是否运行结束了

  7. 线程的中断问题,后面说明

3.启动一个线程-start()

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

覆写 run 方法是提供给线程要做的任务。

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

4.中断一个线程

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

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

  1. 通过共享的标记来进行沟通
  2. 调用 interrupt() 方法来通知

示例-1: 使用自定义的变量来作为标志位

需要给标志位上加 volatile 关键字(这个关键字的功能后面介绍).

public class ThreadDemo1 {
    private static class MyRunnable implements Runnable {
        public volatile boolean isQuit = false;
        @Override
        public void run() {
            while (!isQuit) {
                System.out.println(Thread.currentThread().getName()
                        + ": 别管我,我忙着转账呢!");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName()
                    + ": 啊!险些误了大事");
        }
    }
    public static void main(String[] args) throws InterruptedException {
        MyRunnable target = new MyRunnable();
        Thread thread = new Thread(target, "李四");
        System.out.println(Thread.currentThread().getName()
                + ": 让李四开始转账。");
        thread.start();
        Thread.sleep(5 * 1000);
        System.out.println(Thread.currentThread().getName()
                + ": 老板来电话了,得赶紧通知李四对方是个骗子!");
        target.isQuit = true;
    }
}

//运行结果:
main: 让李四开始转账。
李四: 别管我,我忙着转账呢!
李四: 别管我,我忙着转账呢!
李四: 别管我,我忙着转账呢!
李四: 别管我,我忙着转账呢!
李四: 别管我,我忙着转账呢!
main: 老板来电话了,得赶紧通知李四对方是个骗子!
李四: 啊!险些误了大事

这种中断方式如果线程处于休眠状态时,无法即使停止。

示例-2: 调用 interrupt() 方法来通知

使用 Thread.interrupted() 或者 Thread.currentThread().isInterrupted() 代替自定义标志位.

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

方法说明
public void interrupt()中断对象关联的线程,如果线程正在阻塞,则以异常方式通知,否则设置标志位
public static boolean interrupted()判断当前线程的中断标志位是否设置,调用后清除标志位
public boolean isInterrupted()判断对象关联的线程的标志位是否设置,调用后不清除标志位
public class ThreadDemo2 {
    private static class MyRunnable implements Runnable {
        @Override
        public void run() {
            // 两种方法均可以
            while (!Thread.interrupted()) {
            //while (!Thread.currentThread().isInterrupted()) {
                System.out.println(Thread.currentThread().getName()
                        + ": 别管我,我忙着转账呢!");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    System.out.println(Thread.currentThread().getName()
                            + ": 有内鬼,终止交易!");
                    // 注意此处的 break
                    break;
                }
            }
            System.out.println(Thread.currentThread().getName()
                    + ": 啊!险些误了大事");
        }
    }
    public static void main(String[] args) throws InterruptedException {
        MyRunnable target = new MyRunnable();
        Thread thread = new Thread(target, "李四");
        System.out.println(Thread.currentThread().getName()
                + ": 让李四开始转账。");
        thread.start();
        Thread.sleep(5 * 1000);
        System.out.println(Thread.currentThread().getName()
                + ": 老板来电话了,得赶紧通知李四对方是个骗子!");
        thread.interrupt();
    }
}

//运行结果:
李四: 别管我,我忙着转账呢!
李四: 别管我,我忙着转账呢!
李四: 别管我,我忙着转账呢!
李四: 别管我,我忙着转账呢!
main: 老板来电话了,得赶紧通知李四对方是个骗子!
李四: 有内鬼,终止交易!
李四: 啊!险些误了大事
java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at thread.thread_04_20.test_interrupt.ThreadDemo2$MyRunnable.run(ThreadDemo2.java:18)
	at java.lang.Thread.run(Thread.java:748)

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

  1. 如果线程因为调用 wait/join/sleep 等方法而阻塞挂起,则以 InterruptedException 异常的形式通
    知,清除中断标志
    当出现 InterruptedException 的时候, 要不要结束线程取决于 catch 中代码的写法. 可以选择忽略这个异常, 也可以跳出循环结束线程.
  2. 否则,只是内部的一个中断标志被设置,thread 可以通过
    Thread.interrupted() 判断当前线程的中断标志被设置,清除中断标志
    Thread.currentThread().isInterrupted() 判断指定线程的中断标志被设置,不清除中断标志

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

标志位是否清除, 就类似于一个开关

Thread.interrupted() 相当于按下开关, 开关自动弹起来了. 这个称为 “清除标志位”,返回的是 Thred 内部的中断标志位,默认值为 false,线程请求中断后,值变为了 true,然后又立即清楚标志位变为 false。
Thread.currentThread().isInterrupted() 相当于按下开关之后, 开关弹不起来, 这个称为"不清除标志位".也就是有线程发送中断请求后,该标志位及以后都为 true,没有清除变为 false。

5.等待一个线程-join()

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

有时,我们需要等待一个线程完成它的工作后,才能进行自己的下一步工作。例如,张三只有等李四转账成功,才决定是否存钱,这时我们需要一个方法明确等待线程的结束。

class Account extends Thread{
    @Override
    public void run() {
        try {
            TimeUnit.SECONDS.sleep(5);//等待五秒
            System.out.println("张三转账成功");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

public class Main throws InterruptedException{
    public static void main(String[] args) {
        Account t = new Account();
        t.start();
        //t.join();
        System.out.println("李四存钱");
    }
}

//没有join()的结果:
李四存钱
张三转账成功
//有join()的结果
张三转账成功
李四存钱

6.获取当前线程引用

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

7.休眠当前线程

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

或者使用TimeUnit.SECONDS.sleep() == Thread.sleep(1000)

从线程的状态的角度,调用 sleep() ,就是让当前线程从 “运行” -> “阻塞”
注意:因为线程的调度是不可控的,所以,这个方法只能保证实际休眠时间是大于等于参数设置的休眠时间的。

8.yield关键字

public static void main(String[] args) {
    Thread t1 = new Thread(new Runnable() {
        @Override
        public void run() {
            while (true) {
                System.out.println("张三");
                // 先注释掉, 再放开
                // Thread.yield();
            }
        }
    }, "t1");
    t1.start();
    Thread t2 = new Thread(new Runnable() {
        @Override
        public void run() {
            while (true) {
                System.out.println("李四");
            }
        }
    }, "t2");
    t2.start();
}

可以看到:

  1. 不使用 yield 的时候, 张三李四大概五五开
  2. 使用 yield 时, 张三的数量远远少于李四

结论:

  1. yield 不改变线程的状态, 但是会重新去排队.
  2. yield 主要用于执行一些耗时较旧的计算任务,为了防止计算机处于“卡顿”状态,时不时得让出一些CPU资源,给OS内的其他进程。

5️⃣线程的状态和状态转移

线程的状态是一个枚举类型 Thread.State

public class ThreadState {
    public static void main(String[] args) {
        for (Thread.State state : Thread.State.values()) {
            System.out.println(state);
        }
    }
}
  • NEW: 安排了工作, 还未开始行动
  • RUNNABLE: 可工作的. 又可以分成正在工作中和即将开始工作.
  • BLOCKED: 表示等待获取锁
  • WAITING: 线程在无限等待唤醒
  • TIMED_WAITING: 线程在等待唤醒,但设置了时限
  • TERMINATED: 工作完成了.

看不太懂状态转移图没事,重点是要理解状态的意义以及各个状态的具体意思。
举个栗子:

刚把李四、王五找来,还是给他们在安排任务,没让他们行动起来,就是 NEW 状态;

当李四、王五开始去窗口排队,等待服务,就进入到 RUNNABLE 状态。
该状态并不表示已经被银行工作人员开始接待,排在队伍中也是属于该状态,即可被服务的状态,是否开始服务,则看调度器的调度;

当李四、王五因为一些事情需要去忙,例如需要填写信息、回家取证件、发呆一会等等时,进入
BLOCKED 、WATING 、TIMED_WAITING 状态;

如果李四、王五已经忙完,为 TERMINATED 状态。

所以,之前我们讲过的 isAlive() 方法,可以认为是处于不是 NEW 和 TERMINATED 的状态都是活着
的。

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

bruin_du

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

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

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

打赏作者

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

抵扣说明:

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

余额充值