• Java多线程上——基本概念及操作


    多线程定义:

    1. 线程:操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位
    2. 进程:计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位。
    3. 线程与进程区别:
    • 调度:进程是资源管理的基本单位,线程是程序执行的基本单位。
    • 切换:线程上下文切换比进程上下文切换要快得多。
    • 拥有资源: 进程是拥有资源的一个独立单位,线程不拥有系统资源,但是可以访问隶属于进程的资源。
    • 系统开销: 创建或撤销进程时,系统都要为之分配或回收系统资源,如内存空间,I/O设备等,OS所付出的开销显著大于在创建或撤销线程时的开销,进程切换的开销也远大于线程切换的开销。

    多线程编程

    首先废话不多说,我们先来运行如下一段代码感受一下多线程:

    package demo1;
    
    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();
                }
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40

    感受到了吧,我猜你只发现了他会一直不停的运行。。。。。
    所以接下来我们就正式来看看这到底是怎么个东西。

    创建线程方法

    1. 继承 Thread 类
    1. 创建一个线程类继承Thread方法
    2. 用线程类实例化对象
    3. 启动线程
    public class ThreadDemo {
        private static class MyThread extends Thread {
            //新建类继承Thread,使这个类变成线程类
            @Override
            public void run() {
                System.out.println("这里是线程运行的代码");
            }
        }
    
        public static void main(String[] args) {
            //用线程类实例化对象
            MyThread thr1 = new MyThread();
            //启动线程
            thr1.start();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    1. 实现 Runnable 接口
    1. 实现 Runnable 接口
    2. 创建 Thread 类实例, 调用Thread的构造方法时将Runnable对象作为target参数
    3. 调用start方法启动线程
    public class demo2 {
        static class MyRunnable implements Runnable{
            //新建线程类实现Runnable接口
            @Override
            public void run() {
                System.out.println("这里是线程运行的代码");
            }
        }
    
        public static void main(String[] args) {
           Thread thr1 = new Thread(new MyRunnable());
           thr1.start();
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    1. 当然还有他们的几种变形
    • 匿名内部类创建 Thread 子类对象
    public class demo3 {
        public static void main(String[] args) {
            // 使用匿名类创建Thread 子类对象
            Thread t1 = new Thread() {
                @Override
                public void run() {
                    System.out.println("使用匿名类创建 Thread 子类对象");
                }
            };
            t1.start();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 匿名内部类创建 Runnable 子类对象
    public class 匿名内部类创建Runnable子类对象 {
        public static void main(String[] args) {
            // 使用匿名类创建 Runnable 子类对象
            Thread t2 = new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println("使用匿名类创建 Runnable 子类对象");
                }
            });
            t2.start();
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • lambda 表达式创建
      Java8引入的东西,可以重新了解一下,有机会的话,会出相关内容,重新贴链接
    public class lambda表达式创建线程 extends Thread{
        public static void main(String[] args) {
            new Thread(()->{
                System.out.println("lambda方式创建线程");
            }).start();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    氮气加速——多线程:
    故名思义,使用多线程的好处就是加速,在某些场合下是可以提高程序的整体运行效率的。

    首先我们来看一段代码及结果感受一下

    public class ThreadAdvantage {
        // 多线程并不一定就能提高速度,可以观察,count 不同,实际的运行效果也是不同的
        private static final long count = 10000000;
        public static void main(String[] args) throws InterruptedException {
    // 使用并发方式
            concurrency();
    // 使用串行方式
            serial();
        }
    
        private static void concurrency() throws InterruptedException {
            long begin = System.nanoTime();
            // 利用一个线程计算 a 的值
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    int a = 0;
                    for (long i = 0; i < count; i++) {
                        a--;
                    }
                }
            });
            thread.start();
    // 主线程内计算 b 的值
            int b = 0;
            for (long i = 0; i < count; i++) {
                b--;
            }
    // 等待 thread 线程运行结束
            thread.join();
    
    // 统计耗时
            long end = System.nanoTime();
            double ms = (end - begin) * 1.0 / 1000 / 1000;
            System.out.printf("并发: %f 毫秒%n", ms);
        }
    
        private static void serial() {
    // 全部在主线程内计算 a、b 的值
            long begin = System.nanoTime();
            int a = 0;
            for (long i = 0; i < count; i++) {
                a--;
            }
            int b = 0;
            for (long i = 0; i < count; i++) {
                b--;
            }
            long end = System.nanoTime();
            double ms = (end - begin) * 1.0 / 1000 / 1000;
            System.out.printf("串行: %f 毫秒%n", ms);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53

    运行结果如下:
    在这里插入图片描述


    Thread 类及常见方法

    前面一直介绍Thread类,我们也听了很多遍了,下面来正式了解一下这个类和其常见方法

    1. 构造方法
      在这里插入图片描述
    2. 常见属性
      在这里插入图片描述
      JVM会在一个进程的所有非后台线程结束后,才会结束运行。

    启动问题(start() 与 run())

    记住线程的启动是start()

    区别
    1. start:用start方法来启动线程,真正实现了多线程运行,这时无需等待run方法体代码执行完毕而直接继续执行下面的代码。通过调用Thread类的start()方法来启动一个线程,这时此线程处于就绪(可运行)状态,并没有运行,一旦得到cpu时间片,就开始执行run()方法,这里方法 run()称为线程体,它包含了要执行的这个线程的内容,Run方法运行结束,此线程随即终止。
    2. run: run()方法只是类的一个普通方法而已,如果直接调用Run方法,程序中依然只有主线程这一个线程,其程序执行路径还是只有一条,还是要顺序执行,还是要等待run方法体执行完毕后才可继续执行下面的代码,这样就没有达到写线程的目的。
    3. 总结:调用start方法方可启动线程,而run方法只是thread的一个普通方法调用,还是在主线程里执行。这两个方法应该都比较熟悉,把需要并行处理的代码放在run()方法中,start()方法启动线程将自动调用 run()方法,这是由jvm的内存机制规定的。并且run()方法必须是public访问权限,返回值类型为void。

    看完那个解释是不是更晕了,嗯。。。,知道会晕所以我们来看一图就明白了
    在这里插入图片描述

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

    start()也只是加入就绪队列,真正的开始执行由cpu决定

    中断线程

    等待一个线程-join()

    这个用法没啥讲的,可以看下面这段代码运行感受一下

    public static void main(String[] args) throws InterruptedException {
            Runnable target = () -> {
                for (int i = 0; i < 10; i++) {
                    try {
                        System.out.println(Thread.currentThread().getName() + ": 我还在工作!");
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println(Thread.currentThread().getName() + ": 我结束了!");
            };
            Thread thread1 = new Thread(target, "李四");
            Thread thread2 = new Thread(target, "王五");
            System.out.println("先让李四开始工作");
            thread1.start();
            thread1.join();
            System.out.println("李四工作结束了,让王五开始工作");
            thread2.start();
            thread2.join();
            System.out.println("王五工作结束了");
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    当然还有休眠sleep()【上面代码其实已经看到了】;currentThread()获取当前线程引用等方法,我们不一一介绍了,不然太划水了。


    线程的状态

    在这里插入图片描述


    线程安全

    前面的方法做了解会用知道咋回事就好了,但是这个可是重点哟~
    我们先来看一段代码:

     static class Counter {
            public int count = 0;
            void increase() {
                count++;
            }
        }
    
        public static void main(String[] args) throws InterruptedException {
            final Counter counter = new Counter();
            Thread t1 = new Thread(() -> {
                for (int i = 0; i < 50000; i++) {
                    counter.increase();
                }
            });
            Thread t2 = new Thread(() -> {
                for (int i = 0; i < 50000; i++) {
                    counter.increase();
                }
            });
            t1.start();
            t2.start();
            t1.join();
            t2.join();
            System.out.println(counter.count);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    很明显结果应该是100000,但是下面是我们的运行结果:
    在这里插入图片描述
    在这里插入图片描述
    那么为什么会这样?
    接下来我们正式了解线程安全:

    线程安全定义

    如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线程安全的。

    线程不安全的原因
    jvm内存

    在这里插入图片描述
    在这里插入图片描述
    通过这两张图,我们很明显能看出来哪些东西线程之间共享,哪些是有。

    1. 内存区域:
      共享:堆、方法区、运行时常量池;
      私有:pc、栈
    2. 表现在代码中的话:
      共享:对象、类对象、静态属性;私有:局部变量
    线程不安全原因
    1. 开发者角度:
    1. 共享数据:多个线程之间操作同一块数据
    2. 并且这两个线程至少有一个线程在修改这块共享数据
    1. 系统角度:
      注:
      1. 高级语言的一条语句,可能对应多条指令;
      2. 线程调度可能发生在任一时刻(不会切割指令);
      3. cpu为了提高数据获取速度会设置缓存(Cache):因为指令的执行速度远远大于内存读写速度
      4. jvm对内存进行了模拟,内存:jvm主内存;缓存:jvm:工作内存
      5. 我们写的代码在编译阶段会进行代码重排序(jvm要求无论如何优化,当线程角度结果不应该变化,所以多线程环境可能出问题)
    1. 原子性被破坏
    2. 内存可见性问题,导致某些线程读取到脏数据
    3. 代码重排序,导致线程之间数据配合出现问题

    解决线程不安全方法:

    回顾三个阶段我们知道破环线程安全主要是如下三个条件:

    1. 原子性被破坏
    2. 内存可见性问题,导致某些线程读取到脏数据
    3. 代码重排序,导致线程之间数据配合出现问题

    那么嘿嘿嘿。。。解决方案不就来了嘛:

    synchronized 关键字-监视器锁

    保证原子性,当然他也能保证内存可见性

    在这里插入图片描述
    注意:

    1. 上一个线程解锁之后, 下一个线程并不是立即就能获取到锁. 而是要靠操作系统来 “唤醒”. 这也就是操作系统线程调度的一部分工作.
    2. 假设有 A B C 三个线程, 线程 A 先获取到锁, 然后 B 尝试获取锁, 然后 C 再尝试获取锁, 此时 B和 C 都在阻塞队列中排队等待. 但是当 A 释放锁之后, 虽然 B 比 C 先来的, 但是 B 不一定就能获取到锁, 而是和 C 重新竞争, 并不遵守先来后到的规则.

    当然关于锁的理解这一点肯定是不够的,我们下一篇将详细介绍这个东西

    volatile 关键字

    保证内存可见性,无法保证原子性
    在这里插入图片描述
    上面是jvm的内存工作,下面是加入volatile关键字时的操作;

    1. 代码在写入 volatile 修饰的变量的时候:
      改变线程工作内存中volatile变量副本的值
      将改变后的副本的值从工作内存刷新到主内存
    2. 代码在读取 volatile 修饰的变量的时候:
      从主内存中读取volatile变量的最新值到线程的工作内存中
      从工作内存中读取volatile变量的副本

    那么是什么意思?还记得我们之前说过:直接访问工作内存(实际是 CPU 的寄存器或者 CPU 的缓存), 速度非常快, 但是可能出现数据不一致的情况,而加上 volatile , 强制读写内存. 速度是慢了, 但是数据变的更准确了.
    理论是不是很烦我们来看一段代码理解一下:

    static class Counter {
            public int flag = 0;
        }
        public static void main(String[] args) {
            Counter counter = new Counter();
            Thread t1 = new Thread(() -> {
                while (counter.flag == 0) {
                    // do nothing
                }
                System.out.println("循环结束!");
            });
            Thread t2 = new Thread(() -> {
                Scanner scanner = new Scanner(System.in);
                System.out.println("输入一个整数:");
                counter.flag = scanner.nextInt();
            });
            t1.start();
            t2.start();
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    在这里插入图片描述

    很明显当我们输入0是上面的代码应该停止运行,然而并不会那样,因为有缓存,所以我们给flag加上volatile关键字,这样,就会从内存读写,咳咳咳。。解决了。

    static class Counter {
            public volatile int flag = 0;
        }
    
    • 1
    • 2
    • 3

    如下图所示:
    在这里插入图片描述

    当然这个只是基本知识,其他的后续将出,当然推荐大家自己动手实验一下;

    wait 和 notify

    由于线程之间是抢占式执行的, 因此线程之间执行的先后顺序难以预知,但是实际开发中有时候我们希望合理的协调多个线程之间的执行先后顺序.

    wait()方法

    1. wait 做的事情:
    • 使当前执行代码的线程进行等待. (把线程放到等待队列中)
    • 释放当前的锁
    • 满足一定条件时被唤醒, 重新尝试获取这个锁.
    • wait 要搭配 synchronized 来使用, 脱离 synchronized 使用 wait 会直接抛出异常.
    1. wait 结束等待的条件:
    • 其他线程调用该对象的 notify 方法.
    • wait 等待时间超时 (wait 方法提供一个带有 timeout 参数的版本, 来指定等待时间).
    • 其他线程调用该等待线程的 interrupted 方法, 导致 wait 抛出 InterruptedException 异常

    notify()方法

    1. notify 方法是唤醒等待的线程.
    • 方法notify()也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的其它线程,对其发出通知notify,并使它们重新获取该对象的对象锁。
    • 如果有多个线程等待,则有线程调度器随机挑选出一个呈 wait 状态的线程。(并没有 “先来后到”)
    • 在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行完,也就是退出同步代码块之后才会释放对象锁。

    我们来举个例子看一下:

    static class WaitTask implements Runnable {
            private Object locker;
            public WaitTask(Object locker) {
                this.locker = locker;
            }
            @Override
            public void run() {
                synchronized (locker) {
                    while (true) {
                        try {
                            System.out.println("wait 开始");
                            locker.wait();
                            System.out.println("wait 结束");
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }
        static class NotifyTask implements Runnable {
            private Object locker; public NotifyTask(Object locker) {
                this.locker = locker;
            }
            @Override
            public void run() {
                synchronized (locker) {
                    System.out.println("notify 开始");
                    locker.notify();
                    System.out.println("notify 结束");
                }
            }
        }
        public static void main(String[] args) throws InterruptedException {
            Object locker = new Object();
            Thread t1 = new Thread(new WaitTask(locker));
            Thread t2 = new Thread(new NotifyTask(locker));
            t1.start();
            Thread.sleep(5000);
            t2.start();
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41

    在这里插入图片描述
    结果很明显了吧,嗯。。。就是这样等5秒后会notify介入,会让等待的东西继续进入,当然,这个一次只能唤醒一个线程,所以notifyAll()方法故名思义,唤醒所有的等待线程。不过当然如上代码,因为如果有其他线程竞争同一个资源,一起唤醒(notify()),其唤醒过程也是有顺序的。如下图所示:
    在这里插入图片描述

    wait 和 sleep 的对比

    1. wait 需要搭配 synchronized 使用. sleep 不需要.
    2. wait 是 Object 的方法 sleep 是 Thread 的静态方法.
    3. sleep方法属于Thread类中的静态方法,wait属于Object的成员方法。
    4. sleep()是线程类(Thread)的方法,不涉及线程通信,调用时会暂停此线程指定的时间,但监控依然保持,不会释放对象锁,到时间自动恢复;wait()是Object的方法,用于线程间的通信,调用时会放弃对象锁,进入等待队列,待调用notify()或者notifyAll()唤醒指定的线程或者所有线程,才进入对象锁定池准备获得对象锁进入运行状态。
    5. sleep()方法必须捕获异常InterruptedException,而wait()\notify()以及notifyAll()不需要捕获异常。

    这俩本来关系基本接近于0,就像final和finally一样,一个用法像,一个长得像,但是本质没关系,就像现在很多奇怪的的社会现象,孩子是你在养、却和你一点关系都没有、扎心了吧老铁。。。

    下一篇中,就该考虑一下定时器、线程池、模式等的一些简单用法了,敬请关注

  • 相关阅读:
    【SCAU操作系统】期末复习选择题例题解析
    java项目中git的.ignore文件设置
    Java类和对象
    安卓基础学习 Day 28|绘制文本、图形与图像
    一文搞定Vue2组件通信
    给电脑加硬件的办法 先找电脑支持的接口,再买相同接口的
    数据分析篇-数据认知分析
    antd3和dva-自定义组件初始化值的操作演示和自定义组件校验
    Oracle 快照及 dblink使用 (两台服务器数据同步)
    阿里云ESSD云盘IOPS性能如何?
  • 原文地址:https://blog.csdn.net/weixin_45696320/article/details/126208708