JAVAEE多线程 ---- 三

JAVAEE多线程 ---- 三

1.wait 和 notify

线程的调度是无序的 , 随机的 . 但是 , 也有一定的需求场景 , 希望线程有序执行

join 是一种控制顺序的方式

wait 就是让某个线程先暂停下来等一等 , 发现条件不满足 , 就先阻塞等待

notify 就是把该线程唤醒 , 能够继续执行 , 其他线程构造了一个成熟的条件 , 就可以唤醒

wait 和 notify 都是 Object 的方法

1.1wait

wait 做的事情:

  1. 解锁
  2. 阻塞等待
  3. 当收到通知的时候 , 就唤醒 , 同时尝试重新获取锁

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

public static void main(String[] args) throws InterruptedException {
    Object object = new Object();
    System.out.println("wait 之前");
    synchronized (object) {
        object.wait();
    }


    System.out.println("wait 之后");
}

wait 结束等待的条件:

  1. 其他线程调用该对象的 notify 方法.
  2. wait 等待时间超时 (wait 方法提供一个带有 timeout 参数的版本, 来指定等待时间).
  3. 其他线程调用该等待线程的 interrupted 方法, 导致 wait 抛出 InterruptedException 异常.

1.2 notify

notify 也是要放到 synchronized 中使用的

public static void main(String[] args) throws InterruptedException {
    Object locker = new Object();
    Thread t1 = new Thread(() -> {
        try {
            System.out.println(" wait 开始");
            synchronized (locker) {
                locker.wait();
            }
            System.out.println("wait 结束");
        }catch (InterruptedException e) {
            e.printStackTrace();
        }

    });
    t1.start();

    Thread.sleep(1000);

    Thread t2 = new Thread(() -> {
        synchronized (locker) {
            System.out.println("notify 开始");
            locker.notify();
            System.out.println("notify 结束");
        }
    });
    t2.start();
}

notify 也是要放到 synchronized 中使用的 .

​ 必须要先执行 wait , 然后 notify , 此时才有效果 . 如果现在还没有 wait , 就 notify , 相当于 , 打空了 , 此时 wait 无法唤醒 , 代码不会出现其他异常

wait 和 sleep 的对比

  1. wait 和 sleep 都可以进行提前唤醒
  2. wait 解决的是线程之间的顺序控制
  3. sleep 单纯是让当前线程休眠一会
  4. wait 要搭配锁使用 , sleep 不需要

2.单例模式

单例模式 , 是一种经典的设计模式 (在校招阶段 , 主要考察两个模式 : 单例模式 , 工厂模式 )

单例 : 单个实例 , 一个程序中 , 某个类 , 只创建出一个实例 ( 一个对象 )

Java中实现单例模式有很多种写法 :这里只说两种

  1. 饿汉模式 ( 急迫 ) : 把文件所有内容都读取到内存中 , 并显示
  2. 懒汉模式 ( 从容 ) : 只把文件读一小部分 , 把当前屏幕填充上 , 如果用户翻页了 , 在读其他文件内容 , 如果不翻页 , 就省下了
1.饿汉模式

类加载的同时 , 创建实例

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

被 static 修饰 , 该属性是类的属性 , (类对象上 ) , JVM 中 , 每个类的类对象只有唯一一份 , 类对象里的这个成员自然也是唯一一份了

class Singleton {
    // 唯一实例的本体
    private static Singleton instance = new Singleton();

    //获取到实例的方法
    public static Singleton getInstance() {
        return instance;
    }

    //禁止外部 new 实例
    private Singleton() { }
}

此处 , 在类内部把 实例 创建好 , 同时禁止外部重新创建实例, 此时 , 就可以保证单例的特性了

2.懒汉模式

类加载的时候不创建实例 , 第一次使用的时候才创建实例

//通过懒汉模式实现一个单例模式
class SingletonLazy {
    //volatile 禁止指令重排序
    volatile private static SingletonLazy instance = null;

    public static SingletonLazy getInstance() {
        // 这个条件 , 判定是否要加锁 , 如果对象已经有了 , 就不必加锁了 , 此时本身就是安全的
        if (instance == null) {
            synchronized (SingletonLazy.class) {
                if (instance == null) {
                    instance = new SingletonLazy();
                }
            }
        }
        return instance;
    }

    private SingletonLazy() { }
}

理解双重 if 判定 / volatile:

  • 加锁/ 解锁 是一件开销比较高的事情 , 而懒汉模式的线程不安全只是发生在首次创建实例的时候 , 因此后续使用的时候, 不必再进行加锁了
  • 外层的 if 就是判定下看当前是否已经把 instance 实例创建出来了
  • 同时为了避免 “内存可见性” 导致读取的 Instance 出现偏差 , 于是补上 volatile
  • 当多线程首次调用 getInstance , 大家可能都发现 instance 为 null , 于是又继续往下执行来竞争锁 , 其中竞争成功的线程 , 再完成创建实例的操作
  • 当这个实例创建完了之后, 其他竞争到锁的线程就被里层 if 挡住了 , 也就不会继续创建其他实例

小结: 单例模式 , 线程安全问题

  1. 饿汉模式 , 天然就是安全的 , 只是读操作
  2. 懒汉模式 , 不安全的 , 有读也有写
    1. 加锁 , 把 if 和 new 变成原子操作
      2. 双重 if , 减少不必要的加锁操作
      3. 使用 volatile 禁止 指令重排序 , 保证后续线程肯定能拿到的是完整对象

3.阻塞队列

阻塞队列 : 带有阻塞特性 也遵守 "先进先出"原则

  1. 如果队列空 , 尝试出队列 , 就会阻塞等待 . 等待到队列不空为止
  2. 如果队列满 , 尝试入队列 , 也会阻塞等待 , 等待到队列不满为止
public class ThreadDemo19 {
    public static void main(String[] args) throws InterruptedException {
        BlockingQueue<String> queue = new LinkedBlockingQueue<>();
        // 阻塞队列核心方法, 主要有两个.
        // 1. put 入队列
        queue.put("hello1");
        queue.put("hello2");
        queue.put("hello3");
        queue.put("hello4");
        queue.put("hello5");
        // 2. take 出队列
        String result = null;
        result = queue.take();
        System.out.println(result);
        result = queue.take();
        System.out.println(result);
        result = queue.take();
        System.out.println(result);
        result = queue.take();
        System.out.println(result);
        result = queue.take();
        System.out.println(result);
        result = queue.take();
        System.out.println(result);
    }
}

生产者消费者模型

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

​ 生产者和消费者彼此之间不直接通讯 , 而是通过阻塞队列来进行通讯 , 所以生产者生产完数据之后不用等待消费者处理 , 直接扔给阻塞队列 , 消费者不找生产者要数据 , 而是直接从阻塞队列里取

1.可以让上下游模块之间 , 进行更好的 “解耦合”

2.阻塞队列就相当于一个缓冲区 , 平衡了生产者和消费者的处理能力

标准库中的阻塞队列 :

  1. BlockingQueue 是一个接口 . 真正实现的类是LinkedBlockingQueue
  2. put 方法用于阻塞队列的入队列 , take 用于阻塞队列的出队列
  3. BlockingQueue 也有 offer ,poll,peek等方法 , 但是这些方法不带有阻塞特性
BlockingQueue<String> queue = new LinkedBlockingQueue<>();
//入队列
queue.put("abc");
//出队列 , 如果没有 put 直接 take , 就会阻塞
String elem = queue.take();

生产者消费者模型

public static void main(String[] args) {
    BlockingDeque<Integer> blockingDeque = new LinkedBlockingDeque<>();
    Thread t1 = new Thread(() -> {
        while (true) {
            try {
                int value = blockingDeque.take();
                System.out.println("消费元素 :" + value);

            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    });
    t1.start();

    Thread t2 = new Thread(() -> {
        int value = 0;
        while (true) {
            try {
                System.out.println("生产元素:" + value);
                blockingDeque.put(value);
                value ++;
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    });
    t2.start();

    //上述代码中 , 让生产者 , 每隔 1s 生产一个元素
    //让消费者则直接消费 , 不受限制
}

阻塞队列实现

实现一个阻塞队列 , 需要三部:

  1. 先实现一个普通队列
  2. 加上线程安全
  3. 加上阻塞功能
  • 通过 " 循环队列 "的方式实现
  • 使用 synchroized 进行加锁控制
  • put 插入元素的时候 , 判定如果队列满了 , 就进行 wait , (注意:要在循环中进行 wait ,被唤醒时不一定队列就不满了 , 因为同时可能是唤醒了多个线程)
  • take 取出元素的时候 , 判定如果队列为空 , 就进行 wait (也是循环 wait)
class MyBlockingQueue {
    private int[] items = new int[1000];
    //约定 [head , tail ) 队列 的有效元素
    volatile private int head = 0;
    volatile private int tail = 0;
    volatile private int size = 0;

    //入队列
    synchronized public void put(int elem) throws InterruptedException {
        while (size == items.length) {
            //队列满了 , 插入失败
            //return;
            this.wait();
        }
        //把新元素放到 tail 所在位置上
        items[tail] = elem;
        tail++;
        //万一 tail 达到末尾 , 就需要让 tail 从头再来
        if (tail == items.length) {
            tail = 0;
        }
        size++;
        this.notify();
    }
    //出队列
    synchronized public Integer take() throws InterruptedException {
        while(size == 0) {
            //return null;
            this.wait();
        }
        int value = items[head];
        head++;
        if (head == items.length) {
            head = 0;
        }
        size--;
        this.notify();
        return value;
    }
}
// 注意  上述两处代码的 wait 不可能同时阻塞 , 一个独立不可能即是空 又是满
public class ThreadDemo21 {
    public static void main(String[] args) {
            public static void main(String[] args) {
        MyBlockingQueue queue = new MyBlockingQueue();
        //消费者
        Thread t1 = new Thread(() -> {
            while (true) {
                int value = 0;
                try {
                    value = queue.take();
                    System.out.println("消费 :" + value);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            }
        });

        //生产者
        Thread t2 = new Thread(() -> {
            int value = 0;
            while (true) {

                try {
                    System.out.println("生产 : " + value);
                    queue.put(value);
                    Thread.sleep(1000);
                    value++;
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t1.start();
        t2.start();
    }

    }

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

很有可能在别的代码里暗中 interrupt , 把 wait 给提前唤醒了 , 明明条件还没满足 (队列非空) ,但是 wait 唤醒之后就不继续往下走了 .

更稳妥的做法 , 是在 wait 唤醒之后 , 再判定一次条件

wait 之前 , 发现条件不满足 , 开始 wait ; 然后等到 wait 被唤醒了之后 , 在确认一下条件是不是满足 , 如果不满足 , 还可以继续 wait

解决: 将上述代码的 if 改成 while

4.定时器

设定一个时间 , 当时间到 , 就可以执行一个指定的代码

标准库中的定时器:

  • 标准库中提供了一个 Timer 类 , Timer 类的核心方法为 schedule
  • schedule 包含两个参数 , 第一个参数指定即将要执行的任务代码 , 第二个参数指定多长时间之后执行 (单位为毫秒)
Timer timer = new Timer():
timer.schedule(new TimerTask () {
    @Override
    public void run() {
        System.out.println("hello");
    }
},3000);

在这里插入图片描述

实现一个定时器

定时器 , 内部管理的不仅仅是一个任务 , 可以管理很多个任务

​ 虽然任务可能会有很多 , 他们触发的时间是不同的 , 只需要有一个/一组 工作线程, 每次都找到这个任务中 , 最先到达时间的任务 , 一个线程 , 先执行最早的任务 , 做完了之后再执行第二早的 ,时间到了就执行, 没到就等等

定时器的构成:

  1. 一个带优先级的阻塞队列

    为什么要优先级

    因为阻塞队列中的任务都有各自执行时刻(delay) . 最先执行的任务一定是delay 最小的 , 使用带优先级的队列就可以高效的把这个 delay 最小的任务找出来

  2. 队列中的每个元素是一个 Task 对象

  3. Task 中带有一个时间属性 ,

  4. 同时又一个 woker 线程一直扫描队首元素 , 看队首元素是否需要执行

完整代码:

// 表示一个任务
class MyTask implements Comparable<MyTask>{
    public Runnable runnable;
    //为了方便后续判定 , 使用绝对的时间戳
    public long time;


    public MyTask(Runnable runnable, long delay) {
        this.runnable = runnable;
        // 取当前时刻的时间戳 + delay ,作为该任务实际执行的时间戳
        this.time = System.currentTimeMillis() + delay;
    }

    @Override
    public int compareTo(MyTask o) {
        //这样的写法意味着每次取出来的是时间最小的元素 ,
        //
        return (int)(this.time - o.time);
    }
}
class MyTimer {
    //创建一个锁对象
    private Object locker = new Object();
    //这个结构 , 带有优先级的阻塞队列 , 核心数据结构
    private PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();

    //此处的 delay 是一个形如 3000 这样子的数字 (多长时间之后 , 执行该任务)
    public void schedule(Runnable runnable, long delay) {
        // 根据参数 , 构造 MyTask , 插入队列即可
        MyTask myTask = new MyTask(runnable, delay);
        queue.put(myTask);

        synchronized (locker) {
            locker.notify();
        }
    }


    //在这里构造线程, 负责执行具体任务
    public MyTimer() {
        Thread t = new Thread(() -> {
            while (true) {
                try {
                    //阻塞队列 , 只有阻塞的入队列和阻塞的出队列 , 没有阻塞的查看对首元素.
                    synchronized (locker) {
                        MyTask myTask = queue.take();
                        long curTime = System.currentTimeMillis();
                        if (myTask.time <= curTime) {
                            //时间到了 , 可以执行任务了
                            myTask.runnable.run();
                        }else {
                            //时间还没到
                            //把刚才取出来的任务 , 重新塞回队列中
                            queue.put(myTask);
                            locker.wait(myTask.time - curTime );
                        }
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
    }

}
public class ThreadDemo23 {
    public static void main(String[] args) {
        // ms 级别的时间戳 , 当前时刻和基准时刻的 ms之差 , 基准时刻 : 1970 年 1 月 1 日 00:00:00
        //System.out.println(System.currentTimeMillis());
        MyTimer myTimer = new MyTimer();
        myTimer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello2");
            }
        },2000);
        myTimer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello1");
            }
        },1000);
        System.out.println("hello0");
    }

上述代码中 , 使用wait 等待而不是 sleep , wait方便随时提前唤醒 , wait 的参数是"超时时间" , 时间到达一定程度后 , 还没有 notify 就不等 , 如果时间还没到 , 就 notify 立即返回

5.线程池

线程池的最大好处就是 , 减少每次启动,销毁线程的损耗

从线程池取线程 , 是属于纯用户态操作 , 不涉及到和内核的交互

标准库中的线程池

  • 使用 Executors.newFixedThreadPool(10) 能创建出固定的包含10个线程的线程池
  • 返回值类型为 ExecutorService
  • 通过ExcutorService.submit 可以注册一个任务到线程池中
    public static void main(String[] args) {
        //创建一个10线程的线程池
        ExecutorService pool = Executors.newFixedThreadPool(10);
        pool.submit(new Runnable() {
            @Override
            public void run() {
                System.out.println("pool");
            }
        });
    }

Executors 创建线程池的几种方式:

  1. newFixedThreadPool : 创建固定线程数的线程池
  2. newCachedThreadPool : 创建线程数目动态增长的线程池
  3. newSingleThreadExecutor :创建只包含单个线程的线程池
  4. newScheduleThreadPool : 设定 延迟时间后执行命令 , 或者定期执行 命令 , 是进阶版的 Timer

Executors 本质上是 ThreadPoolExecutor 类的封装

ThreadPoolExecutor 参数 :

  1. corePoolSize : 核心线程数

  2. maximumPoolSize : 最大线程数

    如果当前任务比较多 , 线程池就会多创建一些"临时线程" , 如果当前任务较少 , 线程池就会把多出来的临时工程销毁掉

  3. long keepAliveTime : “临时线程” 保持存活的时间

  4. TimeUnit unit : 单位 s , 分钟 , ms

  5. BlockingQueue workQueue : 线程池里要管理许多任务 , 这些任务也是通过阻塞队列来组织的 , submit 方法其实就是把任务放到该队列中(程序员可以手动指定给线程池一个队列 , 此时程序员就很方便地可以控制/获取队列中的信息了)

  6. ThreadFactory threadFactory : 工厂模式 , 创建线程的辅助类

  7. RejectedExecutionHandler handler : 线程池的拒绝策略 , 如果线程池满了 吗继续往里添加任务 , 如何进行拒绝

标准库中提供的四种拒绝策略

ThreadPoolExecutor.AbortPolicy如果满了,继续添加任务,添加操作直接抛出异常
ThreadPoolExecutor.CallerRunsPolicy添加的线程自己负责执行这个任务
ThreadPoolExecutor.DiscardOldestPolicy丢弃最老的任务(指的是最先安排的任务)
ThreadPoolExecutor.DiscardPolicy丢弃最新的任务

ThreadPoolExecutor 类的构造方法的参数 , 都要重点掌握 ,尤其是拒绝策略

实现线程池

  • 核心操作为submit , 将任务加入线程池中
  • 使用 Worker 类描述一个工作线程 , 使用 Runnable 描述一个任务
  • 使用一个 BlockingQueue 组织所有的任务
  • 每个 worker 线程要做的事情 , 不停地从 BlockingQueue 中取任务并执行
  • 指定以下线程中最大的线程数 maxWorkerCount , 当当前线程超过这个最大值时, 就不在新增线程了
class MyThreadPool {
    //阻塞队列用来存放任务
    private BlockingDeque<Runnable> queue = new LinkedBlockingDeque<>();

    public void submit(Runnable runnable) throws InterruptedException {
        queue.put(runnable);
    }

    //此处实现一个固定线程数的线程池
    public MyThreadPool(int n) {
        for (int i = 0; i < n; i++) {
            Thread t = new Thread(() -> {
                try {
                    while (true) {
                        //此处需要让线程内部有个 while 循环 , 不停地取任务
                        Runnable runnable = null;
                        runnable = queue.take();
                    }
                }catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
            t.start();
        }
    }
}


public class ThreadDemo25 {
    public static void main(String[] args) throws InterruptedException {
        MyThreadPool pool = new MyThreadPool(10);
        for (int i = 0; i < 1000; i++) {
            //每次循环都是创建一个新的 number ,没有人修改 该number
            int number = i;
            pool.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("hello" + number);
                }
            });
        }
        Thread.sleep(3000);
    }
}

当前代码中 , 有十个线程的线程池 , 实际开发中 , 一个线程池的线程数量 ,设置成绩, 是比较合适的??

​ 答 : 测试!!

​ 不同的程序 , 线程的作用不一样 ;

		1. cpu 密集型任务 , 主要做一些计算工作 , 要在cpu 上运行的
		2. IO 密集型任务 , 主要是等待IO操作(等待读写硬盘 , 读写网卡)

6.总结–保证线程安全的思路

  1. 使用没有共享资源的模型
  2. 使用共享资源只读 , 不写的模型
    1. 不需要写共享资源的模型
    2. 使用不可变对象
  3. 直面线程安全
    1. 保证原子性
    2. 保证顺序性
    3. 保证可见性

线程的优点:

  1. 创建一个新线程的代价要比创建一个新进程小的多
  2. 与进程之间的切换相比 , 线程之间的切换需要操作系统做的工作要少很多
  3. 线程占用的资源要比进程少很多
  4. 能充分利用多处理器的课并行数量
  5. 在等待慢速 I/O 操作结束的同时 , 程序可执行其他的计算任务
  6. 计算密集型应用 , 为了能在多处理器系统上运行 , 将计算分解到多个线程中实现
  7. I/O 密集型应用 , 为了提高性能 , 将I/O操作重叠 , 线程可以同时等待不同的I/O操作

进程与线程的区别 :

  1. 进程是系统进行资源分配和调度的一个独立单位 , 线程是程序执行的最小单位
  2. 进程有自己得内存地址空间 , 线程值独享指令流指令的必要资源 , 如寄存器和栈
  3. 由于同一进程的各线程间共享内存和文件资源 , 可以不通过内核进行直接通信
  4. 线程的创建 , 切换及终止效率更高.

证线程安全的思路

  1. 使用没有共享资源的模型
  2. 使用共享资源只读 , 不写的模型
    1. 不需要写共享资源的模型
    2. 使用不可变对象
  3. 直面线程安全
    1. 保证原子性
    2. 保证顺序性
    3. 保证可见性

线程的优点:

  1. 创建一个新线程的代价要比创建一个新进程小的多
  2. 与进程之间的切换相比 , 线程之间的切换需要操作系统做的工作要少很多
  3. 线程占用的资源要比进程少很多
  4. 能充分利用多处理器的课并行数量
  5. 在等待慢速 I/O 操作结束的同时 , 程序可执行其他的计算任务
  6. 计算密集型应用 , 为了能在多处理器系统上运行 , 将计算分解到多个线程中实现
  7. I/O 密集型应用 , 为了提高性能 , 将I/O操作重叠 , 线程可以同时等待不同的I/O操作

进程与线程的区别 :

  1. 进程是系统进行资源分配和调度的一个独立单位 , 线程是程序执行的最小单位
  2. 进程有自己得内存地址空间 , 线程值独享指令流指令的必要资源 , 如寄存器和栈
  3. 由于同一进程的各线程间共享内存和文件资源 , 可以不通过内核进行直接通信
  4. 线程的创建 , 切换及终止效率更高.
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值