多线程基础

多线程

线程的创建方式

  1. 继承Thread类,覆写run方法(线程的核心工作任务方法 )
  2. 覆写Runnable接口,覆写run方法

这两种方法的最终启动线程都是通过Thread类的start进行的

  1. 覆写Callable接口,覆写call方法

  2. 使用线程池创建

方式一:继承Thread类

  • 创建一个子类继承Thread类
  • 覆写run方法
  • 产生当前这个子类对象,调用start方法启动

注意:继承Thread类的子类就是一个线程实体

/**
 * 继承Thread类创建线程
 */
public class ThreadMethod extends Thread {
    @Override
    public void run(){
        System.out.println("这是子线程的输出结果");
        System.out.println("这是子线程的输出结果");
        System.out.println("这是子线程的输出结果");
    }
}
public class Main {
    public static void main(String[] args) {
        // 1. 创建线程类对象
        ThreadMethod mt = new ThreadMethod();
        // 2. 启动线程
        mt.start();
        System.out.println("这是Thread方式实现的主线程的输出语句");
    }
}

方法二:覆写Runnable接口

  • 创建一个子类实现Runnable接口
  • 覆写run方法
  • 产生当前这个子类的对象(即创建了一个任务对象)
  • 创建线程Thread类对象
  • 将子类对象传入线程对象(即向线程中添加一个任务)
  • 调用start方法启动

注意:与继承Thread类不同的是,实现Runnable接口的子类并不是直接的线程对象,只是一个线程的核心工作任务。(说清线程的任务和线程实体的关系)

public class RunnableMethod implements Runnable{
    @Override
    public void run() {
        System.out.println("这是Runnable方式实现的子线程任务");
    }
}
public class Main {
    public static void main(String[] args) {
        // 1. 创建线程的任务对象
        RunnableMethod runnableMethod = new RunnableMethod();
        // 2. 创建线程对象,将任务对象传入线程对象
        // 即创建 Thread 类实例, 调用 Thread 的构造方法时将 Runnable 对象作为 target 参数.
        Thread thread = new Thread(runnableMethod);
        // 3. 启动线程
        thread.start();
        System.out.println("这是Runnable方式实现的主线程的输出语句");
    }
}

推荐用方式二,实现Runnable接口更灵活,子类还能实现别的接口,继承别的类。方式一只能继承Thread类(单继承局限)

注意:调用start方法启动线程,是由JVM产生操作系统的线程并启动,到底什么时候真正启动,对于我们来说不可见,也无法控制。即线程(主线程,子线程)是并行,没有先后顺序,只是代码看起来向串行

对比上面两种方法:

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

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

关于方式一二的不同写法

  1. 匿名内部类继承Thread类,然后实现run方法

    // 匿名内部类继承Thread方法
    Thread t1 = new Thread(){
        @Override
        public void run() {
            System.out.println("匿名内部类继承Thread类");
            // 打印线程名字
            System.out.println(Thread.currentThread().getName());
        }
    };
    t1.start();
    System.out.println("这是主线程"+Thread.currentThread().getName());
    
  2. 匿名内部类实现Runnable接口

    Thread t2 = new Thread(new Runnable() {
        @Override
        public void run() {
            System.out.println("匿名内部类实现Runnable接口");
            // 打印线程名字
            System.out.println(Thread.currentThread().getName());
        }
    });
    t2.start();
    System.out.println("这是主线程"+Thread.currentThread().getName());
    
  3. lamada表达式实现Runnable接口

    lamada表达式建立在函数式接口上,函数式接口只有一个抽象方法

    Thread t3 = new Thread(()-> System.out.println("lamada表达式实现Runnable接口"));
    t3.start();
    System.out.println("这是主线程"+Thread.currentThread().getName());
    

多线程的优势

优势:增加运行速度

可以观察多线程在一些场合下是可以提高程序的整体运行效率的。通过例子,感受一下多线程和顺序执行的速度差异

  • 使用 System.nanoTime() 可以记录当前系统的 纳秒 级时间戳.
  • serial 串行的完成一系列运算. concurrency 使用两个线程并行的完成同样的运算.

image-20220702114756734

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MBXkpTtc-1661621920022)(img/image-20220702114402592.png)]

public class ThreadSpeedCompare {
    private static final long COUNt = 10_0000_0000; // 下划线表分隔

    public static void main(String[] args) throws InterruptedException {
        serial();
        concurrent();
    }

    // 串行进行20亿的累加
    private static void serial() {
        long start = System.nanoTime();
        long a = 0;
        for (long i = 0; i < COUNt; i++) {
            a ++;
        }
        // b的执行需要等待a走完才能进行
        long b = 0;
        for (long i = 0; i < COUNt; i++) {
            b ++;
        }
        long end = System.nanoTime();
        double allTime = (end - start) * 1.0 / 1000 / 1000;
        System.out.println("顺序执行共耗时 : " + allTime + "ms");
    }


    // 并发进行 + 10亿操作
    private static void concurrent() throws InterruptedException {
        long start = System.nanoTime();
        // 子线程进行 + 10亿操作
        Thread thread = new Thread(() -> {
            long a = 0;
            for (long i = 0; i < COUNt; i++) {
                a ++;
            }
        });
        thread.start();
        // 主线程中也执行 + 10亿操作 (主线程所在地就是子线程对象被创建的地方)
        long b = 0;
        for (long i = 0; i < COUNt; i++) {
            b ++;
        }
        // 等待子线程执行结束,即主线程和子线程的加法操作都完成
        // join方法等待子线程thread执行结束才能执行下面代码
        thread.join();
        long end = System.nanoTime();
        double allTime = (end - start) * 1.0 / 1000 / 1000;
        System.out.println("并发执行共耗时 : " + allTime + "ms");
    }
}

总结:多线程最大的应用场景就是把一个大任务拆分成多个子任务(交给子线程),多个子线程并发执行,提高系统处理效率

image-20220702114829576

Thread类及常见方法

Thread 类是 JVM 用来管理线程的一个类,换句话说,每个线程都有一个唯一的 Thread 对象与之关联。

构造方法

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kVMnHoir-1661621920024)(img/image-20220702152437296.png)]

// 一般搭配子类使用,需要有一个继承了Thread类的子类
Thread t1 = new Thread();
Thread t2 = new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("传入Runnable对象");
    }
});
Thread t3 = new Thread("鹏哥线程");
// 最常用:创建线程对象的同时命名
Thread t4 = new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("hehe");
    }
},"铭哥线程");

Thread的核心属性

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-g6YyTWM3-1661621920024)(img/image-20220702155252469.png)]

  • ID 是线程的唯一标识,每个线程都有一个独立的id,不同线程不会重复(ID由JVM生成不会重复,name是人为设置可以重复

  • 名称是各种调试工具用到

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

  • 优先级高的线程理论上来说更容易被调度到(优先级越高的线程越有可能被CPU优先执行,但Java程序只是建议优先级高的线程优先执行,到底执行不执行,OS说了算)

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

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

  • 线程的中断问题,下面进一步进行说明

    Thread thread = new Thread(new Runnable() {
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                // 此时Thread.currentThread()指的是调用run的子线程对象
                System.out.println(Thread.currentThread().getName()+"我还活着");
                try{
                    Thread.sleep(1000);
                }catch (InterruptedException e){
                    throw new RuntimeException(e);
                }
            }
            System.out.println(Thread.currentThread().getName()+"I'm going to die...");
        }
    }, "线程a");
    // --------------------------------------以下为主线程
    // 此时Thread.currentThread()指的是主线程
    System.out.println(Thread.currentThread().getName()+": ID is "+Thread.currentThread().getId());
    System.out.println("状态:"+Thread.currentThread().getState());
    System.out.println("优先级:"+Thread.currentThread().getPriority());
    System.out.println("是否是后台线程?"+Thread.currentThread().isDaemon());
    System.out.println("主线程是否是存活?"+Thread.currentThread().isAlive());
    System.out.println("子线程thread是否是存活?"+thread.isAlive());
    thread.start();
    
image-20220702164211751

启动线程

启动线程调用的是Thread类的start方法

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

通过覆写 run 方法可以创建一个线程对象,但线程对象被创建出来并不意味着线程就开始运行了。

  • 覆写 run 方法是提供给线程要做的事情的指令清单
  • 线程对象可以认为是把 李四、王五叫过来了
  • 而调用 start() 方法,就是喊一声:”行动起来!“,线程才真正独立去执行了。

注意:同一多次调用start方法会抛出异常

中断线程

中断线程是线程间通信的一种方式。

普通线程会在run方法执行结束之后自动停止,而==中断线程指的是中断一个正在执行的线程(即run方法还没有执行结束)。==中断线程有两种方式:

1. 通过共享变量进行中断

Thread类的所有静态方法(如:sleep,interruppt等)都是在哪个线程中调用的,就生效在哪个线程

public class ThreadInterruptByVar {
    private static class  MyThread implements Runnable{
        // 在多个线程都会用到的变量加上volatile关键字
        volatile boolean isQuit = false;

        @Override
        public void run() {
            while (!isQuit){
                System.out.println(Thread.currentThread().getName() + "别烦我,我正在跳舞呢~");
                try {
                    // sleep在子线程使用,中断子线程
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName() + "被中断了~");
        }
    }

    public static void main(String[] args) throws InterruptedException {
        MyThread mt = new MyThread();
        Thread thread = new Thread(mt,"Lay的线程");
        System.out.println("Lay可以开始跳舞了~");
        thread.start();
        // sleep在主线程使用,中断主线程
        // 主线程暂停3s,暂停thread
        Thread.sleep(3000);
        System.out.println("音响坏了,让Lay停下");
        mt.isQuit = true;
    }
}

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

  • thread . interrupt():用于修改指定线程为中断状态,调用 线程对象的interrupt() 方法就会将线程对象状态置为中断状态,并抛出一个异常通知。

  • Thread类的内部包含了一个属性, 指示当前线程是否被中断的属性。因此可以通过调用Thread.interrupted()和Thread.currentThread().isInterrupted()判断线程是否中断

线程接收内置中断通知方式

1)当线程因调用了sleep/wait/join等方法而处在阻塞状态时,收到中断通知 thread.interrupt() 后,就会抛出一个中断异常InterruptedException,清除中断标志。(只要抛出中断异常,就一定会清除中断,与使用Thread类中哪种方法判断中断无关)

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

例:

public class ThreadInterruptedByMethod {
    private static class MyRunnble implements Runnable{
        @Override
        public void run() {
            // 1.判断当前线程是否被中断了
            // 1.1 静态方法,清除中断标志
//            while (!Thread.interrupted()) {
            // 1.2 成员方法,判断当前线程对象是否被中断了,不清除中断标志
            while (!Thread.currentThread().isInterrupted()) {
                System.out.println(Thread.currentThread().getName()+"别烦我,正对线呢!");
                try {
                    // 2. 判断线程是否中断后,仍调用sleep/wait/join方法
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    // 当线程被中断时,会抛出中断异常
                    // 抛出中断异常之后,中断标志一定会被清除!!!与调用 1or2哪种方法判断中断无关
                    // 若无break语句线程会继续执行
                    System.err.println("蛮子在偷家!!!!");
                    break;
                }
            }
            System.out.println(Thread.currentThread().getName() + "糟了,水晶被偷了,凉了~");
        }
    }

    public static void main(String[] args) throws InterruptedException {
        MyRunnble mt = new MyRunnble();
        Thread thread = new Thread(mt,"VN线程");
        System.out.println("VN正在对线");
        thread.start();
        Thread.sleep(5*1000);// 5秒
        // 中断子线程
        // 调用此方法就会将子线程的状态置为中断状态
        thread.interrupt();
    }
}

2)线程没有调用以上三种方法时,仅判断内部的中断标志是否被设置,可以通过:

  • Thread.interrupted() 判断当前线程的中断标志是否被设置,若被设置了,清除中断标志 (即若此时返回true,在本次方法调用之后,还原中断状态为false)
  • Thread.currentThread().isInterrupted() 判断指定线程的中断标志是否被设置,若被设置了,不清除中断标志

例:观察标志位是否清除

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

Thread.isInterrupted() 相当于按下开关, 开关自动弹起来了. 这个称为 “清除标志位”

Thread.currentThread().isInterrupted() 相当于按下开关之后, 开关弹不起来, 这个称为"不清除标志位".

/**
 * 两种中断方法的区别
 **/
public class ThreadDoubleMethod {
    private static class MyRun implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < 5; i++) {
// 若判断当前线程被中断,状态为true,返回当前状态并清除(置为false),在下一次循环时返回false
//                System.out.println(Thread.interrupted());
                // 若判断当前线程被中断,状态为true,仅返回当前状态,不清除,下一次循环仍返回true
                System.out.println(Thread.currentThread().isInterrupted());
            }
        }
    }

    public static void main(String[] args) {
        MyRun myRun = new MyRun();
        Thread thread = new Thread(myRun);
        thread.start();
        // 打断子线程
        thread.interrupt();
    }
}

等待一个线程

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

线程对象join():在某个线程A中调用别的线程对象B的join方法,意思就是这个线程A要等待另一个线程B执行完毕后,才能继续执行本线程A的后续代码。

获取当前正在执行的线程对象

Thread.cunrrentThead() =>获取CPU上正在执行的线程对象
说明

休眠当前线程

Thread.sleep(long millis):单位毫秒,在哪个线程里被调用,就休眠哪个线程

例:

/**
 * 线程等待 - join方法 - 成员方法
 **/
public class ThreadJoin {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                System.out.println(Thread.currentThread().getName() + "还在学习JavaSE阶段...");
                try {
                    // 没有sleep,线程嗖的一下就执行完了,无法知道具体用时。
                    // 用sleep休眠一秒,使得线程运行用时可以被统计记录下来,进行精确控制
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        },"JavaSE线程");
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                System.out.println(Thread.currentThread().getName() + "进入数据结构的学习部分");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        },"数据结构线程");
        System.out.println("先学习JavaSE");
        t1.start();
        // 主线程死等t1,直到t1执行结束主线程再恢复执行
        t1.join();
        // 此时走到这里,t1线程已经执行结束,再启动t2线程
        t2.start();
        
        // 1. main中 -> 调用t2.join() 阻塞主线程,直到t2完全执行结束再恢复主线程的执行
        // 在某个线程A里,让另一个线程对象B调用join()方法,则该线程A被阻塞,必须等另一个线程B执行完毕后才恢复运行
//        t2.join();
        
        // 2. 主线程只等t2 2000ms - 2s,若t2在2s之内还没结束,主线程就会恢复执行
        t2.join(2000);
        
        // t2线程也执行结束了,继续执行主线程
        System.out.println("开始学习JavaEE");
        System.out.println(Thread.currentThread().getName());
    }
}

线程的状态

/**
 * 线程状态
 */
public class ThreadState {
    public static void main(String[] args) {
        for(Thread.State state: Thread.State.values()){
            System.out.println(state); 
        }
    }
}

image-20220703160632600

状态类型

  • NEW: 新建状态,安排了工作, 还未开始行动
  • RUNNABLE: 可执行状态,是新建状态的下一个状态,可以分成正在运行(RUNNING)和即将开始执行(READY)。
  • BLOCKED \ WAITING \ TIMED_WAITING:这三个状态都是等待状态,当前线程暂停执行,等待其他任务或资源,但造成的暂缓执行的原因不同:
    • WAITING:lock.wait(),线程等待,等待被另一个线程唤醒(notify方法)
    • TIMED_WAITING:超时等待,需要等待一段时间后自动唤醒
    • BLOCKED:锁等待,需要等待其他线程释放锁对象
  • TERMINATED: 终止状态,当前线程已经之行结束了,可以被销毁

以下三种状态都属于线程的阻塞状态(该线程需要暂缓执行,):

状态相关函数

  • thread.isAlive():判断线程是否存活。除了New和Terminated状态,都是存活状态

  • thread.getState():获取线程当前状态

    • NEW: 新创建的线程对象就处于new状态
    • RUNNABLE: 就绪和运行都是runnable状态
    • TERMINATED:线程的run方法执行完毕,或者抛出异常不正常执行完毕都会进入终止状态
    // 产生一个线程对象,该对象默认的状态就是新建状态 - NEW
    Thread t = new Thread(() -> {
        for (int i = 0; i < 100_000; i++) {}
    },"子线程");
    System.out.println(t.getName() + " : " + t.getState());
    t.start();
    while (t.isAlive()) {
        System.out.println(t.getName() + " : " + t.getState());
    }
    System.out.println(t.getName() + " : " + t.getState());
    

synchronized 关键字-监视器锁monitor lock

synchronized 的特性

1) 互斥

synchronized 会起到互斥效果, 某个线程执行到某个对象的 synchronized 中时, 其他线程如果也执行到同一个对象 synchronized 就会阻塞等待.

  • 进入 synchronized 修饰的代码块, 相当于 加锁
  • 退出 synchronized 修饰的代码块, 相当于 解锁

synchronized用的锁是在Java对象里的。

可以粗略理解成, 每个对象在内存中存储的时候, 都存有一块内存表示当前的 “锁定” 状态(类似于公共厕所的 “有人/无人”).

如果当前是 “无人” 状态, 那么就可以使用, 使用时需要设为 “有人” 状态.

如果当前是 “有人” 状态, 那么其他人无法使用, 只能排队

一个线程上了锁,其他线程只能等待这个线程释放。

理解 “阻塞等待”.

针对每一把锁, 操作系统内部都维护了一个等待队列. 当这个锁被某个线程占有的时候, 其他线程尝试进行加锁, 就加不上了, 就会阻塞等待, 一直等到之前的线程解锁之后, 由操作系统唤醒一个新的线程, 再来获取到这个锁.

注意:

上一个线程解锁之后, 下一个线程并不是立即就能获取到锁. 而是要靠操作系统来 “唤醒”. 这也就是操作系统线程调度的一部分工作.

假设有 A B C 三个线程, 线程 A 先获取到锁, 然后 B 尝试获取锁, 然后 C 再尝试获取锁, 此时 B 和 C 都在阻塞队列中排队等待. 但是当 A 释放锁之后, 虽然 B 比 C 先来的, 但是 B 不一定就能获取到锁, 而是和 C 重新竞争, 并不遵守先来后到的规则.

例:

Object lock = new Object();
// 线程t1,t2竞争lock对象里的同一把锁,t1比t2先执行,因此t2需等到t1执行完毕后才可以执行
Thread t1 = new Thread(()->{
    synchronized (lock){
        while(true){
            try {
                System.out.println("111");
                // TIMED_WAITING 等待时间到了自动唤醒
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
},"t1");
t1.start();
Thread t2 = new Thread(()->{
    synchronized (lock){
        System.out.println("happy day");
    }
},"t2");
t2.start();

线程t1超时等待,该线程需要等待一段时间之后再恢复执行

该线程t2在等待别的线程释放资源

wait 和 notify

由于线程之间是抢占式执行的, 因此线程之间执行的先后顺序难以预知,但有时希望线程之间的执行有先后顺序,因此需用到以下三个方法:

  • wait() / wait(long timeout): 让当前线程进入等待状态.
  • notify() / notifyAll(): 唤醒在当前对象上等待的线程.

注意: wait, notify, notifyAll 都是 Object 类的方法

wait()方法

注意wait 要搭配 synchronized 来使用,脱离 synchronized 使用 wait 会直接抛出异常.

wait 做的事情:

  • 使当前执行代码的线程进行等待. (把线程放到等待队列中)
  • 释放当前的锁
  • 满足一定条件时被唤醒, 重新尝试获取这个锁.

wait 结束等待的条件:

  • 其他线程调用该对象的 notify 方法.

  • wait 等待时间超时 (wait 方法提供一个带有 timeout 参数的版本, 来指定等待时间).

  • 其他线程调用该等待线程的 interrupt方法, 导致 wait 抛出 InterruptedException 异常.

    例:更改t1线程,令t1线程进入等待队列,先执行t2队列

            Thread t1 = new Thread(()->{
                synchronized (lock){
                    while(true){  // 死循环
                        try {
    //  1. 线程t1,t2竞争lock对象里的同一把锁,t1比t2先执行,因此t2需等到t1执行完毕后才可以执行
                            // TIMED_WAITING 等待时间到了自动唤醒
    //                        Thread.sleep(1000);
    
                            // 2. 令线程进入等待状态,释放当前的锁,分配给t2继续执行
                            lock.wait();
                            System.out.println("我被唤醒了~");
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            },"t1");
    

yield()

yield():会使线程从运行状态转为就绪态(了解,使用不多,都不可控)

  • 调用yield方法的线程会主动让出CPU资源,从运行态转为就绪态,等待被CPU继续调度。(处于就绪态的线程在等待队列中,不缺任何资源,随时可以被CPU调度)

  • 但到底啥时候让出CPU,又是啥时候被CPU再次调度,都是os调度的,我们无权选择:

    ​ 1) 让出之后立马又被调度了

    ​ 2) 让出之后很久都不调度

Thread t1 = new Thread(() -> {
    while (true) {
        System.out.println(Thread.currentThread().getName());
        // Lay线程会让出CPU,进入就绪态,等待被CPU继续调度
        // 未使用yield之前,两个线程被调用的次数差不多,使用之后Z线程被调用次数变多
        Thread.yield();
    }
},"Lay线程");
t1.start();
Thread t2 = new Thread(() -> {
    while (true) {
        System.out.println(Thread.currentThread().getName());
    }
},"Z线程");
t2.start();
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值