• 多线程(基础) - 4万字总结


    1. 线程(Thread)

    1.1 概念

    1) 什么是线程

    在上一篇我们学习了进程可以利用"并发编程"来实现对CPU多核资源的有效利用,但在某些场景下,会存在某些问题,比如需要频繁的创建/销毁进程,这是因为在创建进程时,需要先创建PCB,然后分配系统资源,这一步十分消耗时间(这个是在系统内核资源管理模块,进行一系列遍历操作的,遍历就是十分消耗时间的操作),最后把PCB加入到内核的双向链表中.

    为了提高这个场景下的效率,就引入了"线程",一个进程中可以包含许多线程,每个线程有自己的PCB,同一个进程中的多个线程之间,共用同一份资源(这就意味着,不必给它分配新的资源,直接复用之间的即可,如果不够再去申请),因此,创建线程只需要先创建PCB,然后把PCB加入内核的双向链表中,所以线程的效率更加高.

    2) 线程的作用

    举个例子: 小黑子自己有个鸡场,里面的鸡都会"打篮球",许多游客慕名而来,给小黑子挣得许多钱,于是同行十分嫉妒,也要训练自己的鸡;现在他有100只鸡,现在要训练这些鸡如何才能在质量高并且时间短的情况下,训练好这些鸡.

    >

    从这个例子中看到多线程的缺点,这是我们后面重点掌握(优化处理这些缺点),但是以看到他的优点

    • 使用多核CPU和"并发编程"可以**提高CPU的运算效率.**在计算大量数据时利用多线程可以大大提高执行效率

    • 在某些任务场景中"等待IO"(比如读写硬盘,输出数据…),为了让等待IO的时间让CPU去做其他工作(使用多线程让它去完成其他任务),这时候就可以使用并发编程.

    • 线程比进程更加轻量,因为线程是在进程内部

    • 创建线程比创建进程更快.

    • 销毁线程比销毁进程更快.

    • 调度线程比调度进程更快.

    3) 进程和线程的区别和联系

    1. 进程是包括线程的,线程是在进程内部.
    2. 每个进程有自己独立的虚拟地址空间(进程与进程之间的资源是独立的),同一个进程中的多个线程之间,共用这一份资源,但是不同进程里面的不同线程,则没有共享的资源(隔离性)
    3. 进程是操作系统中资源分配的基本单位,线程是操作系统调度的基本单位.(系统内核不认线程/进程,只认PCB).

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7nDycrVy-1659257738991)(C:\Users\a\AppData\Roaming\Typora\typora-user-images\image-20220722165848037.png)]

    1. 多个进程同时执行,如果一个进程崩溃,一般不会影响其他进程(隔离性),同一个进程中的多个线程之间,如果一个线程崩溃,很可能把整个进程带走,与此同时同进程中的其他线程也没了.
    2. 进程之间有父子关系(即父进程和子进程),父进程为了完成这个任务创建子进程与其共同完成,线程之间是同级关系,进程为了完成任务,创建了许多线程(进程是他们的父亲),这些线程是同级的(都是兄弟姐妹).

    1.2 创建线程

    1) 创建线程方法

    在我们学习任何一门语言时,都会学习的一段代码,也涉及到"线程":

    public static void main(String[] args) {
        System.out.println("Hello world!");
    }
    
    • 1
    • 2
    • 3

    在上述的代码中虽然没有创建任何线程,但是在Java进程运行时,其内部会自动创建多个线程,在这个java进程中就会有一个线程调用main方法.


    下面我们将介绍几种常见的创建线程的方法;

    1. 继承 Thread, 重写 run
    class MyThread extends Thread {
        @Override
        public void run() {
            System.out.println("Hello thread!");
        }
    }
    public class Demo1 {
        public static void main(String[] args) {
            Thread t = new MyThread();
            t.start();
    
            System.out.println("Hello main!");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    我们继承于Tread类(该类属于Java.lang包下的类,会自动添加,不需要手动导包),然后重写其run方法,这是因为要明确这个线程的"任务".

    然后采用向上转型的方法构造实例对象,然后开始使用t.start()方法创建该线程,随后我们执行代码发现,我们是先启动的是线程,应该是先打印"Hello Thread!",然后打印"Hello main!"这和我们预想的不太一样.

    这是因为:

    1. 每个线程是独立的执行流:main线程和Thread线程是相互独立,但是它们是并发的执行关系.
    2. 线程的执行顺序取决于操作系统调度器的具体实现

    为了看见这个并发性,我们修改一下代码.

    class MyThread extends Thread {
        @Override
        public void run() {
            while (true) {
                System.out.println("Hello thread!");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    public class Demo1 {
        public static void main(String[] args) {
            Thread t = new MyThread();
            t.start();
    
            while (true) {
                System.out.println("Hello main!");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    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

    现在就很明显的看见这个"并发性",

    可以理解jconsole来观察线程:

    注:一定要把线程跑起来才能看见哦!!

    1. 在jdk的bin目录下找到这个可执行程序,双击打开

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-foZz4TL6-1659257738993)(C:\Users\a\AppData\Roaming\Typora\typora-user-images\image-20220723004649502.png)]

    1. 找到当前运行代码的类名(如果什么都没有,点击右键以管理员身份打开),点击连接,

    1. 点击不安全的连接

    1. 查看线程

    点击这个线程就可以看见其状态,线程的调用栈,

    1. 实现 Runnable, 重写 run
    class MyRunnable implements Runnable {
        @Override
        public void run() {
            while (true) {
                System.out.println("Hello Runnable!");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    public class Demo2 {
        public static void main(String[] args) {
            Runnable runnable = new MyRunnable();
            Thread t = new Thread(runnable);
            t.start();
    
            while (true) {
                System.out.println("Hello main!");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    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

    通过查看Runnable接口可以看见里面只有一个抽象接口,指明了这个接口的作用就是明确这个进程的"任务"是什么?

    既然是任务,那么就要给线程来完成,Thread提供了这个构造方法;

    Runnable runnable = new MyRunnable();
    Thread t = new Thread(runnable);
    
    • 1
    • 2

    这样就把任务和线程分离开,把代码的耦合性降低,

    1. 继承 Thread, 重写 run, 使用匿名内部类
    public static void main(String[] args) {
        Thread t = new Thread(){
            @Override
            public void run() {
                System.out.println("Hello thread!");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        t.start();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    1. 实现 Runnable, 重写 run, 使用匿名内部类
    public static void main(String[] args) {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("Hello thread!");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    1. 使用 lambda 表达式
    public static void main(String[] args) {
        Thread t = new Thread(() ->{
            while (true) {
                System.out.println("Hello thread!");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    注意事项:

    run()和start()的区别

    执行start()方法

    public static final int COUNT = 5;
    public static void main(String[] args) {
        //t线程的任务
        Thread t = new Thread(() -> {
            int a = 0;
            while (a++ < COUNT) {
                System.out.println("Thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
        //main线程的任务
        int a = 0;
        while (a++ < COUNT) {
            System.out.println("main");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                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

    输出结果:

    main和Thread交替打印;

    分析:从执行经过可以看出start()方法可以启动一个新的线程,此时该线程就处于就绪(可运行)状态,并没有运行,当得到CPU时间片时,就会开始执行run()方法,此时新线程与旧线程是"并发关系",使用start()方法是实现了多线程,直到run()方法结束,随即该新线程结束.

    执行run()方法

    public static final int COUNT = 5;
    public static void main(String[] args) {
        //t线程的任务
        Thread t = new Thread(() -> {
            int a = 0;
            while (a++ < COUNT) {
                System.out.println("Thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.run();
        //main线程的任务
        int a = 0;
        while (a++ < COUNT) {
            System.out.println("main");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                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

    执行结果:

    先完成t线程的run()的任务,随后完成main线程的任务.

    分析:直接使用run()方法,并没有创建新线程,而是之前的线程(主线程)来执行run()方法的内容,随后顺序执行后面的内容.

    主线程:由java进程创建的主线程来调用main方法的线程

    总结区别:

    1. 当使用start()方法时会创建一个新线程,新线程和旧线程之间是并发运行的,也就是说执行结果是受操作系统调度的,最后的结果是随机的,而run()方法,会在由当前的主线程来调用run()方法来调用方法.
    2. 一个线程只能start()一次,而run()方法可以执行多次

    总结起来就是run()就是一个普通的方法,而start()会创建一个新线程去执行run()的代码。

    2) 多线程的优势

    在某些场景下多线程可以增加程序的运行效率

    一个采用串行方式(让一个线程去完成任务),哪一个采用并发方式(让多个线程去完成任务).

    private static final long COUNT = 10_0000_0000;
    public static void main(String[] args) {
        //采用串行方式
        serial();
        //采用并发方式
        concurrency();
    }
    
    private static void concurrency() {
        long begin = System.currentTimeMillis();
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                int a = 0;
                while (a++ < COUNT);
            }
        });
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                int a = 0;
                while (a++ < COUNT);
            }
        });
        t1.start();
        t2.start();
        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    
        long end = System.currentTimeMillis();
        System.out.println("concurrency: " + (end - begin));
    }
    
    private static void serial() {
        long begin = System.currentTimeMillis();
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                int a = 0;
                while (a++ < COUNT << 1);
            }
        });
        t.start();
        try {
            t.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        long end = System.currentTimeMillis();
        System.out.println("serial: " + (end - begin));
    
    }
    
    • 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
    • 54
    • 55
    • 56

    为什么要使用join()方法?

    在这个Java进程中,当我们启动mian函数时,在concurrency()函数中创建了三个线程,分别是t1,t2,concurrency;这三者的关系就如同在长跑比赛中,它们就是三位选手,如果没有使用join()那么会在第一位选手达到终点时,结束计时,使用join()就会等待所有选手到达终点,才结束计时.注意即使使用jion()也不是一直等待某个线程结束才开始下一个线程,每一个线程都是并发的,就像上面的跑步一样,每个人都在跑,并不确定谁会在前面.

    在jion()的源码中可以看到:

    对比下来,效率确实高,但是效率也不是成倍的增长,这是因为:

    1. 线程的创建是需要时间的(分配资源).
    2. 由于多个线程在执行任务时,可能一会是并行,一会是串行.
    3. 线程的调度需要时间开销

    2. Thread 类介绍

    Thread类是专门用来组织和操作线程的类,每个线程都会有唯一的Thread对象与之对应,然后JVM会把这些Thread对象组织起来,用于线程调度,线程管理.

    2.1 构造方法

    方法说明
    Thread()创建线程对象
    Thread(Runnable target)使用 Runnable 对象创建线程对象
    Thread(String name)创建线程对象,并命名
    Thread(Runnable target, String name)使用 Runnable 对象创建线程对象,并命名
    Thread(ThreadGroup group, Runnable target)使用 Runnable 对象创建线程组

    对于默认姓名位"Thread- ?", ?为当前是第几个未命名线程.

    2.2 常见属性

    属性获取方法
    IDgetId()
    名称getName()
    状态getState()
    优先级getPriority()
    是否后台线程isDaemon()
    是否存活isAlive()
    是否被中断isInterrupted()
    1. ID

    获取的是线程在JVM中的身份标识,在内核的PCB,用户态线程库中都有标识,它们虽然名字不同,但都是作为身份的标识

    1. 名称

    程序员给线程提供的名称

    1. 状态

    在JVM中的设立的状态体系.

    1. 优先级

    获取当前线程的优先级

    1. 后台线程

    线程分为前台程序和后台程序(守护线程),

    • 后台程序: 指为其他线程提供服务的线程,也称为守护线程。JVM的垃圾回收线程就是一个后台线程
    • 前台程序: 是指接受后台线程服务的线程,前后线程是类似于提线木偶(前台程序)和艺人(后台程序)的关系,属于由后台程序创建的是前台程序,由前台程序创建的是前台程序.

    二者的区别和联系:

    1. 后台线程不会阻止进程的终止。属于某个进程的所有前台线程都终止后,该进程就会被终止。所有剩余的后台线程会停止且不会完成。用户线程就可以认为是系统的工作线程,它会完成整个系统的业务操作。用户线程完全结束后就意味着整个系统的业务任务全部结束了,因此系统就没有对象需要守护的了,守护线程自然而然就会退。
    2. 在创建线程对象后,使用setDaemon()方法设置为后台程序,在start()之前,否则会抛出java.lang.IllegalThreadStateException异常
    3. 不管是前台线程还是后台线程,如果线程内出现了异常,都会导致进程的终止。
    4. 默认创建的线程为前台程序

    1. 是否存活

    检查在某一时刻线程是否存活

    1. 是否被中断

    判断当前线程是否被中断

    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            while (true) {
                System.out.println("Thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"ME!");
        t.start();
        System.out.println(t.getId());
        System.out.println(t.isAlive());
        System.out.println(t.isDaemon());
        System.out.println(t.getName());
        System.out.println(t.getPriority());
        System.out.println(t.isInterrupted());
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    2.3 启动线程

    源码:

    使用start()方法创建一个新线程,并把该线程放入"可运行线程池",变得可运行,等待并得到CPU时间片后就会执行run()方法中的内容,这时候才真的在操作系统内核中创建一个新线程.

    2.4 中断线程

    每个线程都有自己的任务,所以对应的"中断线程"就是结束当前任务,不管任务是否完成,最后结束这个线程

    如何手动退出线程呢?

    1. 设置标志位
    private static boolean isQuit = false;
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            while (!isQuit) {
                System.out.println("线程运行中...");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("线程终止");
        });
        t.start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("标志位改变!");
        isQuit = true;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    1. 利用内部标记位

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

    方法说明
    public void interrupt()中断对象关联的线程,如果线程正在阻塞,则以异常方式通知, 否则设置标志位
    public static boolean interrupted()判断当前线程的中断标志位是否设置,调用后清除标志位
    public boolean isInterrupted()判断对象关联的线程的标志位是否设置,调用后不清除标志位
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            while (!Thread.currentThread().isInterrupted()) {
                System.out.println("线程运行中...");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("线程退出!");
        });
        t.start();
        Thread.sleep(2000);
        System.out.println("控制线程退出");
        t.interrupt();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    输出结果:

    可以看出该线程只是抛出个异常,然后线程并没有中断,只是由于这个异常,使我们可以自定义在中断线程后的操作.

    interrupt方法的作用:

    1. 如果该线程没有处于阻塞状态,此时就会逆转内置的标记位
    2. 如果该线程正在处于阻塞状态,此时就会让"使线程产生堵塞的方法,抛出异常",比如上方sleep()就是产生异常.

    总而言之,interrupt()不能中断在运行中的线程,它只能改变中断状态。

    Thread.isInterrupted() , 线程中断会清除标志位

    public static void main(String[] args) {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 1; i <= 10; i++) {
                    System.out.println("第 " + i + " 次的中断状态: "+ Thread.interrupted());
                }
            }
        });
        t.start();
        t.interrupt();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    在启动线程后,使用interrupt()方法改变状态,所以第一次为true,后面使用interrupted()清除中断状态变为false

    2.5 等待线程

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

    由于线程的执行顺序是随机调度的,使用join()函数可以使线程的结束顺序有所保证.

    public static int COUNT = 10;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
            System.out.println("a - begin");
            int a = 0;
            while (a <= COUNT) {
                a++;
            }
            System.out.println("a - end");
        });
    
        Thread t2 = new Thread(() -> {
            System.out.println("b - begin");
            int b = 0;
            while (b <= COUNT) {
                b++;
            }
            System.out.println("b - end");
        });
        t1.start();
        t1.join();
        t2.start();
        t2.join();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0TBiRCzI-1659257738993)(C:\Users\a\AppData\Roaming\Typora\typora-user-images\image-20220724224602655.png)]

    结束顺序为先a线程后b线程

    join()的源码:

    从源码中可以看到: join方法的原理就是调用相应线程的wait方法进行等待操作的,例如A线程中调用了B线程的join方法,则相当于在A线程中调用了B线程的wait方法,当B线程执行完(或者到达等待时间)B线程会自动调用自身的notifyAll方法唤醒A线程,从而达到同步的目的。

    2.3 常见静态方法

    1) 获取当前线程引用

    方法说明
    public static Thread currentThread();返回当前线程对象的引用

    注意:

    如果使继承Thread,并且重写run()方法,可以直接在run()方法中直接使用this获取当前对象实例,但是通过Lambda表达式,Runnable接口实现的,其中的this就不是指向当前对象的实例了.

    static class MyThread extends Thread {
        @Override
        public void run() {
            System.out.println("MyThread: " + this + " VS " + Thread.currentThread());
        }
    }
    public static void main(String[] args) {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("Runnable: " + this + " VS " + Thread.currentThread());
            }
        });
        Thread t3 = new MyThread();
        t1.start();
        t3.start();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nx9RHgF0-1659257738994)(C:\Users\a\AppData\Roaming\Typora\typora-user-images\image-20220725141450871.png)]

    2) 休眠当前线程

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

    1. 当前进程调用sleep()进入阻塞队列.
    2. 休眠时间结束,进入就绪队列,准备执行

    注意:如果设置休眠时间为1000ms,则最终休眠时间是大于或者等于这个时间的,因为进入就绪队列后不一定马上就会执行.

    3. 线程的状态

    3.1 线程的六种状态

    状态名称
    NEW初始状态,线程被构建,但是没有调用start()方法
    RUNNABLE运行状态,Java将操作系统中的就绪和运行这两种状态笼统地称作为"运行中"
    BLOCKED阻塞状态,表示线程被阻塞
    WAITING等待状态,表示线程进入等待状态,进入该状态表示当前线程需要等待其他线程做出一些特定动作(通知或中断)
    TIMED_WAITING**超时等待状态,**该状态不同于WAITING,它是可以在指定地时间自行返回的.
    TERMINATED终止状态,表示当前线程已经执行完毕

    举个例子:

    你妈妈让你下午去银行取钱,此时你已经被安排任务,但是还没有执行,此时的状态就是NEW

    下午你去银行,准备取钱,等待排队和正在取钱,就被称为RUNNABLE

    在取钱时,遇到银行卡消磁,需要填表…,这些停止当前任务的所表现出的行为,称为BLOCKED,WAITING,TIMED_WAITING

    最后,你成功取到钱,任务结束,此时的状态就是TERMINATED

    在之前学习的isAlive()就是判断你是否在执行任务,就是除了NEW和TERMINATED 的剩余状态

    3.2 线程状态的转移

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oVSkNWqH-1659257738994)(C:\Users\a\AppData\Roaming\Typora\typora-user-images\image-20220725160235180.png)]

    上图源自《JAVA并发编程的艺术》中对线程状态转移的描述。

    • NEW,RUNNABLE,TERMINATED之间的转化
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            System.out.println("新线程执行中...");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        System.out.println("在start之前获取状态: " + t.getState());
        t.start();
        System.out.println("在start之后获取状态: "+ t.getState());
        t.join();
        System.out.println("任务结束后: " + t.getState());
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    显示结果:

    • WAITING 、 BLOCKED 、 TIMED_WAITING 状态的转换
    static Object locker = new Object();
    
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            synchronized (locker) {
                while (true) {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
        t1.start();
    
        Thread t2 = new Thread(() -> {
            synchronized (locker) {
                System.out.println("Hello");
            }
        });
        t2.start();
        System.out.println("t1状态: "+t1.getState());
        System.out.println("t2状态: "+t2.getState());
    }
    
    • 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

    结果:

    把 Thread.sleep(1000)替换为locker.wait()后,结果为:

    • yield()
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            int a = 0;
            while (true) {
                System.out.println("t1: " + a);
                a++;
                //查看注释前后线程执行次数
                Thread.yield();
            }
        });
        t1.start();
        Thread t2 = new Thread(() -> {
            int b = 0;
            while (true) {
                System.out.println("t2: " + b);
                b++;
            }
        });
        t2.start();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 没有使用yield()时二者执行次数差不多.

    • 使用yield()后,t2执行次数远远大于t1

    • yield()类似于sleep(0),进入阻塞队列0ms,让出CPU资源给其他线程.

    • yield()不会改变线程状态,只会把它放入阻塞队列

    4.多线程带来的风险 - 线程安全

    4.1 为什么要有内存模型

    首先内存模型(Memory Model)**描述了在多核多线程的场景下,各种不同的CPU是如何以一种统一的方式来与内存进行交互的.**下面介绍它与计算机硬件的关系.

    • CPU和缓存一致性

    现在我们知道多核CPU内部存在三级缓存(L1,L2,L3),这些高速缓存是为了解决内存读写速度慢和CPU执行速度快这一互相矛盾而产生的解决方案,于是在拥有多级缓存后,当CPU读取数据时,会从一级缓存开始向下寻找,但随着多线程编程的发展,问题就产生了.下面是对于单线程,多线程对不同核心数CPU的影响:

    1. 单核CPU,单线程

    CPU中的缓存只会被单个线程访问,不会发生访问冲突.

    1. 单核CPU,多线程

    在同一进程下的多个线程,由于"虚拟内存地址"的存在,即使不同的线程访问相同的缓存地址,结果也是不同的,所以不会发生访问冲突

    1. 多核CPU,多线程

    在多个线程访问进程中的某个共享内存时,且这多个线程在不同的CPU核心上运行,则每个核心都会在各自的caehe中保留一份共享内存的缓冲,由于多核是可以并行的,可能会出现多个线程同时写各自的缓存的情况,而各自的cache之间的数据就有可能不同。

    所以每个核心中的缓存数据,关于同一数据的缓存内容有可能不相同,所以会发生信息不同步,即发生缓存一致性问题.

    • 处理器优化和指令重排
    1. 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。

    2. 指令级并行的重排序。现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。

    3. 内存系统的重排序。由于处理器使用缓存和读写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

    所以为了保证共享内存的统一性,正确性(可见性,原子性,有序性),通过建立内存模型来对多线程读写行为操作的规范.

    4.2 观察线程不安全

    观察下面多线程代码(并发执行)和单线程代码(串发执行)的将count累计到10w.

    • 多线程
    class Counter {
        public int count;
        void increase() {
            count++;
        }
    }
    public static Counter counter = new Counter();
    public static void main(String[] args) throws InterruptedException {
        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
    • 单线程
    class Counter {
        public int count;
        void increase() {
            count++;
        }
    }
    public static Counter counter = new Counter();
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            for (int i = 0; i < 100000 ; i++) {
                counter.increase();
            }
        });
        t.start();;
        t.join();
        System.out.println(counter.count);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    最后多次执行(我执行了1000多次)多线程,结果是(5w, 10w]之前的数据,单线程的结果始终是10w.那这是为什么呢?

    首先我们要明确线程安全的概念:如果在单线程下运行的结果与多线程运行的结果是相同的,那么说这个线程是安全的.

    4.3 分析线程不安全的原因

    在分析线程不安全的原因之前,我们先了解Java的内存模型.

    1) Java内存模型 - JMM

    Java内存模型(Java Memory Model),是基于计算机内存模型(定义了共享内存系统中多线程程序读写操作行为的规范),目的是屏蔽掉各种硬件和操作系统的内存访问差异,保证Java程序在各种平台下对内存的访问都能达到一致的并发性,即保证对共享内存的原子性,可见性,有序性.

    各部分的作用如下:

    名称作用
    方法区存储已被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据
    存储实例对象
    虚拟机栈存放程序运行时需要的数据,即栈帧
    本地方法栈为JVM调用的native方法,提供服务
    程序计数器记录当前线程执行到的字节码的行数

    在线程共享区中访问同一对象或者变量时,就会发生不可预期的结果,下面将介绍在多线程的情况下,对"线程共享区"的访问情况:

    如上图所示,线程A和线程B拥有之间的工作内存,当线程进行读写操作时,不能直接对主内存的变量进行操作,首先在主内存拷贝一份变量副本到工作内存区,然后在自己的工作区进行操作,最后把变量副本同步到主内存中.

    注: 不同线程之间的内存是互不可见的.

    2) 原子性

    现在我们知道这个counter.count就是主内存中的变量,那么多个线程就可以对它进行读写操作,那么对于上面的读写操作(count++)涉及到了三个机械指令:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KUpIlDgL-1659257738995)(C:\Users\a\AppData\Roaming\Typora\typora-user-images\image-20220726163454275.png)]

    1. 从主内存中读取数据并拷贝到线程工作内存 - Load
    2. 在线程工作内存中完成数据增加 - Add
    3. 把工作内存中的值同步到主内存中 - Save

    在上述操作中,由于多线程的"随机调度"和"抢占式执行"对上面的三个操作有影响.

    假如现在有两个线程操作主内存,按照正常逻辑这三步是具有原子性的,即如下图所示:

    由于操作系统的随机调度,会出现下面的情况:

    当然,还要很多情况,有些结果是1,有些是2,这就引发了缓存一致性问题,首先先了解什么是原子性:

    • 对于一个完整的操作,要么全部完成且不能被中断,要么就不完成.

    所以对于在多线程中的这三个指令是具有原子性的,但是由于操作系统的随机调度出现了指令中断/插入,原子性被破坏,结果就有可能出错.

    指令重排序为什么会提高效率:

     [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uJ9gOKnX-1659257738995)(C:\Users\a\AppData\Roaming\Typora\typora-user-images\image-20220726212953387.png)]'

    可以理解方案2是我们的代码,方案1为编译器为我们优化后的代码.

    3) 可见性

    可见性:多线程操作共享内存时,执行结果能够及时的同步到共享内存,确保其他线程对此结果及时可见。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-03lFIvYI-1659257738995)(C:\Users\a\AppData\Roaming\Typora\typora-user-images\image-20220726211516393.png)]

    线程A反复的Load(从内存加载数据到寄存器),然后Add,Save(对寄存器进行操作),此时线程B在任意时间读取的数据都是同步的.

    但是JVM,会对代码进行优化,因为Load的速度太慢了,并且每一次读取的数据都是相同的,所以只读取一次,然后反复add,save,由于线程B不知道线程A此时共享内存还在修改,这时候执行Save从主内存中读取的共享变量就是之前的数据,导致线程安全问题

    为什么要分工作内存和主内存?

    首先区分二者的概念:工作内存:CPU中的寄存器和高速缓存; 主内存:真正的内存,在之前我们提到过随着CPU技术的提升,CPU访问缓存速度远远大于访问内存速度,所以可以先把需要的数据从内存中拿到高速缓存区中,这样CPU就可以迅速访问到数据,大大的提高了工作效率,又有一个问题,缓存器的访问速度那么快,为什么不全部都用它呢?一个是贵,另一个是散热问题(随着缓存大小的增加,集成到CPU芯片上的晶体管数量也会快速增加,芯片工作的发热量也就会快速增加)

    所以可见性和原子性就保证缓存一致性

    4) 有序性

    程序的执行是顺序执行,在单线程下,程序的执行都是有序的,但是在多线程下,操作系统,编译器,JMM会对其进行优化操作,达到提高效率的功能,但是指令重排序可以修改代码的执行顺序(逻辑顺序改变),这里就会产生问题.

    举个例子:

    Test t = new Test();
    
    • 1
    1. 为对象分配空间
    2. 在该空间上构造对象Test
    3. 把这个内存的引用赋给t

    在多线程情况下,A线程尝试读取t的引用

    如果按照2,3顺序,当B线程读取到一个非null的t对象时,此时的t就是一个有效对象

    如果按照3,2顺序,即使B线程读取的是非null对象,t也有可能是一个无效对象

    5) 总结

    线程安全的五种原因

    原因解决方案
    操作系统的"随机调度"无能为力
    多线程破坏"原子性"通过加锁操作,Java提供"synchronized"关键字,解决多个线程写的问题
    多个线程同时访问"共享内存"可以通过调整程序的设计来部分规避
    内存可见性没有及时同步数据使用volatile,解决一个线程读,另一个线程写的问题
    指令重排序使用volatile

    下面我们将详细介绍这两个关键字的使用方法

    5. synchronized 关键字

    5.1 概念

    在介绍JMM时谈到"线程共享区",为了管控和协调在多线程之间的共享数据的访问,使用synchronizied关键字可以对类或者对象加锁,由监视锁Monitor保证在同一时刻只有一个线程访问共享资源,如果一个线程需要访问这个锁,需要询问虚拟机,如果申请成功,这个锁就会分配给这个线程,同时这个线程无法获取锁了,如果申请失败,有可能虚拟机等会给它,这个"等会"有可能很快,也有可能很慢,这和操作系统的调度有关

    相关细节:

    1. 同一个线程可以对同一个对象进行多次加锁,每个对象维护着一个记录被锁的计数器,未锁时为0,当一个线程获得锁时,计数器+1,当同一个线程再次获得该对象锁时,计数器+1,当同一个线程释放锁时,计数器-1,当计数器为0时,锁被释放,其他线程就可以获得该锁了
    2. 当有多个线程对同一变量进行访问时,Java提供两种内置方式来使线程同步的访问数据:同步代码块和同步方法
    3. synchronized 翻译为"同步",但是这里指的"互斥",互斥就和谈对象一样,通常情况下,一个人同一时刻只能有一个对象.也就是说同一个锁同一时刻只能被一个线程拥有,其他线程无法访问到锁.
      • 介绍一下"同步"和"异步"
        • 同步:由调用者自己负责获取调用的结果.
        • 异步:调用者不负责获取调用的结果,而是由被调用者把需要的结果主动推送过来

    5.2 synchronized 原理讲解

    在Java中,synchronized有两种使用方式:同步方法和同步代码块,我们在反汇编来查看其差异

    public class Demo3 {
        //同步方法
        public synchronized void test() {
            System.out.println("Hello world");
        }
        //同步代码块
        public void test1() {
            synchronized (Demo3.class) {
                System.out.println("Hello");
            }
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    现在我们可以很清楚的查看JVM在处理的差异.对于同步方法,JVM采用"ACC_SYNCHRONIZED"标记符来顺序同步,对于同步代码块,JVM采用monitorenter,monitorexit这两个指令来实现.在JVM规范中有相关描述.

    同步方法:

    The Java® Virtual Machine Specification中有对同步方法的介绍

    Method-level synchronization is performed implicitly, as part of method invocation and return. A synchronized method is distinguished in the run-time constant pool’s method_info structure by the ACC_SYNCHRONIZED flag, which is checked by the method invocation instructions. When invoking a method for which ACC_SYNCHRONIZED is set, the executing thread enters a monitor, invokes the method itself, and exits the monitor whether the method invocation completes normally or abruptly. During the time the executing thread owns the monitor, no other thread may enter it. If an exception is thrown during invocation of the synchronized method and the synchronized method does not handle the exception, the monitor for the method is automatically exited before the exception is rethrown out of the synchronized method.

    表达的含义是 :方法级的同步是隐式的,在运行时常量池的method_info结构中,通过ACC_SYNCHRONIZED标志来区分同步方法,该标志由方法调用指令进行检查。当调用设置了ACC_SYNCHRONIZED的方法时,执行线程进入监视器,调用该方法本身,然后退出监视器,无论方法调用是正常完成还是突然完成。在执行线程拥有监视器的时间内,其他线程不能进入。如果在调用同步方法期间抛出异常,而同步方法不处理该异常,则在同步方法重新抛出异常之前自动退出该方法的监视器。

    同步代码块:

    The Java® Virtual Machine Specification 中同样有对monitorenter和monitorexit的介绍

    • monitorenter

    Chapter 6. The Java Virtual Machine Instruction Set (oracle.com)

    Operation

    Enter monitor for object

    Description

    Each object is associated with a monitor. A monitor is locked if and only if it has an owner. The thread that executes monitorenter attempts to gain ownership of the monitor associated with objectref, as follows:

    • If the entry count of the monitor associated with objectref is zero, the thread enters the monitor and sets its entry count to one. The thread is then the owner of the monitor.
    • If the thread already owns the monitor associated with objectref, it reenters the monitor, incrementing its entry count.
    • If another thread already owns the monitor associated with objectref, the thread blocks until the monitor’s entry count is zero, then tries again to gain ownership.

    翻译:

    每个对象都与一个监视器相关联。当且仅当监视器有所有者时,它才会被锁定。执行monitorenter的线程试图获得与objectref关联的监视器的所有权,如下所示:

    • 如果与objectref关联的监视器的条目计数为零,线程进入监视器并将其条目计数设置为1。然后线程是监视器的所有者。
    • 如果线程已经拥有与objectref关联的监视器,它将重新进入监视器,增加监视器的条目计数。
    • 如果另一个线程已经拥有与objectref关联的监视器,该线程将一直阻塞,直到监视器的条目计数为零,然后再次尝试获得所有权。

    monitorexit

    Chapter 6. The Java Virtual Machine Instruction Set (oracle.com)

    Operation

    Exit monitor for object

    Description

    The thread that executes monitorexit must be the owner of the monitor associated with the instance referenced by objectref.

    The thread decrements the entry count of the monitor associated with objectref. If as a result the value of the entry count is zero, the thread exits the monitor and is no longer its owner. Other threads that are blocking to enter the monitor are allowed to attempt to do so.

    翻译:

    执行monitorexit的线程必须是与objectref引用的实例相关联的监视器的所有者。

    线程减少与objectref关联的监视器的条目计数。如果条目计数的值为零,则线程退出监视器并不再是它的所有者。允许阻塞进入监视器的其他线程尝试这样做。

    注:objectref为所有对象引用的基类.你可以理解为当前对象

    表达的含义是:我们可以理解monitorenter为加锁(计数器+1),monitorexit为释放锁(计数器-1),每个对象都有一个监视器与之关联,在监视器中维护这一个计数器,记录着当前对象被当前线程上锁次数,计数器为0,有两层含义,一是当前对象未被锁定,二是其他线程可以去锁定该线程,

    总结:

    同步方法通过使用ACC_SYNCHRONIZED实现对该方法的加锁,线程需要使用被ACC_SYNCHRONIZED标记的方法时需要先获得该锁

    同步代码块通过使用monitorenter来获得锁,然后就可以执行方法,最后执行到monitorexit时释放锁.

    5.3 Java对象的对象头

    上面已经了解synchronized的原理,那么关于锁的信息存放在哪里呢?

    首先先了解Java对象的组成

    1. 对象头
      • Mark Word :用于存储对象自身的运行时数据, 如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等
      • Klass Pointer :对象指向它的类的元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例
    2. 实例数据
      • 存放着实例对象的一些字段属性内容。
    3. 对齐填充
      • 由于HotSpot虚拟机的自动内存管理系统要求对象的起始地址必须是8字节的整数倍,也就是对象的大小必须是8字节的整数倍

    所以对象头中存放了当前线程对应的锁信息,对象在JVM中的结构是什么样子的呢?这个建议大家去阅读HotSpot虚拟机的源码了.

    我们主要了解一下Mark Word的内部信息

    Mark Word的32位比特位在不同对象状态下被赋予不同的含义,下图介绍了在32位虚拟机上,不同对象状态的Mark Word上各个比特位区间的含义:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TqXCefmW-1659257738996)(http://www.hollischuang.com/wp-content/uploads/2018/01/ObjectHead-1024x329.png)]

    在HotSpot中的markOop.hpp类中的枚举定义中有GC分代年龄、锁状态标记、哈希码、epoch等信息。

    enum { age_bits                = 4,
          lock_bits                = 2,
          biased_lock_bits         = 1,
          max_hash_bits            = BitsPerWord - age_bits - lock_bits - biased_lock_bits,
          hash_bits                = max_hash_bits > 31 ? 31 : max_hash_bits,
          cms_bits                 = LP64_ONLY(1) NOT_LP64(0),
          epoch_bits               = 2
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    构建锁标志位的不同,对象的状态分为五种:无锁态、轻量级锁、重量级锁、GC标记和偏向锁,关于这些我们将会在进阶中讲解

    5.4 Monitor 工作机制

    上图分析:

    image

    分析:当前的monitor监视方法AB,当t1线程进入时,Monitor监视的方法没有被上锁,所以当t1进入后方法被上锁,当t2进程想要访问方法A,t3进程想要访问方法B都无法访问,只有t1释放锁喉,t2才能正常访问.

    存在多个Monitor 时,多个Monitor 是互不影响的,可以使用方法名.class或者this来获得当前对象.

    image

    总结:Monitor 保证在同一时刻对同一对象只有一个进程访问.

    5.5 作用及特点

    1. 保证方法和代码块的原子性

    由于synchronized 保证方法和代码块的内部资源的"互斥"访问,即在同一时间,由同一个监视器监视的代码,只能由一个线程访问

    1. 保证内存可见性

    当一个进程获取到锁后,会先将共享内存中的数据拷贝到自己的工作内存区,随后执行代码,然后把更改后的共享数据重新刷新当共享内存区,最后释放锁.这一个整体的操作,就保证了数据的及时同步,从而保证了内存可见性

    1. 可重入锁

    前面在Synchronized 原理中讲解到,对于同一个线程,是一直可以上锁的,也就是可重入锁

    5.5 使用

    Synchronized 关键字是对对象的对象头进行标记,来实现进程对代码块操作的原子性和可见性,下面介绍其使用方法.

    大体上分为同步方法和同步代码块,每个代码块又可以分为静态代码块和普通代码块,这是因为对于静态方法获取其对象使用的是 类名.class,而普通代码块内可以直接使用this代表当前对象.

    • 静态代码块
    class Synchronized{
        //修饰静态同步方法
        public static synchronized void method1() {
            //code...
        }
        //修饰静态同步代码块
        public static void method2() {
            //code..
            synchronized (Synchronized.class) {
    
            }
            //code..
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 普通代码块
    class Synchronized{
    	//修饰普通同步方法
        public synchronized void method3() {
            //code..
        }
        //修饰普通同步代码块
        public void method4() {
            //code..
            synchronized(this) {
                //code..
            }
            //code..
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    代码示例:针对不同对象的上锁

    public class Demo4 {
        public static Object locker1 = new Object();
        public static Object locker2 = new Object();
    
        public static void main(String[] args) {
            Thread t1 = new Thread(() -> {
                synchronized (locker1) {
                    System.out.println("t1 - start");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("t1 - finish");
                }
            });
            t1.start();
    
            Thread t2 = new Thread(() -> {
                synchronized (locker1) {
                    System.out.println("t2 - start");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("t2 - finish");
                }
            });
            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

    结果1:

    把t2用locker2上锁后:结果2

    对于结果1由于t1先上锁,只有等到t1释放锁后,t2才能开始获得锁,对于结果2,t1和t2线程被不同的对象上锁,二者的执行顺序是随机调度的,结果未知.

    6. volatile 关键字

    我们先看个例子:

    static class Flag{
        public int flag = 0;
    }
    public static void main(String[] args) {
        Flag flag = new Flag();
    
        Thread t1 = new Thread(() -> {
            System.out.println("t1 - start");
            //循环只是判断当前flag是否为0
            while (flag.flag == 0) {
            }
            System.out.println("t1 - finish");
        });
        t1.start();
    
        Thread t2 = new Thread(() -> {
            Scanner scanner = new Scanner(System.in);
            //修改flag的值
            System.out.println("请输入一个整数:");
            flag.flag = scanner.nextInt();
        });
        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

    按照我们正常代码逻辑思维来看,当我们输入一个非0的数字时,t1线程就应该结束,但是结果却不是我们所预料的一样.

    原因是编译器的优化,编译器觉得我们反复的读取内存中flag到工作内存中,这个时间太长了(原因读内存速度太慢了),所以编译器觉得每一次读取的值都是一样的,速度又慢,所以干脆不读了,导致读取到非0值也没有效果

    那如何不让编译器优化呢?

    1. 使用sleep()

    由于while循环速度太快了,导致编译器去优化,所以可以让它循环速度减慢,使用sleep(),就可以做到

    static class Flag{
        public int flag = 0;
    }
    public static void main(String[] args) {
        Flag flag = new Flag();
    
        Thread t1 = new Thread(() -> {
            System.out.println("t1 - start");
            //循环只是判断当前flag是否为0
            while (flag.flag == 0) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("t1 - finish");
        });
        t1.start();
    
        Thread t2 = new Thread(() -> {
            Scanner scanner = new Scanner(System.in);
            //修改flag的值
            System.out.println("请输入一个整数:");
            flag.flag = scanner.nextInt();
        });
        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

    输出结果:

    1. 使用volatile

    使用volatile 就显式的禁止编译器的优化,是对这个变量加上了"内存屏障"(特殊的二级制指令),导致VM在读取该变量时,每一次都要重新从内存中读取该变量.但是带来的缺点也是明显的:由于频繁读取内存,导致效率降低.

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

    这是因为volatile能够保证:

    每次读入前:必须从主内存中刷新最新的值

    每次写入后:必须马上从工作内存中同步到主内存中

    总结:

    1. 保证内存可见性,所有的线程都可以看见被volatile修饰变量的最新状态
    2. 防止指令重排序:volatile关键字通过“内存屏障”来防止指令被重排序。编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序

    对volatile 修饰的变量变得可见,解决了一个线程读,另一个线程写发生的内存可见性问题,但是不保证原子性

    7. wait 和 notify

    我们知道线程之间时抢占式执行的,多个线程之间执行的先后顺序是不确定的,但是我们又需要使线程的执行顺序有序,这时候就可以利用wait()方法和notify()方法.

    1) wait()方法

    方法名解释
    public final void wait() throws InterruptedException使当前线程进入等待状态(WAITING)
    public final native void wait(long timeout) throws InterruptedException在规定时间结束后,自动唤醒
    public final void wait(long timeout, int nanos) throws InterruptedException更高精度的描述等待结束时间(前面的毫秒加上后面的纳秒)

    wait()方法的执行过程

    1.释放锁

    • 当前线程必须拥有这个对象的监视器,然后释放该监视器的所有权并等待,即释放锁.

    2.等待通知

    • 等待当前对象调用notify()来唤醒该线程

    3.得到通知,并且尝试重新获得锁

    • 通知到达,随即被唤醒,

    注:wait()要和synchronized搭配使用,并且synchronized加锁的对象,调用wait()方法的对象,调用notify()的对象是同一个.否则会抛出异常

    2) notify()方法

    方法名解释
    public final native void notify()随机唤醒一个获得过对象锁的线程并且当前处于WAITING状态的线程
    public final native void notifyAll()唤醒所有处于WAITING状态的
    1. notifyAll 和 notify 也要在同步方法或同步代码块中使用,来向那些等待该对象的对象锁的其他线程,然后通知它,并使其重新获取当前对象的锁.

    2. 使用notifyAll 和 notify 后线程不会立即释放该对象锁,要等待调用方执行完代码执行完毕,才会释放对象锁.

    3. 使用notify 是唤醒一个(随机从waiting的线程中唤醒),notifyAll 是全部唤醒(再由这些线程去竞争)

    3) 使用方法及总结

    使用wait方法和notify方法可以使两个毫无关联的线程联系在一起,也是在面试题中常提到的线程之间如何通信.例:

    static Object locker = new Object();
    
    public static void main(String[] args) {
        Thread waitTask = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (locker) {
                    try {
                        System.out.println("wait 开始");
                        locker.wait();
                        System.out.println("wait 结束");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
        waitTask.start();
    
        Thread notifyTask = new Thread(new Runnable() {
            @Override
            public void run() {
                //先执行其他操作,来延迟通知
                Scanner scanner = new Scanner(System.in);
                System.out.println("输入任意内容开始通知:");
                // next 会阻塞线程
                scanner.next();
                synchronized (locker) {
                    System.out.println("notify 开始");
                    locker.notify();
                    System.out.println("notify 结束");
                }
            }
        });
        notifyTask.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

    结果1:

    如果在notifyTask中使用另一个对象锁,结果会是怎样?

    结果2:

    分析:对于结果1,当开始线程waitTask使用wait方法后,释放锁,notifyTask线程获得锁,执行完同步代码块后,线程waitTask才被唤醒,才会执行后面的代码.对于结果2,因为是不同的对象锁,使用线程waitTask一直处于WIATING状态,然后执行notifyTask线程后,唤醒的是locker1的线程,没有,则对进程waitTask没有影响.

    总结:

    1. wait和notify针对是同一对象时,才有作用
    2. 只有notify代码块内容执行完成后,才会唤醒另一个线程

    4) wait和sleep的异同点

    不同点:

    1. 所属类不同

    wait 方法属于 Object 类的方法,而 sleep 属于 Thread 类的方法


    1. 使用方法不同

    wait方法必须搭配synchronized 一起使用,不然会抛出IllegalMonitorStateException异常

    sleep方法可以单独使用

    1. 使用后线程状态不同

    使用wait方法后,线程会进入WAITING 无时限等待状态

    使用sleep方法后,线程会进入TIMED_WAITING 有时限等待状态

    1. 释放锁资源不同

    wait方法使用后该线程会释放锁

    sleep方法使用后线程不释放锁

    1. 唤醒方式不同

    wait方法可以使用不带参数的方法代表一直等待通知,如果使用带参数的,代表到了规定时间会自动唤醒.

    sleep方法一定要指明休眠时间,到了规定时间后从阻塞队列进入就绪队列

    相同点:

    都可以使线程进入阻塞队列

    8. 多线程案例

    8.1 设计模式

    1) 什么是设计模式

    设计模式是程序员在面对软件工程设计问题中总结出的"经验",是解决某类问题的通用方案.设计模式的本质是提高软件的通用性,可维护性,拓展性,并降低代码的复杂程度,既然它是一种设计模式,就不会限于某种语言.

    下图解释了23种模式:

    下面主要讲解创造性模式中的单例模式(也是校招中常考点)

    2) 单例模式

    单例模式是指采用一种设计方法使整个项目或者工程中只存在一个实例,并且不能再次创建,只提供一个获取该实例的方法.

    比如在JDBC中的DataSource实例只需要一个

    单例模式创建实例的时机,分为"懒汉"模式和"饿汉"模式.

    1.饿汉模式

    在类加载的同时,创建实例对象

    class Singleton {
        //直接创建实列对象
        private static Singleton instance = new Singleton();
        //构造方法私有化,无法通过构造方法创建实例
        private Singleton() {
    
        }
        //提供获取实例的接口
        public static Singleton getInstance() {
            return instance;
        }
    }
    
    public class Demo {
        public static void main(String[] args) {
            Singleton instance1 = Singleton.getInstance();
            Singleton instance2 = Singleton.getInstance();
            //利用每个对象的哈希值不同来判断创建的实例对象是否相同
            System.out.println(instance1.hashCode() == instance2.hashCode());
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    总结: 饿汉模式是天然线程安全的,因为getInstance只涉及到线程的读.

    2.懒汉模式 - 单线程版

    在使用使用实例对象时,才去创建

    class SingletonLazy{
        //不创建实例对象
        private static SingletonLazy instance = null;
        private SingletonLazy(){
    
        };
        public static SingletonLazy getInstance() {
            if (instance == null) {
                instance = new SingletonLazy();
            }
            return instance;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    3.懒汉模式 - 多线程版

    在多线程的情况下,获取实例对象的方法中的判断条件存在竟态条件,导致instance被多次赋值,使用户得到不同的实例.例如:存在AB线程,在if判断时A线程先判断为空,但是B线程已经进入并且创建实例对象,随后A又创建了实例对象,这时候就得到不同的实例

    解决方法:使用synchronized,使判断,读,写,创建实例对象成为一个原子操作,保证了线程安全

    class SingletonLazy{
        //不创建实例对象
        private static SingletonLazy instance = null;
        private SingletonLazy(){
    
        };
        public static  SingletonLazy getInstance() {
            synchronized (SingletonLazy.class) {
                if (instance == null) {
                    instance = new SingletonLazy();
                }
            }
            return instance;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    缺点: 使用synchronized使效率降低,并且每一次都要调用getInstance()方法获取对象时都要进行同步,虽然这个对象只实例化只执行一次.


    优化1:

    使用DCL(Double Check Lock,双重检查锁)机制,使得大部分请求都不会进入阻塞代码块.

    class SingletonLazy{
        //不创建实例对象
        private static SingletonLazy instance = null;
        private SingletonLazy(){
    
        };
        public static  SingletonLazy getInstance() {
            if (instance == null) {
                synchronized (SingletonLazy.class) {
                    if (instance == null) {
                        instance = new SingletonLazy();
                    }
                }
            }
            return instance;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    外层判断当前实例是否已经初始化,如果未初始化就尝试加锁,如果已经实例化就返回当前实例对象,这样就大大减少线程去竞争锁,降低了开销

    内层代码块: 进入这里的是竞争成功的线程,如果有线程已经完成创建实例的操作,那么后面拿到锁的线程经过if判断,就不会去重新创建了,

    优化2:

    上面的代码还存在一个问题 - 当instance == null 仍可能指向一个被**“部分初始化的实例对象”**

    问题在于这句赋值语句:

    instance = new SingletonLazy();
    
    • 1

    这并不是一个原子操作,而是分为下面的JVM指令:

    // 1. 为对象分配内存空间
    memory = allocate();
    // 2.初始化对象
    initInstance(memory);
    // 3.使instance指向刚分配的内存空间
    instance = memory;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    由于操作2依赖操作1,但是操作3并不依赖操作2,所以JVM会优化代码,进行指令重排序.

    // 1. 为对象分配内存空间
    memory = allocate();
    // 3.使instance指向刚分配的内存空间(此时的对象并没有初始化)
    instance = memory;
    // 2.初始化对象
    initInstance(memory);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    所以交换操作23次序后,最终得到的是一个未初始化内存的对象(关于对象的信息未加载到分配的内存中),如果有线程去调用getInstance方法,由于instance指向了内存空间不为null,所以if判断为false,最后就返回了一个"未完全初始化的实例对象".

    为了解决这个问题,只需要用volatile修饰instance

    private static volatile SingletonLazy instance = null;
    
    • 1

    8.2 阻塞式队列

    1) 概念

    在数据结构中的队列是"先进先出",阻塞式队列是线程安全的数据结构,具有以下特征:

    队列满的时候, 继续入队列就会阻塞, 直到有其他线程从队列中取走元素.
    队列空的时候, 继续出队列也会阻塞, 直到有其他线程往队列中插入元素 .

    了解其特征后大家应该知道为什么它线程安全了吧.阻塞式队列的一个典型应用场景就是"生产者消费者模型".

    2) 生产者消费者模型

    生产者消费者模型是提供一个容器来解决生产者和消费者之间强耦合问题

    生产者消费者之间并不直接联系,当生产者产出数据时,把它交给阻塞队列,让它交给消费者,消费者需要数据时,不是去找生产者,而是去阻塞队列中获取数据.

    生产者消费者模型的优点:

    1. 降低代码块之间的"耦合性"

    采用方式1,A和B,C之间的耦合性强,在开发A时就要考虑B,C是如何接受数据的,开发B,C时要考虑A是如何发送数据的.在某些情况下,A出现异常时可能会把B,C带走. 而采用方式2,A,B,C都只需要知道如何和阻塞队列之间如何交互,甚至都不知道对方的存在,所以当A,B,C出现异常时都不会对其产生任何影响.

    1. 阻塞队列类似于一个缓冲区,平衡消费者和生产者之间的数据流量.

    在购物平台上的"秒杀"活动,在同一时刻服务器会收到大量的"支付请求",如果服务器直接去处理这些支付,那么工作量是十分巨大的(支付请求的处理十分复杂),这时候就可以把这些请求放入阻塞队列中,然后由消费者线程来处理这些请求.

    这样可以有效"削峰",大大的提高的系统的抗风险能力!

    3) 标准库中的阻塞队列

    Java中的BlockingQueue接口的实现类LinkedBlockingQueue实现了这个接口,put 方法用于阻塞式的入队列, take 用于阻塞式的出队列

    消费者生产者模型:

    public static void main(String[] args) {
        BlockingQueue<Integer> queue = new LinkedBlockingQueue<>();
    
        Thread customer = new Thread(() -> {
            while (true) {
                try {
                    int value = queue.take();
                    System.out.println("消费元素: " + value);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        customer.start();
    
        Thread producer = new Thread(() -> {
            Integer n = 0;
            while (true) {
                try {
                    System.out.println("生产元素: " + n);
                    queue.put(n);
                    n++;
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        producer.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

    4) 阻塞队列实现

    class MyBlockingQueue{
        //默认队列容量
        private int[] items = new int[1000];
        //队头下标
        private int head = 0;
        //队尾下标
        private int tail = 0;
        //队列中的元素
        volatile private int size = 0;
    
        public void put(int value) throws InterruptedException {
            synchronized (this) {
                //因为针对的是多线程,会有多次读写操作
                while (this.size == items.length) {
                    //当队列满时,停止入队
                    this.wait();
                }
                items[tail] = value;
                tail++;
                // 如果容量满了,利用循环队列的特性
                if (tail == items.length) {
                    tail = 0;
                }
                //另一个版本
                tail = tail % items.length;
                size++;
                this.notify();
            }
        }
    
        public Integer take() throws InterruptedException {
            Integer res = 0;
            synchronized (this) {
                while (size == 0) {
                    //队列为空,进入等待
                    this.wait();
                }
                res = items[head];
                head++;
                if (head == items.length) {
                    head = 0;
                }
                size--;
                this.notify();
            }
            return res;
        }
    
    }
    
    • 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

    8.3 定时器

    1) 概念

    定时器在软件开发中的作用是:到达规定指定时间后执行某段代码.

    有三种表现形式:

    • 按固定周期定时执行
    • 延迟一定时间后执行
    • 指定某个时刻执行

    JDK(1.8)提供了三种定时器的实现方法:

    • Timer
    • ScheduledThreadPoolExecutor 线程池(后面讲)
    • DelayQueue 延迟队列

    2) 如何使用

    1. Timer

    public static void main(String[] args) throws InterruptedException {
        Timer timer = new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("这是一个要执行的任务");
            }
        },3000);
        while (true) {
            System.out.println("main");
            Thread.sleep(1000);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • TimerTask是实现Runnable接口的抽象类
    • schedule 根据传入参数不同,表现形式也不同
    1. 延迟一定时间后执行
    public void schedule(TimerTask task, long delay) {
        if (delay < 0)
            throw new IllegalArgumentException("Negative delay.");
        sched(task, System.currentTimeMillis()+delay, 0);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    1. 按固定周期定时执行
    public void schedule(TimerTask task, long delay, long period) {
        if (delay < 0)
            throw new IllegalArgumentException("Negative delay.");
        if (period <= 0)
            throw new IllegalArgumentException("Non-positive period.");
        sched(task, System.currentTimeMillis()+delay, -period);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    2. DelayQueue

    static class Task implements Delayed {
            long time;
            public Task(long time) {
                this.time = time;
            }
            public long getTime() {
                return time;
            }
    
            /**
             * 获取延迟结束时间 - 单位/ms
             * @param unit
             * @return
             */
            @Override
            public long getDelay(TimeUnit unit) {
                return unit.convert(time - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
            }
    
            @Override
            public int compareTo(Delayed o) {
                return Long.compare(this.getDelay(TimeUnit.MILLISECONDS), o.getDelay(TimeUnit.MILLISECONDS));
            }
        }
        public static void main(String[] args) throws InterruptedException {
            BlockingQueue<Task> delayQueue = new DelayQueue<>();
            long now = System.currentTimeMillis();
            delayQueue.put(new Task(now + 1000));
            delayQueue.put(new Task(now + 2000));
            delayQueue.put(new Task(now + 3000));
            for (int i = 0; i < 3; i++) {
                System.out.println(new Date(delayQueue.take().getTime()));
            }
        }
    
    • 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
    • BlockingQueue队列实现了DelayQueue接口
    • BlockingQueue中的每一个对象都需要实现Delayed接口,并且重写compareTo方法和getDelay方法

    3) 实现定时器

    组成部分:

    1. 任务类
    //描述任务属性,实现比较器,因为涉及到时间比较
    class MyTask implements Comparable<MyTask> {
        // 任务需要完成什么?
        private Runnable command;
        // 任务开始执行时间
        private long time;
    
        //构造方法
        public MyTask(Runnable command, long after) {
            this.command = command;
            this.time = System.currentTimeMillis() + after;
        }
    
        //执行任务的run方法
        public void run() {
            command.run();
        }
    
        public long getTime() {
            return this.time;
        }
    
        @Override
        public int compareTo(MyTask o) {
            return (int)(this.time - o.time);
        }
    }
    
    • 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
    1. 定时器类:
    //定时器的构建
    class MyTimer{
        //用于阻塞等待的锁对象
        //因为在队列不为空时,执行顺序: 拿到任务 -> 比较时间 -> 进行判断,时间未到重新插入
        //然后重新循环,在多线程情况下会产生大量无用的循环,浪费资源,所以可以使线程等待
        private Object locker = new Object();
    
        //优先级队列保存任务
        private PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();
    
        /**
         *
         * @param command - 任务
         * @param after - 延迟时间
         */
        public void schedule(Runnable command, long after) {
            MyTask myTask = new MyTask(command, after);
            synchronized (locker) {
                queue.put(myTask);
                //注意唤醒时机,在有任务插入时唤醒
                locker.notify();
            }
        }
    
        public MyTimer() {
            //启动线程
            Thread t = new Thread(() -> {
                //在循环中尝试从队列中获取元素
                //取出队头元素后判断时间是否符合
                while (true) {
                    try {
                        synchronized (locker) {
                            //队列为空要等待新任务
                            if (queue.isEmpty()) {
                                locker.wait();
                            }
                            MyTask myTask = queue.take();
                            long curTime = System.currentTimeMillis();
                            //这里拿到的是优先级最高的任务
                            //如果没有达到当前时间,则等待,等待时间 -> 肯定那个保证到指定时间后执行
                            if (myTask.getTime() > curTime) {
                                queue.put(myTask);
                                locker.wait(myTask.getTime() - curTime);
                            } else {
                                myTask.run();
                            }
                        }
                    }catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
            t.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
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55

    测试案例:

    public static void main(String[] args) {
        MyTimer myTimer = new MyTimer();
        myTimer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("3333");
            }
        }, 6000);
        myTimer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("2222");
            }
        }, 4000);
        myTimer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("1111");
            }
        }, 2000);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    8.4 线程池

    除了线程池,我们之前还学习过字符串常量池,数据库连接池,那线程池的作用是什么呢?

    最开始学习的进程实现了"并发编程",但是进程的创建和销毁效率太低,所以出现更加轻量的线程,线程共用进程中的同一份资源,在一定程度上减少创建销毁的时间空间消耗,但是在频繁的创建销毁场景下,还是有点吃不消,所以线程池应运而生,把创建好的线程放入线程池中,需要使用时直接去池中取,使用完毕后就归还给线程池,解决了上述问题.

    为什么把线程放入线程池后,从池中取线程比通过操作系统创建线程快呢?

    这是因为从线程池中获取线程是纯用户态操作,而通过系统来创建涉及到内核态操作,而用户态操作比内核态操作更加高效.

    1) 标准库中的线程池

    public static void main(String[] args) {
        ExecutorService pool = Executors.newFixedThreadPool(10);
    
        for (int i = 0; i < 10; i++) {
            pool.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("Hello");
                }
            });
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    这里通过调用Executors的静态方法创建实例对象的方法称为"工厂方法",对应的设计模式,叫做"工厂模式".

    在通常情况下,创建对象的方式是使用构造方法,但是里面的构造方法也有许多限制,比如构造方法的名字和类名相同,要实现不同版本的构造就需要重载,并且重载需要参数类型及其个数不同,但是有时候需要相同参数类型和个数相同的构造方法,这种时候就可以使用静态方法,只需要其方法名不同来区分不同的方法.

    常见构造方法:

    构造方法解释
    newFixedThreadPool(int nThreads)创建固定线程数的线程池
    newCachedThreadPool()创建线程数目动态增长的线程池
    newSingleThreadExecutor()创建只包含单个线程的线程池.
    newScheduledThreadPool(int corePoolSize, ThreadFactory threadFactory)设定 延迟时间后执行命令,或者定期执行命令. 是进阶版的 Timer.

    2) 实现线程池

    class MyThreadPool{
        //任务队列 = 保存线程池中的任务
        private BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();
    
        //向线程池中提交任务
        public void submit(Runnable runnable) {
            try {
                queue.put(runnable);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    
        public MyThreadPool(int n) {
            //添加线程
            for (int i = 0; i < n; i++) {
                Thread t = new Thread(() -> {
                    //如果队列已满.则会被阻塞
                    while (!Thread.currentThread().isInterrupted()) {
                        Runnable runnable = null;
                        try {
                            runnable = queue.take();
                            runnable.run();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                            break;
                        }
                    }
                });
                t.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

    ) 标准库中的线程池

    public static void main(String[] args) {
        ExecutorService pool = Executors.newFixedThreadPool(10);
    
        for (int i = 0; i < 10; i++) {
            pool.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("Hello");
                }
            });
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    这里通过调用Executors的静态方法创建实例对象的方法称为"工厂方法",对应的设计模式,叫做"工厂模式".

    在通常情况下,创建对象的方式是使用构造方法,但是里面的构造方法也有许多限制,比如构造方法的名字和类名相同,要实现不同版本的构造就需要重载,并且重载需要参数类型及其个数不同,但是有时候需要相同参数类型和个数相同的构造方法,这种时候就可以使用静态方法,只需要其方法名不同来区分不同的方法.

    常见构造方法:

    构造方法解释
    newFixedThreadPool(int nThreads)创建固定线程数的线程池
    newCachedThreadPool()创建线程数目动态增长的线程池
    newSingleThreadExecutor()创建只包含单个线程的线程池.
    newScheduledThreadPool(int corePoolSize, ThreadFactory threadFactory)设定 延迟时间后执行命令,或者定期执行命令. 是进阶版的 Timer.

    2) 实现线程池

    class MyThreadPool{
        //任务队列 = 保存线程池中的任务
        private BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();
    
        //向线程池中提交任务
        public void submit(Runnable runnable) {
            try {
                queue.put(runnable);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    
        public MyThreadPool(int n) {
            //添加线程
            for (int i = 0; i < n; i++) {
                Thread t = new Thread(() -> {
                    //如果队列已满.则会被阻塞
                    while (!Thread.currentThread().isInterrupted()) {
                        Runnable runnable = null;
                        try {
                            runnable = queue.take();
                            runnable.run();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                            break;
                        }
                    }
                });
                t.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
  • 相关阅读:
    Jmeter接口测试
    【Java8新特性】- Stream流
    唐山海德教育成人高考
    SpringBoot+Vue项目在线学习网站
    mqant启动流程
    前后端分离计算机毕设项目之基于Java+springboot+vue的智能停车计费系统设计与实现(内含源码+文档+教程)
    GUI编程--PyQt5--QAbstractButton
    Matlab如何批量读取txt数据?科研效率UpUp第1期
    「Python条件结构」实现超市付款功能
    ElasticSearch (ES)学习之路(二)Win10安装ES,可视化界面,Kibanna
  • 原文地址:https://blog.csdn.net/weixin_61543874/article/details/126087590