Bootstrap

JAVA EE - 多线程初阶(2w字)

目录

前言:

1.线程:

        与进程的关系:

2.多线程出现的问题

        ①. 当一个线程崩溃的时候,可能会导致其他也崩溃~~

        ②.增加线程数量,不会一直提高速度

        ③.线程安全问题

3.Thread类

4.start与run

5.java中创建线程的几种方式

6.Thread类中常见的方法及其属性

        6.1 构造方法     

          6.2 属性

               前台线程/后台线程:

                isAlive() 

        6.3 线程终止 

         6.4 线程等待

        6.5  线程休眠                

7.线程状态

        多线程的意义 :

 8.线程安全 

         8.1 线程安全出现的原因:

                1> 根本原因:抢占性执行

                2> 代码结构:多个线程修改了同一个变量

                3> 原子性:线程修改的操作对象如果是非原子性,是危险的

                4> 内存可见性问题

                5> 指令重排序:编译器优化bug

         8.2 synchronized用法

         8.3 synchronized 关键字-监视器锁monitor lock

                 8.3.1 三大特性

                        ​​​​​1.互斥

                ​​​​​​​        ​​​​​​​   2.刷新内存

                ​​​​​​​        ​​​​​​​        3.可重入

        死锁的三种常见情况:

                 1.一个线程 一把锁 连续加锁俩次

                 2.俩个线程 俩个锁,俩个线程先各自对自己“想要的”对象进行加锁,还想获取对方对象的锁

                  3. 多个线程,多把锁

        8.3.2 死锁的必要条件

1.互斥

2.不可抢占

3.请求和保持

4.循环等待

        8.3.3 解决死锁

9. 线程安全类

10.volatile关键字

 1.volatile可以保证内存可见性(问题):

2.volatile不能实现原子性

11. wait / notify

        11.2 wait/notify

wait(带参数)与sleep的比较

11.3 notifyALL

12. 设计模式

        12.1 单例模式

                1.饿汉模式

                 ​​​​​​​2.懒汉模式

        12.2 单例模式的线程安全问题

13. 阻塞式队列

13.2 生产者消费者模型

13.2.2 生产者消费者模型的意义

        1.解耦合

        2.削峰填谷

13.3 实现一个阻塞式队列

 14. 定时器

 14.2 实现一个定时器

14. 线程池

14.2 常见的线程池创建方法

14.3 ThreadPoolExecutor 构造方法

14.2.2 标准库里的四种拒绝策略

14.4 实现一个线程池


前言:

        上一个文章我们了解到 多进程编程已经可以解决了并发编程问题了,已经可以利用起来cpu多核资源了,但是进程太重了,其消耗资源重(消耗资源多,速度慢)因此,线程应运而生,线程就是进程的轻量化,(为啥轻呢?把申请资源/释放资源的操作给省下了)

        总而言之,线程会复用资源

1.线程:

        与进程的关系:

        进程包含线程,一个进程可以包含一个线程,也可以包含多个线程(不能没有)~~ziyu

        因此只有第一个线程启动的时候费劲,后续的就省事了

同一个进程里面的多个线程之间公用的是进程的同一份资源(内存和文件描述符)

内存:new 的对象都能用

文件描述符:线程1打开的文件,其他线程也能使用

因此,操作系统调度的时候,是以线程为单位进行调度的

如果每个进程有多个线程,每个线程都是独立在CPU上调度的,每个线程都有自己的执行逻辑(执行流)

总结:操作系统调度的时候,其实不关心进程,只关心线程了~~

一个线程也是通过一个PCB来描述的,一个进程里面可能对应一个PCB,也可能是多个 

因此上篇文章介绍的PCB里的状态,上下文,优先级,记账信息,都是线程有自己的,各种记录自己的,但是同一个进程的PCB之间的pid是一样的~~

包括内存,文件描述符表也是一样的~~

2.多线程出现的问题

        ①. 当一个线程崩溃的时候,可能会导致其他也崩溃~~

如果一个线程抛异常,如果处理不好,就很可能把整个进程都给带走,其他线程也就挂了

        ②.增加线程数量,不会一直提高速度

 线程太多,核心数目有限,不少的开销会浪费在线程调度上了(关键在于CPU的核心数量)

        ③.线程安全问题

在多进程中,不会出现这种情况,多进程里已经分好了,自己干自己的~~


3.Thread类

        java中操作多进程,就要用到Thread类

package thread;

class Mythread extends Thread{
    @Override
    public void run() {
        System.out.println("hello");
    }
}

public class ThreadDemo {
    public static void main(String[] args) {
        Thread t = new Mythread();
        t.start();
    }
}

这里和直接打印截然不同的~~

这里创建线程打印:

start没有调用run,它只是创建一个线程(就是调用操作系统的API,通过操作系统的内核创建新线程的PCB,并且把要执行的指令交给PCB,当PCB被调度到CPU上执行的时候,也就执行了线程run方法中的代码),再由这个新的线程来执行run方法

如果只是打印hello的话,这个时候java进程主要就是有一个线程(调用main方法的线程:主线程)通过t.start(),主线程调用t.start(),创建了一个新线程,新的线程调用t.run();

当run方法执行完毕,新的线程就被销毁


class Mythread extends Thread{
    @Override
    public void run() {
        while(true){
            System.out.println("hello thread");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

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

打印结果是

结果来看是同时执行的,但是不一定谁先谁后,操作系统调度进程的时候,是“抢占式执行”(万恶之源)的,具体在哪个进程先上,哪个后上,取决于操作系统调度器具体实现策略~~

虽然有优先级,但在应用程序层面上无法修改

从代码角度来看,看到的效果是随机的,但内核里并非是随机的(但是干预因素太多,应用程序这一层无法感受到细节,就只能认为是随机的~~)

        可以通过一些API进行有限的干预 

本质上Thread类还是系统里的线程的封装。每个Thread的对象就对应到系统中的一个线程(也就对应一个PCB)


4.start与run

        start 是真正创建一个线程(从系统这里创建的),线程是独立的执行流

        run只是描述了该线程要干啥活,如果直接在main中调用run,此时没有创建新线程,全是main线程一个人干活


 5.java中创建线程的几种方式

        1.继承Thread 重写run

        2.实现Runnanble接口

//Runnable 作用是描述要干啥活
class MyRunnable implements Runnable{
    @Override
    public void run() {
        System.out.println("hello thread");
    }
}

public class ThreadDemo2 {
    public static void main(String[] args) {
        //这是描述了任务
        Runnable runnable = new Mythread();
        //把任务交给线程来执行~~
        Thread thread = new Thread(runnable);
        thread.start();
    }
}

因此将描述和执行分开; 其最大意义就是解耦合了,将线程的任务和线程分离开~~

那么以后要改写代码,使用多进程,线程池等等,代码就改动不是那么大了~~

        3.使用匿名内部类,继承Thread

public class ThreadDemo3 {
    public static void main(String[] args) {
        Thread t = new Thread(){
            @Override
            public void run() {
                System.out.println("hello");
            }
        };
        t.start();
    }
}

        4.使用匿名内部类,实现Runnable

public class ThreadDemo4 {
    public static void main(String[] args) {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello");
            }
        });
        thread.start();
    }
}

        5.Lambda表达式 最推荐~~

public class ThreadDemo5 {
    public static void main(String[] args) {
        Thread t = new Thread(()->{
            System.out.println("hello");
        });
        t.start();
    }
}


6.Thread类中常见的方法及其属性

        6.1 构造方法
     

          6.2 属性

ID getId()
名称 getName()
状态 getState()
优先级 getPriority()
是否后台线程 isDaemon()  (守护线程~~)
是否存活 isAlive()
是否被中断 isInterrupted()

        前台线程/后台线程:

前台线程,会阻止线程的结束,前台工程的工作没完成,是无法终止的

后台线程,则反之~~

 因为t.setDaemon 将其设为后台进程后,执行结果可知 t线程并没有执行完,但却结束了,此时前台进程只有main了,什么时候main前台线程结束,整个就结束了~~

因此 进程 是否终止 与 t 毫无关系了~~

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

        isAlive() 

    public static void main(String[] args) {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                for(int i = 0;i < 3;i++){
                    System.out.println("hello thread");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        });
        t.start();
        while(true){
            try {
                Thread.sleep(1000);
                System.out.println(t.isAlive());
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }

        6.3 线程终止 

我们要先明确一下中断的意思:线程终止有三种可能:1.立即终止        2.等会一终止        3.不听

方式一:自定义一个flag

public class ThreadDemo {
    private static boolean flag = true;
    public static void main(String[] args) {
        Thread t = new Thread(()->{
            while(flag){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("hello thread");
            }
        });
        t.start();
        try {
            Thread.sleep(3000);
            flag = false;
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}

方式二: 自带的标志位~~

        先上代码:

public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(()->{
            while(!Thread.currentThread().isInterrupted()){
                System.out.println("hello");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
        Thread.sleep(3000);
        t.interrupt();
    }

因此,该写法 是忽视终止,总言之,interrupt的调用,只是通知终止,真正的是否会终止,不一定!!

         6.4 线程等待

public static void main(String[] args) {
        Thread t = new Thread(()->{
            for(int i = 0 ;i < 3;i++){
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        t.start();
        System.out.println("join之前");
        try {
            t.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("join之后");
    }

线程等待很好理解:上述代码,t.join()在main线程上调度的(会发生阻塞(block)),那么也就是main线程在t执行完毕后 接着再继续执行后续的代码~~

        

        6.5  线程休眠

线程休眠。本质上就是不让其参与调度了

具体是这样的:

         7.线程状态

线程状态是对针对当前线程调度情况描述的

java对线程状态,进行了细分

        

for循环里没有任何代码: 

 

 for内加上个sleep

 打印运行时的状态:


多线程的意义 :

        那么多线程的意义是什么呢?

程序可以分成俩部分:

        CPU密集:包换了加减乘除等算术运算

                IO密集:涉及到读写文件等等IO操作

public class ThreadDemo2 {
    private static void function(){
        long beg = System.currentTimeMillis();
        long a = 0;
        long b = 0;
        for(long i = 0;i < 1_0000_0000_00L;i++){
            a++;
        }
        for(long i = 0;i < 1_0000_0000_00L;i++){
            b++;
        }

        long end = System.currentTimeMillis();
        System.out.println("单线程操作的情况下:" + (end - beg));
    }

    public static void main(String[] args) {
        function();
        long beg = System.currentTimeMillis();
        Thread t1 = new Thread(()->{
            long a = 0;
            for(long i = 0;i < 1_0000_0000_00L;i++){
                a++;
            }
        });
        Thread t2 = new Thread(()->{
            long b = 0;
            for(long i = 0;i < 1_0000_0000_00L;i++){
                b++;
            }
        });
        t1.start();
        t2.start();
        long end = System.currentTimeMillis();
        System.out.println("多线程的情况下:" + (end - beg));
    }
}

 

        8.线程安全 

其万恶之源:抢占行执行        如果是单线程的话,就按照代码顺序向下执行就完事了,但如果是多线程的话,这些代码的执行顺序因为抢占性执行,是由一种情况突变成无数种,但凡有一个结果不符合预期,那么就是出现了BUG~~

我们通过以下的例子来理解线程安全:

static class Coutter{
        public int count = 0;
        public void add(){
            count++;
        }

        @Override
        public String toString() {
            return "Coutter{" +
                    "count=" + count +
                    '}';
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Coutter counter = new Coutter();
        Thread t1 = new Thread(()->{
           for(int i = 0;i < 50000;i++){
               counter.add();
           }
        });
        Thread t2 = new Thread(()->{
            for(int i = 0;i < 50000;i++){
                counter.add();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(counter.count);
    }

但是打印结果确不是我们所预期的

 

         8.1 线程安全出现的原因:

注意一下 以下的原因是经典的问题:

1> 根本原因:抢占性执行

        其线程是随机调度的,我们无能为力

2> 代码结构:多个线程修改了同一个变量

        多个线程修改一个变量是非常危险的,需通过调整代码结构来解决这个问题

3> 原子性:线程修改的操作对象如果是非原子性,是危险的

        原子:不可拆分的基本单位

上述add操作是将其分为3个操作 是非原子性的 我们需要将其变为原子性的对象 (加锁)

4> 内存可见性问题

5> 指令重排序:编译器优化bug

        指的是单个线程里 代码的执行顺序发生了改变

总之:多线程编码里不出现 bug 就是成功 安全的~~

在尝试解决线程安全的问题,我们大部分是从原子性方面来入手的,具体一点(用上述例子来将):就是对add方法加锁


 

static class Coutter{
        public int count = 0;
        synchronized public void add(){
            count++;
        }

        @Override
        public String toString() {
            return "Coutter{" +
                    "count=" + count +
                    '}';
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Coutter counter = new Coutter();
        Thread t1 = new Thread(()->{
           for(int i = 0;i < 50000;i++){
               counter.add();
           }
        });
        Thread t2 = new Thread(()->{
            for(int i = 0;i < 50000;i++){
                counter.add();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(counter.count);
    }

打印结构就是我们预期的~~ 线程就是安全的啦

还有一个 当俩个线程同时要求加锁的时候,一个线程上锁之后 只能等他解锁之后(另一个线程会进入阻塞),另一个线程才会上锁 等操作~~ 

还有要注意的是,其中的细节 并不是真正的将add的三步变为一步,而是等待(阻塞)~~

        具体是这样的:

 我们要知道是:加锁操作 肯定会让效率大打折扣的 但是保证了数据的准确性 这样的代价我们是可以接受的~~
 


        8.2 synchronized用法

1> 修饰方法

        1.修饰普通方法        锁的是this 引用的对象

        2.修饰静态方法        锁的是类对象

2> 修饰代码块                  显式/手动进行指向对象

所以 加锁 是要明确对谁加锁!!如果是俩个线程对同一个对象加锁,会出现锁竞争(阻塞等待)


虽然啊 这个关键字是在方法的前面,但是加锁 是针对这个对象的!!!

就比如说 上述的 代码 对add方法加锁 是针对 counter这个对象进行了加锁

 修饰代码块

出代码就是解锁,进代码就是加锁(这是一个 非常有用 且 非常有才华的举措)

         8.3 synchronized 关键字-监视器锁monitor lock

monitor lock是jvm对其命名的~~

         8.3.1 三大特性

1.互斥

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

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

2.刷新内存

         这一部分有争议,就不详细讲解

3.可重入

        一个对象如果已经上锁了,继续对其上锁,按理说是等待其解锁才能上锁,这里可能会出现锁死问题 但是synchronized特性是可重入的 因此不会出现死锁~~

        死锁的三种常见情况:

1.一个线程 一把锁 连续加锁俩次

死锁问题:

        第二次上锁是要等第一次解锁的,但是解锁是靠这个线程来执行的,但是这个线程因为要上第二把锁一直在阻塞等待,因此出现了死锁问题。

        这是死锁的一种情况~~

 2.俩个线程 俩个锁,俩个线程先各自对自己“想要的”对象进行加锁,还想获取对方对象的锁

        这个也很好理解,可以想作是 小明和小刚出去吃饭,小明点了一份蛋炒饭,小刚点了一份盖浇饭,这时候,小明这个线程 相当于对蛋炒饭进行了加锁 ,小刚这个线程对盖浇饭进行了加锁,别人要想或者这俩把锁 得看线程是否给他们(会进入阻塞等待),这时候小明想要 对小刚的盖浇饭这个对象进行品尝(对盖浇饭这个对象进行加锁),小刚也想对小明的蛋炒饭浅尝一下(对蛋炒饭这个对象进行加锁),这时候,小明因为已经对蛋炒饭这个对象进行了加锁,小刚就只好阻塞等待,同理,小明也进行了阻塞等待,这个时候 他们就互相等待(谁也吃不上了)~~

具体来看代码实现:
 

public class ThreadDemo5 {
    static class danchaofan{
        public void s(){
            synchronized (this){
                System.out.println("对蛋炒饭加锁");
            }
        }
    }

    static class gaijiaofan{
        public void k(){
            synchronized (this){
                System.out.println("对盖浇饭加锁");
            }
        }
    }

    public static void main(String[] args) {
        danchaofan danchaofan = new danchaofan();
        gaijiaofan gaijiaofan = new gaijiaofan();
        Thread xiaoming = new Thread(()->{

            synchronized (danchaofan){
                System.out.println("小明对蛋炒饭加锁");
            }
            synchronized (gaijiaofan){
                System.out.println("小明对盖浇饭加锁");
            }
        });
        Thread xiaogang = new Thread(()->{
           synchronized (gaijiaofan){
               System.out.println("小刚对盖浇饭枷锁");
           }
           synchronized (danchaofan){
               System.out.println("小刚对蛋炒饭枷锁");
           }
        });
    }
}

3. 多个线程,多把锁

        哲学家就餐问题

详解请看(13条消息) 哲学家就餐问题详解_FangYwang的博客-CSDN博客_什么是哲学家进餐问题

        8.3.2 死锁的必要条件

1.互斥

        线程1拿到了该对象的锁,线程2也想获得该对象的锁的话,就得阻塞等待

2.不可抢占

        比如线程1拿到了该对象的锁,线程2也想获得的话 就得等线程1将该对象解锁,不能抢占~~

3.请求和保持

        一个线程 先是获得了锁A,继续获得锁B 但锁A不会因为获得锁B就会解锁A

4.循环等待

        小明小刚问题~~

虽说死锁有这四个条件,但是对于synchronized这个锁来说,前三点是锁定特性。关键就是最后一个

        8.3.2 解决死锁

如何解决呢?因为前三点是锁定特性,我们无法改变,突破口是第四点 代码结构~~

        9. 线程安全类

        Java 标准库中很多都是线程不安全的. 这些类可能会涉及到多线程修改共享数据, 又没有任何加锁措施.
        Java 标准库中很多都是线程不安全的. 这些类可能会涉及到多线程修改共享数据, 又没有任何加锁措施.
ArrayList
LinkedList
HashMap
TreeMap
HashSet
TreeSet
StringBuilder
        但是还有一些是线程安全的. 使用了一些锁机制来控制.
Vector (不推荐使用)
HashTable (不推荐使用)
ConcurrentHashMap
StringBuffer
StringBuffer 的核心方法都带有 synchronized .

虽然string是没有锁的,但是string不能修改对象 其实它也是线程安全的~~

10.volatile关键字

        

这样的情况就是 “内存可见性问题” 

这也是一个线程安全的问题~~

为啥会发生这样子的问题呢,先上结论:JVM自作聪明!编译器优化出现bug~~

 1.volatile可以保证内存可见性(问题):

一个线程对一个变量进行读操作,另一个线程对该变量进行写操作,通过调度写操作的线程,来改变该变量,可能会导致读线程无法感受到变量的变化~~

解决办法也非常之简单:

在变量前加上一个关键字volatile~~

但我们要知道这个关键字,是一把双刃剑,不能全部无脑上,这会增加资源的浪费~~

这里以JMM(Java内存模型)的视角再来解释内存可见性:

        我们先明确一些概念,java程序里,(继续以上述代码为例)flag值存在于主内存,但是线程有自己的工作内存区(t1的工作内存区和t2的不是一个东西)

就是说:加上了volatile了,就是强制性让这个线程读取主内存~~ 虽然速度变慢,但是数据的准确性提高了

2.volatile不能实现原子性

        synchronized保证了原子性,但不一定保证内存可见性,volatile则是不能实现原子性,但可以保证内存可见性,但是这俩个关键字都是保证了线程安全~~

11. wait / notify

        我们知道这些线程,它们的调度顺序是完全随机的,采取抢占行执行,随即调度,这让程序员非常头疼。因为,这些随机的东西往往意味着“把握不住,会出现bug~~”

        这时候,我们就可以使用wait/notify这些关键字 来实现这些操作~~
比如说,有t1,t2线程    如果我们想t1先干活,然后让t2在预期时再进行调度(干活)呢?我们先让t1进行调度,t2先wait,等到t1干到我们预期的样子的时候,再去唤醒 t2 (notify)

       

首先明确一点,这些方法都是Object类的方法,那么这意味着些啥?

要知道 Object是所有类的父类,那就意味着所有的类都可以使用这些方法~~

        11.2 wait/notify

wait 阻塞

         某个线程调用了wait方法,无论该线程在何处调用,哪个对象调用,这个线程就处在阻塞状态。此时就处在WAITING。

这里举一个简单的代码栗子:

public static void main(String[] args)throws InterruptedException {
        Object o = new Object();
        Thread t1 = new Thread(()->{
            System.out.println("t1:wait之前");
            synchronized (o) {
                try {
                    o.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println("t1:wait之后");
        });

        Thread t2 = new Thread(()->{
            System.out.println("t2:notify之前");
            synchronized (o){
                o.notify();
            }
            System.out.println("t2:notify之后");
        });

        t1.start();
        t2.start();
    }

我们要注意 t1,t2线程里面的o这个对象都是同一个,如果想使用wait,notify的话,使用对象都必须是同一个,而且上锁的对象也是这同一个~~

此次的notify通知与wait配对,如果是同一个对象的话,notify则不会有任何效果,t1就会一直死等。

wait(带参数)与sleep的比较

        这二者非常相似,但本质上还是有差别的,sleep用interrupt来唤醒,怎么唤醒的?是抛一个异常,然后try catch

        wait是用notify来唤醒的,是一个正常的业务逻辑~~

然后一个是Object类的方法,一个是静态方法

11.2 notifyALL

没啥可说的,就是把所有都唤醒,然后一起进行锁竞争~~


12. 设计模式

        啥是设计模式呢?很好理解,java中啊,有一些大佬 认为我们写的代码有些LJ,就用JAVA里的基本语法来实现适用于一些特定场景的代码~~

        也可以这样理解:这些大佬是技术高超的棋手,然后把这些设计模式 写成了“棋谱” ,也就是设计模式。供我们小萌新学习以及使用~~

        12.1 单例模式

啥是单例模式呢?很好理解:在某些场景中,一些特定的类只能实例化一次,只能实例一个对象,而不能实例多个对象~~

        单例模式有俩种实现模式:

        1.饿汉模式

 ok,前期铺垫做足了 让我们 上代码~~

class Singleton{
    //饿汉模式
    private static Singleton instance = new Singleton();

    public static Singleton getInstance(){
        return instance;
    }
    //空的构造方法 防止new一个新的对象
    private Singleton(){}
}

public class ThreadDemo8 {
    public static void main(String[] args) {
        Singleton s1 = Singleton.getInstance();
        Singleton s2 = Singleton.getInstance();

    }
}

 

         2.懒汉模式

        12.2 单例模式的线程安全问题

说到线程安全 我们知道 如果一个线程有修改操作 可能这个线程会暴露出线程安全问题~~
那么以上俩种模式 哪个会有线程安全问题呢?

解决办法也很简单 无非就是加锁(synchronized) 

class SingletonLazy{
    private static SingletonLazy instance = null;

    public static SingletonLazy getInstance(){
        synchronized (SingletonLazy.class){
            if(instance == null){
                instance = new SingletonLazy();
            }
        }
        return instance;
    }
    private SingletonLazy(){}
}

利用锁 使读操作和写操作变成一个整体(原子性)

还有一个问题,因此这个new对象 只会new第一次,而上述加锁操作 使每次都要耗费空间,因此:我们可不可以 判断以下是否需要加锁呢?

这样做法也很简单,在加锁外面套一个判断条件 如果instance为空就加锁,不为空的话 直接return~~

public static SingletonLazy getInstance(){
        if(instance == null){
            synchronized (SingletonLazy.class){
                if(instance == null){
                    instance = new SingletonLazy();
                }
            }
        }
        return instance;
    }

         除了这个原子性问题,还会遇到 内容可见性问题,还有重排序问题 :volatile关键字去解决~~

        内容可见性:第一次才是读的内存 剩下的都是寄存器的 可能会造成数据更新,编译器感受不到的问题

        指令重排序:在new SingletonLazy()的时候,会分为三个操作:1.申请空间 2.调用构造方法,给这个空间创建一个合理的实例 3.将这个空间地址引用到instance使用

如果是123的顺序 是没有问题的,但是如果 通过编译器优化 顺序变成了132 先申请空间,直接就把空间给了instance这时候就会出现大乱子~~正要执行2的时候 这个线程突然停止,然后交给另一个线程来执行,另一个线程拿到的instance可以说是空的对象 非法的对象。

因此 完整体是这样的:

class SingletonLazy{
    private volatile static SingletonLazy instance = null;

    public static SingletonLazy getInstance(){
        if(instance == null){
            synchronized (instance){
                if(instance == null){
                    instance = new SingletonLazy();
                }
            }
        }
        return instance;
    }
    private SingletonLazy(){}
}

13. 阻塞式队列

        阻塞式队列也是一种特殊类型的队列,那特殊在哪里呢?具体体现在俩点:

1.当入队列的时候,如果队列是满的,则会阻塞等待,等该队列不满为止(等待其他线程从此队列取走元素)。

2.当取队列的时候,如果队列是空的,则会阻塞等待。等该队列不空为之(等待其他线程向此队列添加元素)。

我们可以想象成这一场景用于理解:

        在一个流水线上 一款产品需要A 和 B分别做不同的工作来完成,如果A完成一部分的加工的半成品直接给B的话,那可能会出现A等B(A做的太多了,B做的少了),B等A(A做的太少了,B做的太多了)的情况 ,我们想要解决这种情况的话,可以在A,B之间加上一个”缓冲带“,就是将A加红好的半成品放到一个框子中(缓冲带),这种应用场景我们可以简单的认为是 消费者生产者模型!

13.2 生产者消费者模型

        生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。

这样的模型使得生产者和消费者不会进行直接的信息交互,就是相当于说,生产者将生产的数据放置于阻塞队列中,而消费者如果想要获取数据的话,就从阻塞队列中获取。二者互不干扰。

13.2.2 生产者消费者模型的意义

        1.解耦合

我们知道耦合越低对于程序员来说越友好,如果我们使用了这种模型之后,可以有效的降低代码之间的耦合性,也就是说防止A线程出现了bug会导致B线程也崩溃 这一情况的发生!

        2.削峰填谷

比如一种场景,在游戏装备交易这一场景下,同一时间客户端向服务器请求了大量的订单信息。

如果直接进行交互的话,很有可能发生服务器崩溃,如果我们在服务器之间添加一个阻塞队列的

话,就很大的程度上可以避免或者是缓解这种情况的发生了。

13.3 实现一个阻塞式队列

链表实现会很简单,我们就用数组来实现一个阻塞式队列。

首先我们要想实现它的话,就得让这个数组变成一个循环数组。

public class MyBlockingQueue {
    private int[] items = new int[1000];
    public int size = 0;
    public int tail = 0;
    public int head = 0;

    public void put(int val) throws InterruptedException {
        synchronized (this) {
            //首先先判断 该数组是不是满了 需不需要阻塞等待 这里用while 是官方建议
            while (size == items.length) {
                this.wait();
            }
            items[tail] = val;
            tail++;
            size++;
            //填入之后 在判断一下 tail是否循环到head处
            if (tail >= items.length) {
                tail = 0;
            }
            //这里是唤醒take里的wait
            this.notify();
        }
    }

    public Integer take() throws InterruptedException {
        synchronized (this){
            while(size == 0){
                this.wait();
            }
            int ret = items[head];
            head++;
            size--;
            if(head >= items.length){
                head = 0;
            }
            //这里是唤醒put里的wait
            this.notify();
            return ret;
        }
    }

    //测试代码
    public static void main(String[] args) {
        MyBlockingQueue myBlockingQueue = new MyBlockingQueue();
        Thread t1 = new Thread(()->{
            while(true){
                try {
                    int ret = myBlockingQueue.take();
                    System.out.println("消费者得到的数是 "+ret);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        },"消费者");
        Thread t2 = new Thread(()->{
            int i = 0;
            while(true){
                try {
                    System.out.println("生产者提供的数是 "+ i);
                    myBlockingQueue.put(i++);
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        },"生产者");
        t1.start();
        t2.start();
    }
}

当然了,在java中也有封装好的阻塞队列的类型供我们使用

在 Java 标准库中内置了阻塞队列. 如果我们需要在一些程序中使用阻塞队列, 直接使用标准库中的即可.

BlockingQueue 是一个接口. 真正实现的类是 LinkedBlockingQueue.

put 方法用于阻塞式的入队列, take 用于阻塞式的出队列.

BlockingQueue 也有 offer, poll, peek 等方法, 但是这些方法不带有阻塞特性.

 14. 定时器

定时器的作用就如其名:等过了一段时间之后,就会提醒!
标准库中提供了一个 Timer 类. Timer 类的核心方法为 schedule .
schedule 包含两个参数. 第一个参数指定即将要执行的任务代码, 第二个参数指定多长时间之后
执行 (单位为毫秒). 

public static void main(String[] args) {
        Timer timer = new Timer();
        Thread t1 = new Thread(()->{
            //在线程t1中使用了定时器,在3000ms后再试行run内的代码
           timer.schedule(new TimerTask() {
               @Override
               public void run() {
                   System.out.println("hello");
               }
           },3000);
        });
        t1.start();
    }

 14.2 实现一个定时器

我们研究过了 定时器内部的工作原理,我们来实现这一定时器:

首先 在MyTimer这个类中,我们要明确需要什么样的数据结构来存储这些需要run的事件? 毕竟扫描线程的话 仅需要扫描第一个(也就是等待时间最段的)事件的时间到没到点就完事了,因此我们很快能够想到“优先级队列”,但是还有一个更完美的数据结构 供我们使用 —— PriorityBlockingQueue 阻塞优先级队列~~

我们来学习一下这个特殊的数据结构

它是一中具有堵塞性能的优先级队列,但是我们需要把事件传入此队列中,就需要自定义一个类:Mytask类,内部的方法不再一一赘述

class MyTask implements Comparable<MyTask> {
        private Runnable runnable;
        private long time;

        public MyTask(Runnable runnable, long time) {
            this.runnable = runnable;
            this.time = time;
        }

        public long getTime(){
            return this.time;
        }

        public void run(){
            runnable.run();
        }

        public int compareTo(MyTask o) {
            return (int) (this.time - o.time);
        }
    }

 前期工作完成 我们试着先写一下:

class MyTimer{
        //1.需要一个数据结构 来储存事件
        //这里是泛型,我们要存的是事件
        PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();
        //2.还需要一个线程来不断判断 执行
        //扫描线程
        private Thread t = null;
        //这里就要实现一个扫描线程 不断判断时间到没到
        public MyTimer(){
            t = new Thread(() -> {
                while(true){
                    try {
                        //阻塞队列 不能peek只能取出来观察时间
                        //注意一下 这里如果没调用schedule方法(就是没想队列添加数据) 就会发生阻塞~~
                        MyTask myTask = queue.take();
                        if(System.currentTimeMillis() < myTask.getTime()){
                            //现在的时间还没到 规定的时间 我们就继续等待(把mytask放回队列中)
                            queue.put(myTask);
                        }else{
                            //到时间了 就执行
                            myTask.run();
                        }
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            });
            //启动扫描线程
            t.start();
        }
    }

MyTimer内的核心写的差不都了,但不要忘了schedule方法


        //schedule这个方法很简单 就是单纯创建一个Mytask 然后将这个放入队列中~~
        public void schedule(Runnable runnable,long after){
            MyTask m = new MyTask(runnable,System.currentTimeMillis() + after);
            queue.put(m);
        }

最后写一个测试代码检测一下 是否成功:

public class ThreadDemo7 {
        public static void main(String[] args) {
            MyTimer myTimer = new MyTimer();
            myTimer.schedule(new Runnable() {
                @Override
                public void run() {
                    System.out.println("1");
                }
            }, 3000);
            myTimer.schedule(new Runnable() {
                @Override
                public void run() {
                    System.out.println("2");
                }
            }, 2000);
        }
    }

经过一段事件 结果是我们预期的,但这时候有一个小细节,那就是这里并没说结束运行,为什么呢?我想这里根前台线程有着密切关系(JVM是再所有非后台线程结束才结束),那就证明定时器是一个前台线程~~

 

ok家人们。到了现在虽说是可以实现,但我们发现在扫描线程里面,有一个很致命的问题,那就是它会不断的进行判断时间到没到点,就像我们早晨起床看闹钟,如果是时间还没到,我们会继续睡到闹铃醒为止,但是在这里如果是我们写的代码的话,它就相当于,看一眼躺一下,看一眼躺一下,就会非常浪费系统资源。那么我们如何解决这个问题呢?

其实想一想也很简单,就是让他继续睡到时间点呗,sleep方法似乎可以?不,sleep方法并不适用,因为不能确保时间的准确,并且如果在等待3000ms的时候 突然schedule一个1000ms的情况下会忽略新事件的,那么我们就剩下一个方法 那就是 wait方法~~
 

class MyTimer{
        //1.需要一个数据结构 来储存事件
        //这里是泛型,我们要存的是事件
        PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();
        //2.还需要一个线程来不断判断 执行
        //扫描线程
        private Thread t = null;
        //这里就要实现一个扫描线程 不断判断时间到没到
        public MyTimer(){
            t = new Thread(() -> {
                while(true){
                    try {
                        //阻塞队列 不能peek只能取出来观察时间
                        //注意一下 这里如果没调用schedule方法(就是没想队列添加数据) 就会发生阻塞~~
                        MyTask myTask = queue.take();
                        if(System.currentTimeMillis() < myTask.getTime()){
                            //现在的时间还没到 规定的时间 我们就继续等待(把mytask放回队列中)
                            synchronized (this){
                                this.wait(myTask.getTime() - System.currentTimeMillis());
                            }
                            queue.put(myTask);
                        }else{
                            //到时间了 就执行
                            myTask.run();
                        }
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            });
            //启动扫描线程
            t.start();
        }


        //schedule这个方法很简单 就是单纯创建一个Mytask 然后将这个放入队列中~~
        public void schedule(Runnable runnable,long after){
            MyTask m = new MyTask(runnable,System.currentTimeMillis() + after);
            synchronized (this){
                this.notify();
            }
            queue.put(m);
        }
    }

wait方法有很多优点在这里,不仅会等到时间到点唤醒,也会在队列插入事件的时候,唤醒线程。

注意一下 这里的wait是在判断时间的时候,notify就是在插入进队列的时候(来唤醒一下)。

家人们,还有一个不好的消息那就是 写到这里 还有一个非常极端的情况:那就是 如果(简单来说:先notify再wait了)

 那么就会发生 这个新事件的插入 空打一炮(白唤醒了),就没办法将这个事件插入队列中,也就是会错过它........

解决办法很简单,将synchronized包括全部构造方法内。

完美代码如下:

class MyTask implements Comparable<MyTask> {
        private Runnable runnable;
        private long time;

        public MyTask(Runnable runnable, long time) {
            this.runnable = runnable;
            this.time = time;
        }

        public long getTime(){
            return this.time;
        }

        public void run(){
            runnable.run();
        }

        public int compareTo(MyTask o) {
            return (int) (this.time - o.time);
        }
    }

    class MyTimer{
        //1.需要一个数据结构 来储存事件
        //这里是泛型,我们要存的是事件
        PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();
        //2.还需要一个线程来不断判断 执行
        //扫描线程
        private Thread t = null;
        //这里就要实现一个扫描线程 不断判断时间到没到
        public MyTimer(){
                t = new Thread(() -> {
                    while (true) {
                        try {
                            synchronized (this) {
                                //阻塞队列 不能peek只能取出来观察时间
                                //注意一下 这里如果没调用schedule方法(就是没想队列添加数据) 就会发生阻塞~~
                                MyTask myTask = queue.take();
                                long time = System.currentTimeMillis();
                                if (time < myTask.getTime()) {
                                    //现在的时间还没到 规定的时间 我们就继续等待(把mytask放回队列中)
                                    queue.put(myTask);
                                    this.wait(myTask.getTime() - time);
                                } else {
                                    //到时间了 就执行
                                    myTask.run();
                                }
                            }
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                    }
                });
                //启动扫描线程
                t.start();
        }


        //schedule这个方法很简单 就是单纯创建一个Mytask 然后将这个放入队列中~~
        public void schedule(Runnable runnable,long after){
            MyTask m = new MyTask(runnable,System.currentTimeMillis() + after);
            queue.put(m);
            synchronized (this){
                this.notify();
            }
        }
    }
    public class ThreadDemo7 {
        public static void main(String[] args) {
            MyTimer myTimer = new MyTimer();
            myTimer.schedule(new Runnable() {
                @Override
                public void run() {
                    System.out.println("1");
                }
            }, 3000);
            myTimer.schedule(new Runnable() {
                @Override
                public void run() {
                    System.out.println("2");
                }
            }, 2000);
        }
    }

还有要注意的是在schedule方法内 notify和put的顺序不要搞混。?

14. 线程池

虽然说线程已经是轻量后的进程了,但是线程仍会消耗较多的系统资源,我们还想追求一个更简洁更高效的方式来解决(或者是缓解)多线程资源开销问题,那么该咋做呢?

一个是引入一个更加轻量化的线程———纤程 但这个在java标准库中并没有写入,就不多赘述。

另一个是我们可以先整一群线程,什么时候用什么时候调,不用的时候,再放回去,就不用重复的创建销毁了(创建销毁 需要系统调用(交给操作系统内核完成),会造成大量的资源开销,并且cpu还得干其他事情,不会刚拿来请求,就帮你做这件事),因此 线程池的概念就应运而生。

在java标准库中 提供了现成的线程池供我们使用。

标准库中的线程池
使用 Executors.newFixedThreadPool(10) 能创建出固定包含 10 个线程的线程池.
返回值类型为 ExecutorService
通过 ExecutorService.submit 可以注册一个任务到线程池中

相当于:十个线程近乎等于平均每人100次for循环,而具体到每一个线程的话,就是这个线程干完一个打印 ,再来一个任务来做。

啥是工厂模式? 就是用一些方法代替构造方法 创建对象~~

14.2 常见的线程池创建方法

 然而,以上这些是通过包装ThreadPoolExecutor实现出来的

14.3 ThreadPoolExecutor 构造方法

14.2.2 标准库里的四种拒绝策略

 

14.4 实现一个线程池

一个线程池需要俩部分 :1.一个队列来保存任务 2.需要有多个线程来执行 这里就用循坏来解决。

class MyThreadPool{
    private BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();

    //构造方法内实现多个线程
    //只要循环创建目标线程,再取队列中任务给各个线程来run
    public MyThreadPool(int n){
        for(int i = 0;i < n;i++){
            Thread t = new Thread(()->{
    //这里用while循环 多次取队列中的元素                
                while(true) {
                    try {
                        Runnable runnable = queue.take();
                        runnable.run();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            });
            t.start();
        }
    }

    //注册任务给线程池
    public void submit(Runnable runnable)  {
        try {
            queue.put(runnable);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}



public class ThreadDemo9 {
    public static void main(String[] args) {
        MyThreadPool myThreadPool = new MyThreadPool(10);
        for(int i = 0;i < 10;i++){
            int ret = i;
            myThreadPool.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println(ret);
                }
            });
        }
    }
}

这部分相比较比较简单 就不多赘述了~~


长达2万字的多线程初阶就这样完成了,继续努力

;