厚积薄发打卡Day26:狂神说Java之JUC并发编程<代码+笔记>(上)

前言:

学习视频来源:【狂神说Java】JUC并发编程最新版通俗易懂

一个十分优秀且励志的技术大牛+Java讲师,十分推荐他的频道:遇见狂神说 👍👍👍

至于为啥这篇文章不是”全网最全“了,是因为狂神自己总结了一份学习笔记哈哈,本文是根据学习资料自己总结,方便以后查漏补缺🤭

狂神置顶评论

(置顶) 白漂有罪,拒绝白嫖,从点赞转发关注做起!
文章同步在公众号:狂神说 (公众号日更,记得关注)
视频文档地址:https://gitee.com/kuangstudy/openclass 记得三连➕转发

本文截止至异步回调 ,从 JMM 开始分为下篇。

🚀好,接下来就正式开始 ”勾优西 “ 的学习了~🚀


前置知识:学习JUC前需要对java的多线程有一定的掌握。不过本期视频狂神也在第1~4节带着我们回顾了多线程的一些知识点:

详细资料可参考博客:厚积薄发打卡Day25 :狂神说Java之多线程详解<全网最全(代码+笔记)>

0.学习方法

源码+官方文档:

其实官方文档就是源码编译出来的,其本质还是看源码,不过文档会比较方便学习

  • 只有多看源码,多研究文档才会进步
  • Java英文文档可以通过点击查看源码获取
  • Java1.8中文文档(中文 – 谷歌版)
    • 在线版: https://blog.fondme.cn/apidoc/jdk-1.8-google/
    • 下载链接: https://pan.baidu.com/s/10wTC1F_4EUPsHzrn-_sPTw 密码:k7rm

1.什么是JUC

  • JUC其实就是Java.Util.concurrent包的缩写
java.util.concurrent
java.util.concurrent.atomi
java.util.concurrentlocks

是 java.util 工具包、包、分类

  • 回顾开启线程的三种方式:

    1. Thread

    2. Runnable

    3. Callable

在这里插入图片描述

2.线程与进程

线程、进程,如果不能使用一句话说出来的技术,不扎实!

打开(Win10)任务管理器可以清楚看到执行的线程与进程:
在这里插入图片描述

参考博客:什么是线程?什么是进程

  • 进程

    • 官方定义:

      进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础

    • 简单理解:

      进行(运行)中的程序,如打开任务管理器后中各种.exe程序

  • 线程

    • 官方定义:

      线程(英语:thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。

    • 简单理解:

      线程是真正执行资源调度(使程序跑起来)的主体,一个进程往往可以包含多个线程,但至少包含一个线程。

      如:开一个idea进程,其中至少有—> 线程1:输入代码,线程2:自动保存

😶老是强调多线程,那么 Java真的可以开启线程吗?

答案是 : 不能。查看Thread.start()源码可得知:

public synchronized void start() {
    if (threadStatus != 0)
        throw new IllegalThreadStateException();
    group.add(this);
    boolean started = false;
    try {
        start0();
        started = true;
    } finally {
        try {
            if (!started) {
                group.threadStartFailed(this);
            }
        } catch (Throwable ignore) {
        }
    }
}
// 本地方法,底层的C++ ,Java 无法直接操作硬件
private native void start0();
  • 并发编程

    并发编程:并发、并行

    • 并发(多线程操作同一个资源)
      • CPU 一核 ,模拟出来多条线程,天下武功,唯快不破,快速交替
    • 并行(多个人一起行走)
      • CPU 多核 ,多个线程可以同时执行; 线程池
    public class Test1 {
        public static void main(String[] args) {
            // 获取cpu的核数
            // CPU 密集型,IO密集型
            System.out.println(Runtime.getRuntime().availableProcessors());
           //输出为8
            //说明笔者电脑为八核处理器
        }
    }
    
  • 线程的几个状态:

    从源码回答,有理有据

    public enum State {
        // 新生
        NEW,
        // 运行
        RUNNABLE,
        // 阻塞
        BLOCKED,
        // 等待,死死地等
        WAITING,
        // 超时等待
        TIMED_WAITING,
        // 终止
        TERMINATED;
    }
    
  • wait与sleep的区别

    看源码说话嗷🎈

    //Object.wait()
    public final void wait() throws InterruptedException {
        wait(0);
    }
    
    //Thread.sleep()
    public static native void sleep(long millis) throws InterruptedException;
    
    1. 来自不同的类

      1. wait() 来自 Object类
      2. sleep() 来自 Thread类
    2. 关于锁的释放

      1. wait() 会释放锁:wait是进入线程等待池等待,出让系统资源,其他线程可以占用CPU。
      2. sleep() 不出让系统资源;(简单来说,就是抱着🔒睡觉)
    3. 是否需要捕获异常?

      需要。源码都在上面写的死死的,throws InterruptedException 😓都不知网上随手一搜的博客说wait() 不用捕获异常怎么搞得。

      好吧,回看了下视频,狂神好像也翻车了哈哈😂

    4. 使用范围:

      1. wait() 需要在同步代码块中使用

        // wait、notify/notifyAll必须在同步控制块、同步方法里面使用。而sleep的使用在任意地方。
        synchronized(x){
            x.notify()
           //或者wait()
        }
        
      2. sleep()可以在任何地方睡

    5. 作用对象:

      1. wait()定义在Object类中,作用于对象本身
      2. sleep()定义在Thread 类中,作用当前线程。
    6. 方法属性:

      1. wait()是实例方法
      2. sleep()是静态方法 有static

3.Lock锁(重点)

  • 回顾用传统的 synchronized 实现 线程安全的卖票例子

    真正的多线程开发,公司中的开发,降低耦合性

    线程就是一个单独的资源类,没有任何附属的操作!

    对于资源类只有: 属性、方法

    开启线程错误方式:

    class Ticket implements Runnable{}
    

    耦合度高,违背了oop(面向对象)思想

    public class SaleTicket_WithSynchronized {
        public static void main(String[] args) {
            // 并发:多线程操作同一个资源类, 把资源类丢入线程
            Ticket ticket = new Ticket();
            // @FunctionalInterface 函数式接口,jdk1.8 lambda表达式 (参数)->{ 代码 }
            new Thread(() -> {
                for (int i = 1; i < 40; i++) {
                    ticket.saleTicket();
                }
            }, "A").start();
            new Thread(() -> {
                for (int i = 1; i < 40; i++) {
                    ticket.saleTicket();
                }
            }, "B").start();
            new Thread(() -> {
                for (int i = 1; i < 40; i++) {
                    ticket.saleTicket();
                }
            }, "C").start();
    
        }
    }
    
    //资源类 OOP:
    class Ticket {
        //属性、方法
        private int number = 30;
        //卖票方法
        //用synchronized 上锁
        public synchronized void saleTicket() {
            if (number > 0) {
                System.out.println(Thread.currentThread().getName() + "卖出了第" + (number--) + "张票,剩余" + number + "张");
            }
        }
    }
    
  • 用Lock接口

    Lock源码定义:
    在这里插入图片描述

    • 常用上锁语句:

      Lock l = ...;
      l.lock();   //加锁
      try { // access the resource protected by this lock
      } 
      finally 
      { l.unlock(); //解锁} 
      
    • ReentrantLock 可重入锁在这里插入图片描述

    • 公平锁与非公平锁(简单认识,后面详解)

      • 公平锁: 非常公平, 不能够插队,必须先来后到!
      • 非公平锁:非常不公平,可以插队 (默认都是非公平)
    • 上案例

    // Lock 三部曲
    // 1、 new ReentrantLock();
    // 2、 lock.lock(); // 加锁
    // 3、 finally=> lock.unlock(); // 解锁
    public class SaleTicket_WithLock {
        public static void main(String[] args) {
            // 并发:多线程操作同一个资源类, 把资源类丢入线程
            Ticket ticket = new Ticket();
            // @FunctionalInterface 函数式接口,jdk1.8 lambda表达式 (参数)->{ 代码 }
            new Thread(() -> {
                for (int i = 1; i < 40; i++) {
                    ticket.saleTicket();
                }
            }, "A").start();
            new Thread(() -> {
                for (int i = 1; i < 40; i++) {
                    ticket.saleTicket();
                }
            }, "B").start();
            new Thread(() -> {
                for (int i = 1; i < 40; i++) {
                    ticket.saleTicket();
                }
            }, "C").start();
    
        }
    
    }
    
    class Ticket2 {
        // 属性、方法
        private int number = 30;
        Lock lock = new ReentrantLock();
    
        public void sale() {
            lock.lock(); // 加锁
            try {
                // 业务代码
                if (number > 0) {
                    System.out.println(Thread.currentThread().getName() + "卖出了第" +
                            (number--) + "张票,剩余:" + number + "张");
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock(); // 解锁
            }
        }
    }
    
  • Synchronized 和 Lock 区别

    Java中的锁——Lock和synchronized

    4、相比于synchronized,Lock接口所具备的其他特性

    ①尝试非阻塞的获取锁tryLock():当前线程尝试获取锁,如果该时刻锁没有被其他线程获取到,就能成功获取并持有锁

    ②能被中断的获取锁lockInterruptibly():获取到锁的线程能够响应中断,当获取到锁的线程被中断的时候,会抛出中断异常同时释放持有的锁

    ③超时的获取锁tryLock(long time, TimeUnit unit):在指定的截止时间获取锁,如果没有获取到锁返回false

    synchronizedLock
    性质Java的内置关键字,在JVM层面上Java类(接口)
    锁的释放1、以获取锁的线程执行完同步代码,释放锁
    2、线程执行发生异常,jvm会让线程释放锁
    必须要手动释放锁,如果不释放锁,会造成死锁
    锁的获取假设A线程获得锁,B线程等待。
    如果A线程阻塞,B线程会一直等待
    分情况而定,Lock有多个锁获取的方式:大致就是可以尝试获得锁,线程可以不用一直等待
    锁状态无法判断可以判断
    锁类型可重入 不可中断 非公平可重入 可判断 可公平(两者皆可,可手动设置)

4.生产者消费者问题

从pc(producer<–>consumer)问题看锁的本质

1.synchronized版本

  • 生产者消费者synchronized版本

    案例:对一个数字不停进行 +1 -1操作,加完了减,减完了加

    public class PC_WithSynchronized {
        public static void main(String[] args) {
            Data data = new Data();
            new Thread(() -> {
                for (int i = 0; i < 10; i++) {
                    try {
                        data.increment();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            },"ADD").start();
            new Thread(()->{
                for (int i = 0; i < 10; i++) {
                    try {
                        data.decrement();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            },"MINUS").start();
        }
    }
    //判断等待-->业务代码-->通知
    //数字:资源类
    class Data{
        //属性
        private int number = 0;
        //+1方法
        public synchronized  void increment() throws InterruptedException {
            if (number != 0){
                //等待
                this.wait();
            }
            number++;
            System.out.println(Thread.currentThread().getName()+"-->"+number);
            //加完了通知其他线程
            this.notifyAll();
        }
    
        //-1
        public synchronized void decrement() throws InterruptedException {
            if (number == 0){
                //等待
                this.wait();
            }
            number--;
            System.out.println(Thread.currentThread().getName()+"-->"+number);
            //减完了通知其他线程
            this.notifyAll();
        }
    }
    

    此时输出:

    加减交替执行,一片祥和~

    ..
    ADD-->1
    MINUS-->0
    ADD-->1
    MINUS-->0
    ADD-->1
    MINUS-->0
    ADD-->1
    MINUS-->0
    ADD-->1
    MINUS-->0
    ...
    

    问题来了,现在只有"ADD"和"MINUS"两个线程执行操作,如果增加多两个线程会怎样呢?

    于是在main方法中增加了"ADD2" 和 "MINUS2"两条线程:

    new Thread(() -> {
        for (int i = 0; i < 10; i++) {
            try {
                data.increment();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    },"ADD2").start();
    new Thread(()->{
        for (int i = 0; i < 10; i++) {
            try {
                data.decrement();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    },"MINUS2").start();
    

    出大问题了:出现了数据错误甚至死锁问题

    在这里插入图片描述

    原因如下:

    • 用if判断只执行了一次判断,而wait()方法会导致🔒的释放

    • 具体说明:以两个加法线程ADD、ADD2举例:

      • 比如ADD先执行,执行时调用了wait方法,那它会等待,此时会释放锁。
      • 那么线程ADD2 获得锁并且也会执行wait()方法,且释放锁,同时两个加线程一起进入等待状态,等待被唤醒。
      • 此时减线程中的某一个线程执行完毕并且唤醒了这俩加线程(notifyAll),那么这俩加线程不会一起执行,其中ADD获取了锁并且加1,执行完毕之后ADD2再执行。
      • 如果是if的话,那么ADD修改完num后,ADD2不会再去判断num的值,直接会给num+1,如果是while的话,ADD执行完之后,ADD2还会去判断num的值,因此就不会执行。
    • 上述情况称为:虚假唤醒

      在这里插入图片描述

    • 此时解决方法:将 if 改为 while

    package com.kuangstudy.JUC;
    
    /**
     * @BelongsProject: itstack-demo-design-1-00
     * @BelongsPackage: com.kuangstudy.JUC
     * @Author: lgwayne
     * @CreateTime: 2021-02-20 17:32
     * @Description:
     */
    public class PC_WithSynchronized {
        public static void main(String[] args) {
            Data data = new Data();
            new Thread(() -> {
                for (int i = 0; i < 10; i++) {
                    try {
                        data.increment();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            },"ADD").start();
            new Thread(()->{
                for (int i = 0; i < 10; i++) {
                    try {
                        data.decrement();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            },"MINUS").start();
            new Thread(() -> {
                for (int i = 0; i < 10; i++) {
                    try {
                        data.increment();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            },"ADD2").start();
            new Thread(()->{
                for (int i = 0; i < 10; i++) {
                    try {
                        data.decrement();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            },"MINUS2").start();
        }
    }
    //判断等待-->业务代码-->通知
    //数字:资源类
    class Data{
      //data类相同,不再赘述
    }
    

    结果一片祥和~

    ...
    ADD2-->1
    MINUS2-->0
    ADD2-->1
    MINUS2-->0
    ADD2-->1
    MINUS2-->0
    ADD2-->1
    MINUS2-->0
    ADD2-->1
    MINUS2-->0
    ...    
    

2.JUC的船新版本

在这里插入图片描述

通过Lock找到condition来配合控制对线程的唤醒

  • public interface Condition
    

    Condition因素出Object监视器方法( waitnotifynotifyAll )成不同的对象,以得到具有多个等待集的每个对象,通过将它们与使用任意的组合的效果Lock实现。 Lock替换synchronized方法和语句的使用, Condition取代了对象监视器方法的使用。

public class PC_WithLock {
    public static void main(String[] args) {
        //主线程测试方法与上述一致,不再赘述
    }
}

//判断等待-->业务代码-->通知
//数字:资源类
class Data_withLock {
    //属性
    private int number = 0;

    private Lock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();

    //+1方法
    public void increment() throws InterruptedException {
        lock.lock();
        try {
            while (number != 0) {
                //等待
                condition.await();
            }
            number++;
            System.out.println(Thread.currentThread().getName() + "-->" + number);
            //加完了通知其他线程
            condition.signalAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    //-1
    public void decrement() throws InterruptedException {
        lock.lock();
        try {
            while (number == 0) {
                //等待
                condition.await();
            }
            number--;
            System.out.println(Thread.currentThread().getName() + "-->" + number);
            //减完了通知其他线程
            condition.signalAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
}

3.wait()与await() 区别

两种在表现上有点相似,但是作用机制是有区别的

  1. wait()来自Object await()来自condition

  2. object wait() 不能单独使用,必须是在synchronized 下才能使用

    为啥强制wait必须用在synchronized中?否则IllegalMonitorStateException

    (1)为什么wait()必须在同步(Synchronized)方法/代码块中调用?

    答:调用wait()就是释放锁,释放锁的前提是必须要先获得锁,先获得锁才能释放锁。

    (2)为什么notify(),notifyAll()必须在同步(Synchronized)方法/代码块中调用?

    答:notify(),notifyAll()是将锁交给含有wait()方法的线程,让其继续执行下去,如果自身没有锁,怎么叫把锁交给其他线程呢;
    ————————————————
    原文链接:https://blog.csdn.net/qq_42145871/article/details/81950949

  3. wait()必须要通过notify()方法进行唤醒

  4. await() 必须是当前线程被排斥锁 lock 后,,获取到condition 后才能使用

    条件变量的await()释放锁吗?

    await()的作用是能够让其他线程访问竞争资源,所以挂起状态就是要释放竞争资源的锁。 在javaSE5的java.util.concurrent类库中,使用互斥并允许任务挂起的基本类就是Condition,你可以通过await()来挂起一个任务,当外部条件改变时,意味着某个任务可以继续执行,你可以通过signal()来通知这个任务。

    每个lock()的调用必须紧跟一个try-finally子句,用来保证在所有情况下都可以释放锁。任务在可以调用await(),signal(),signalAll()之前,必须拥有这个锁

  5. condition await() 必须通过 signal() 方法进行唤醒

4.使用condition实现精准唤醒线程

用condition搭配状态位置试试

public class PC_WithAwakeByCondition {
    public static void main(String[] args) {
        Data_AwakeInOrder data = new Data_AwakeInOrder();

        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                data.printA();
            }
        },"线程A").start();
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                data.printB();
            }
        },"线程B").start();
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                data.printC();
            }
        },"线程C").start();
    }
}

//资源类
class Data_AwakeInOrder{
    private Lock lock = new ReentrantLock();
    private Condition condition1 = lock.newCondition();
    private Condition condition2 = lock.newCondition();
    private Condition condition3 = lock.newCondition();
    private int number = 1;

    public void printA(){
        lock.lock();
        try {
        // 业务,判断-> 执行-> 通知
            while (number != 1) {
            // 等待
                condition1.await();
            }
            System.out.println(Thread.currentThread().getName() + "=>AAAAAA"+"-----number为->"+number);
            // 唤醒,唤醒指定的人,B
            number = 2;
            condition2.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void printB(){
        lock.lock();
        try {
            // 业务,判断-> 执行-> 通知
            while (number != 2) {
                // 等待
                condition2.await();
            }
            System.out.println(Thread.currentThread().getName() + "=>BBBBBB"+"-----number为->"+number);
            // 唤醒,唤醒指定的人C
            number = 3;
            condition3.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
    public void printC(){
        lock.lock();
        try {
            // 业务,判断-> 执行-> 通知
            while (number != 3) {
                // 等待
                condition3.await();
            }
            System.out.println(Thread.currentThread().getName() + "=>CCCCCC"+"-----number为->"+number);
            // 唤醒,唤醒指定的人A
            number = 1;
            condition1.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

运行结果:😁一片祥和~依序执行

...
线程A=>AAAAAA-----number为->1
线程B=>BBBBBB-----number为->2
线程C=>CCCCCC-----number为->3
线程A=>AAAAAA-----number为->1
线程B=>BBBBBB-----number为->2
线程C=>CCCCCC-----number为->3
...

看视频的时候看到有人用wait和notify 也实现了精准唤醒,接下来稍作尝试

5.使用wait()与notify()实现精准唤醒

public class PC_AwakeWithWait {
    public static void main(String[] args) {
        Data_AwakeInOrderByWait data = new Data_AwakeInOrderByWait();

        new Thread(()->{
            for (int i = 0; i < 100; i++) {
                data.printA();
            }
        },"T_A").start();
        new Thread(()->{
            for (int i = 0; i < 100; i++) {
                data.printB();
            }
        },"T_B").start();
        new Thread(()->{
            for (int i = 0; i < 100; i++) {
                data.printC();
            }
        },"T_C").start();
    }
}

//资源类
class Data_AwakeInOrderByWait{

    private int number = 1;

    public synchronized void printA() {
        // 业务,判断-> 执行-> 通知
        while (number != 1) {
            // 等待
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(Thread.currentThread().getName() + "=>AAAAAA->"+number);
        // 唤醒,唤醒指定的人,B
        number = 2;
        this.notifyAll();
    }

    public synchronized void printB(){
        // 业务,判断-> 执行-> 通知
        while (number != 2) {
            // 等待
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(Thread.currentThread().getName() + "=>BBBBBB->"+number);
        // 唤醒,唤醒指定的人,B
        number = 3;
        this.notifyAll();

    }
    public synchronized void printC(){
        // 业务,判断-> 执行-> 通知
        while (number != 3) {
            // 等待
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(Thread.currentThread().getName() + "=>CCCCCC->"+number);
        // 唤醒,唤醒指定的人,B
        number = 1;
        this.notifyAll();
    }
}

结果也确实可以实现精准唤醒:

...
T_A=>AAAAAA->1
T_B=>BBBBBB->2
T_C=>CCCCCC->3
T_A=>AAAAAA->1
T_B=>BBBBBB->2
T_C=>CCCCCC->3
...

后来的技术是迭代出的更优秀的版本。等学成归来对比下这两种唤醒的效率

5.8个代码加锁的例子

用4套八种加锁的例子对比理解锁的本质

  • 情况一:实例化一个对象

    连续十次执行线程,A/B两条线程交替执行

    public class Test1 {
        public static void main(String[] args) {
    
            Phone phone = new Phone();
            for (int i = 0; i < 10; i++) {
                //锁的存在
                new Thread(() -> {
                    phone.sendSms();
                }, "A").start();
    
                new Thread(() -> {
                    phone.call();
                }, "B").start();
            }
        }
    }
    
    class Phone {
        public synchronized void sendSms() {
            System.out.println("发短信###");
        }
        public synchronized void call() {
            System.out.println("打电话******");
        }
    }
    
    ...
    发短信###
    打电话******
    打电话******
    打电话******
    发短信###
    ...
    
    • 在线程执行的过程中增加sleep()休眠(Phone类不变)

      public static void main(String[] args) {
              Phone phone = new Phone();
              for (int i = 0; i < 10; i++) {
                  //锁的存在
                  new Thread(() -> {
                      phone.sendSms();
                  }, "A").start();
      
                  //增加0.5s的休眠时间
                  try {
                      TimeUnit.MILLISECONDS.sleep(500);
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
                  
                  new Thread(() -> {
                      phone.call();
                  }, "B").start();
              }
          }
      
      发短信###
      打电话******
      发短信###
      打电话******
      
      • 结果:

        发短信—>打电话

        依次执行

    • 在Phone的 发短信的方法中增加2s的休眠(main方法不变)

      class Phone {
          public synchronized void sendSms() {
              try {
                  TimeUnit.SECONDS.sleep(2);
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
              System.out.println("发短信###");
          }
      
          public synchronized void call() {
              System.out.println("打电话******");
          }
      }
      
      • 结果:

        发短信—>打电话

        依次执行

    • 总结

      • synchronized 锁的对象是方法的调用者,也就是实例化后的对象。
      • 两个方法用的是同一个锁,谁先拿到谁执行!
        • 在例子中,线程的调度是随机的,但是由于实例抱着🔒休眠,因此线程极大可能(几乎一定)会先执行首个即发短信方法
  • 情况二:实例化一个对象

    • 两个对象,两个调用者,两把🔒

    • 两个问题:

      • 3. 增加了一个普通方法后!先执行发短信还是Hello? 普通方法
        4. 两个对象,两个同步方法, 发短信还是 打电话? // 打电话
        
    • 例子:

      public class Test2  {
          public static void main(String[] args) {
              // 两个对象,两个调用者,两把锁!
              Phone2 phone1 = new Phone2();
              Phone2 phone2 = new Phone2();
      
              //锁的存在
              new Thread(phone1::sendSms,"A").start();
      
              new Thread(phone1::hello,"C1").start();
              try {
                  TimeUnit.SECONDS.sleep(1);
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
      
              new Thread(phone2::call,"B").start();
      
              new Thread(phone2::hello,"C2").start();
          }
      }
      
      class Phone2{
          public synchronized void sendSms(){
              try {
                  TimeUnit.SECONDS.sleep(2);
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
              System.out.println("发短信sssss <--"+Thread.currentThread().getName());
          }
      
          public synchronized void call(){
              System.out.println("打电话cccc <--"+Thread.currentThread().getName());
          }
      
          public void hello(){
              System.out.println("hello <--"+Thread.currentThread().getName());
          }
      }
      
      • 运行结果(多次)

        hello <--C1
        打电话cccc <--B
        hello <--C2
        发短信sssss <--A
        
    • 总结:

      • synchronized 锁的对象是方法的调用者,也就是实例化后的对象。
      • 普通方法不会上🔒,因此会首先打印hello
      • A 线程调用 sendSms 时延时了 2s,因此会先调用B线程的发短信方法
  • 情况三:静态方法

    • 问题:

      5.增加两个静态的同步方法,只有一个对象,先打印 发短信?打电话?
      6.两个对象!增加两个静态的同步方法, 先打印 发短信?打电话?
      
    • 例子1(实例化一个对象)

      public class Test3  {
          public static void main(String[] args) {
              Phone3 phone1 = new Phone3();
              new Thread(()->{
                  phone1.sendSms();
              },"A").start();
              try {
                  TimeUnit.SECONDS.sleep(1);
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
              new Thread(()->{
                  phone1.call();
              },"B").start();
          }
      }
      class Phone3{
          public static synchronized void sendSms(){
              try {
                  TimeUnit.SECONDS.sleep(2);
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
              System.out.println("发短信");
          }
          public static synchronized void call(){
              System.out.println("打电话");
          }
      }
      
      • 运行结果:

        发短信 -->打电话

    • 例子2:(实例化两个对象)

      Phone3 phone1 = new Phone3();
      Phone3 phone2 = new Phone3();
      new Thread(()->{
          phone1.sendSms();
      },"A").start();
      try {
          TimeUnit.SECONDS.sleep(1);
      } catch (InterruptedException e) {
          e.printStackTrace();
      }
      //另一个实例化对象
      new Thread(()->{
          phone2.call();
      },"B").start();
      
      • 运行结果:

        发短信 -->打电话

    • 总结:

      • 两个对象的Class类模板只有一个,static方法时在类加载的时候初始化,因此锁的是Class对象
      • 在休眠后(极大可能)调用发短信对象,同时对模板上🔒,因此依次打印
  • 情况四:静态与普通方法

    • 问题:

      7.1个静态的同步方法,1个普通的同步方法 ,一个对象,先打印 发短信?打电话?
      8.1个静态的同步方法,1个普通的同步方法 ,两个对象,先打印 发短信?打电话?
      
    • 例子7:

      public class Test4  {
          public static void main(String[] args) {
              Phone4 phone1 = new Phone4();
              new Thread(()->{
                  phone1.sendSms();
              },"A").start();
              try {
                  TimeUnit.SECONDS.sleep(1);
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
              new Thread(()->{
                  phone1.call();
              },"B").start();
          }
      }
      class Phone4{
          public static synchronized void sendSms(){
              try {
                  TimeUnit.SECONDS.sleep(2);
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
              System.out.println("发短信");
          }
          public synchronized void call(){
              System.out.println("打电话");
          }
      }
      
      • 运行结果:
        • 打电话 --> 发短信
    • 例子8:

              Phone4 phone1 = new Phone4();
              Phone4 phone2 = new Phone4();
              new Thread(()->{
                  phone1.sendSms();
              },"A").start();
              try {
                  TimeUnit.SECONDS.sleep(1);
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
              new Thread(()->{
                  phone2.call();
              },"B").start();
      
      • 运行结果:
        • 打电话 --> 发短信
  • 总结:

    • 普通方法 🔒的是对象
    • static 方法 🔒的是模板

双冒号表达式:

插播一个知识点,双冒号(::)表达式

详见:JAVA 8 ‘::’ 关键字,带你深入了解它!

语法种类示例
引用静态方法ContainingClass::staticMethodName
引用特定对象的实例方法containingObject::instanceMethodName
引用特定类型的任意对象的实例方法ContainingType::methodName
引用构造函数ClassName::new

举例:

//表达式转换
 new Thread(() -> {
                phone.call();
            }, "B").start();
//可以转换成:
new Thread(phone::call,"B").start();

6.集合不安全类

  • List不安全

    狂神多线程 的 三个线程不安全的案例中有提及,今天在并发角度测试下

    public class TestList {
        public static void main(String[] args) {
            List<String> list = new ArrayList<>();
            for (int i = 0; i < 10; i++) {
                new Thread(()->{
                 list.add(UUID.randomUUID().toString().substring(0,5));
                 System.out.println(list);
                },String.valueOf(i)).start();
            }
        }
    }
    

    执行程序后会抛出:

    java.util.ConcurrentModificationException异常(后续抛出同样异常不做细说)

    在这里插入图片描述

    也就是不同线程同时操作了同一list索引元素抛出的异常。

    解决方法:

    public static void main(String[] args) {
        //        List<String> list = new ArrayList<>();
        //1.集合自带的线程安全的list
        //        List<String> list = new Vector<>();
    
        //2.Collections工具类强行上锁
        //        List<String> list =Collections.synchronizedList(new ArrayList<>());
    
        //3.用JUC包下的读写数组CopyOnWriteArrayList,读写分离
        List<String> list = new CopyOnWriteArrayList<>();
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                list.add(UUID.randomUUID().toString().substring(0, 5));
                System.out.println(list);
            }, String.valueOf(i)).start();
        }
    }
    
  • Set不安全

    例子:

    public class TestSet {
        public static void main(String[] args) {
            Set<Object> set = new HashSet<>();
    
            for (int i = 0; i < 10; i++) {
                new Thread(() -> {
                    set.add(UUID.randomUUID().toString().substring(0, 5));
                    System.out.println(set);
                }, String.valueOf(i)).start();
            }
        }
    }
    

    解决方法:

    public static void main(String[] args) {
        //        Set<Object> set = new HashSet<>();
        //1.用collections工具类强行上锁:
        //        Set<Object> set = Collections.synchronizedSet(new HashSet<>());
    
        //2.用CopyOnWriteArrayList 实现的CopyOnWriteArraySet
        Set<String> set  = new CopyOnWriteArraySet<>();
    
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                set.add(UUID.randomUUID().toString().substring(0, 5));
                System.out.println(set);
            }, String.valueOf(i)).start();
        }
    }
    

    在这里插入图片描述

    • hashSet本质:

      public HashSet() {
      map = new HashMap<>();
      }
      
      // add set 本质就是 map key是无法重复的!
      public boolean add(E e) {
      return map.put(e, PRESENT)==null;
      }
      
      private static final Object PRESENT = new Object(); // 不变的值,做value
      
  • Map不安全

    例子:

    public class TestMap {
        public static void main(String[] args) {
            // 默认等价于什么? new HashMap<>(16,0.75);
            // Map<String, Object> map = new HashMap<>();
            //1.用juc下线程安全的map
            Map<String, Object> map = new ConcurrentHashMap<>();
            for (int i = 0; i < 10; i++) {
                new Thread(() -> {
                    map.put(Thread.currentThread().getName(),UUID.randomUUID().toString().substring(0, 5));
                    System.out.println(map);
                }, String.valueOf(i)).start();
            }
        }
    }
    

    在这里插入图片描述

7.Callable使用

下列博客对callable有详细的介绍,值得一看✌

Callable接口及Futrue接口详解

目录

有两种创建线程的方法-一种是通过创建Thread类,另一种是通过使用Runnable创建线程。但是,Runnable缺少的一项功能是,当线程终止时(即run()完成时),我们无法使线程返回结果。为了支持此功能,Java中提供了Callable接口。

  • 为了实现Runnable,需要实现不返回任何内容的run()方法,
  • 对于Callable,需要实现在完成时返回结果的call()方法。
    • 请注意,不能使用Callable创建线程,只能使用Runnable创建线程。
  • 另一个区别是call()方法可以引发异常,而run()则不能。
  • 为实现Callable而必须重写call()方法。
public class CallableFutureTest {
    public static void main(String[] args) throws Exception {

        // FutureTask is a concrete class that implements both Runnable and Future
        FutureTask[] randomNumberTasks = new FutureTask[5];

        for (int i = 0; i < 5; i++) {
            Callable callable = new CallableExample();
            // Create the FutureTask with Callable
            randomNumberTasks[i] = new FutureTask(callable);  //适配器模式
            // As it implements Runnable, create Thread with FutureTask
            Thread t = new Thread(randomNumberTasks[i], String.valueOf(i));
            t.start();  //结果会被缓存,效率高
        }

        for (int i = 0; i < 5; i++) {
            // As it implements Future, we can call get()
            System.out.println(randomNumberTasks[i].get());
            // This method blocks till the result is obtained
            // The get method can throw checked exceptions
            // like when it is interrupted. This is the reason
            // for adding the throws clause to main
        }
    }
}
class CallableExample implements Callable {

    public Object call() throws Exception {
        Random generator = new Random();
        Integer randomNumber = generator.nextInt(5);
        TimeUnit.SECONDS.sleep(1);
        return randomNumber;
    }
}

8. JUC常用辅助类

8.1 CountDownLatch

顾名思义:倒计时锁存器

不管你线程中间执行的情况,结果若是线程执行完了,那就再执行最后语句,如果没达到条件就一直等

(领导:不看过程只看结果)

public class CountDownLatchDemo {
    public static void main(String[] args) throws InterruptedException {
        // 总数是6,必须要执行任务的时候,再使用!
        CountDownLatch countDownLatch = new CountDownLatch(6);
        for (int i = 1; i <=6 ; i++) {
            new Thread(()->{
                System.out.println(Thread.currentThread().getName()+" Go out");
                countDownLatch.countDown(); // 数量-1
            },String.valueOf(i)).start();
        }
        countDownLatch.await(); // 等待计数器归零,然后再向下执行
        System.out.println("Close Door");
    }
}
1 Go out
5 Go out
4 Go out
3 Go out
2 Go out
6 Go out
Close Door

如果创建了7条任务线程,但只countDown了6次,那么将会一直阻塞线程

总结:

CountDownLatch countDownLatch = new CountDownLatch(6); 创建线程总数

countDownLatch.countDown(); 实行完线程数-1

countDownLatch.await(); 等待计数器归零,然后再向下执行
每次有线程调用 countDown() 数量-1,假设计数器变为0,countDownLatch.await() 就会被唤醒,继续执行!

8.2 CyclicBarrier

见名之意:循环障碍,与8.1 方法相反的,加法计时器

public class CyclicBarrierDemo {
    public static void main(String[] args) {

        /**
         * 集齐7颗龙珠召唤神龙
         */
        // 召唤龙珠的线程:7条线程
        // 创建成功后执行runnable接口打印“召唤七颗龙珠成功”
        CyclicBarrier cyclicBarrier = new CyclicBarrier(7, () -> {
            System.out.println("召唤七颗龙珠成功");
        });
        for (int i = 1; i <= 7; i++) {
            final int temp = i;
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + "收集到第" + temp + "个龙珠");

                try {
                    cyclicBarrier.await();
                } catch (InterruptedException | BrokenBarrierException e){
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

创建线程Barrier后开启线程执行:

CyclicBarrier(int parties, Runnable barrierAction) 
创建一个新的 CyclicBarrier ,当给定数量的线程(线程)等待时,它将跳闸,当屏障跳闸时执行给定的屏障动作,由最后一个进入屏障的线程执行。

并在调用await()方法时自动+1:

  • 如果执行的线程数到达设定值,则会执行 创建时设定的屏障动作,
  • 如果无法到达则线程会处在阻塞状态
8.2.1 lambda为什么要用final?

原创来源: lambda里面赋值局部变量必须是final原因

简单复习下lambda函数:匿名实现函数式接口的方法

Runnable r= new Runnable() {
    @Override
    public void run() {
        System.out.println("这是常用实例化函数式接口");
    }
};

Runnable rl = ()->System.out.println("这是lambda实例化接口");

四、为什么lambda里面要访问局部变量必须是final
现在再来解释为什么lambda里面要访问局部变量必须是final?

首先:lambda表示可以无限制捕获实例变量(即表达式主体中的引用)和静态变量

但是,局部变量必须是显示声明为final或事实上是final

那么这lambda里面要赋值局部变量必须是final有什么关系?

实例变量:存储在堆中

局部变量:则保存在栈上

lambda表达式以内联的形式创建一个函数式接口的实例,保存在堆中,而局部变量则保存在栈中,可能造成实例对象得生命周期很有可能超过局部变量得生命周期

总结:

Lambda可以直接访问局部变量,而且Lambda是在一个线程中使用的,则使用Lambda的线程,可能会在分配该变量的线程将这个变量收回之后,去访问该变量。因此,Java在访问自由局部变量时,实际上是在访问它的副本,而不是访问原始变量,因此需要final作限制

//      int k = 1;
//      Runnable rl = ()->System.out.println("这是lambda实例化接口"+k);//正常

//      int k = 1;
//      Runnable rl = ()->System.out.println("这是lambda实例化接口"+k);//Variable used in lambda expression should be final or effectively final
//        k=3; 

final int k = 1;
Runnable rl = ()->System.out.println("这是lambda实例化接口"+k);

8.3 Semaphore

见名之意:信号量,可近似看作资源池。

举例子(抢车位):

public class SemaphoreDemo {
    public static void main(String[] args) {
        // 线程数量:停车位! 限流!
        //创建只有3个停车位的停车场
        Semaphore semaphore = new Semaphore(3);

        for (int i = 1; i <=6 ; i++) {
            new Thread(()->{
                // acquire() 得到
                try {
                    //抢车位。。。
                    semaphore.acquire();
                    System.out.println(Thread.currentThread().getName()+"抢到车位👍");
                    TimeUnit.SECONDS.sleep(2);
                    System.out.println(Thread.currentThread().getName()+"离开车位😀");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {//停车结束,离开车位
                    semaphore.release(); // release() 释放
                }
            },String.valueOf(i)+"号车-->").start();
        }

    }
}
1号车-->抢到车位👍
3号车-->抢到车位👍
2号车-->抢到车位👍
2号车-->离开车位😀
1号车-->离开车位😀
4号车-->抢到车位👍
3号车-->离开车位😀
6号车-->抢到车位👍
5号车-->抢到车位👍
5号车-->离开车位😀
6号车-->离开车位😀
4号车-->离开车位😀

每个人都能停到车🚘

9.读写锁

ReadWriteLock 接口

所有已知实现类: ReentrantReadWriteLock

  • 读:可多条线程同时获取数据
  • 写:只能单条线程写入
public class TestReadWriteLock {
    public static void main(String[] args) {
        //未上锁:
        //        MyCache myCache = new MyCache();
        //上了读写锁:
        MyCacheWithLock myCache = new MyCacheWithLock();
        //写入:
        for (int i = 1; i <= 5; i++) {
            final int temp = i;
            new Thread(() -> {
                myCache.write(temp + "", temp + "");
            }, String.valueOf(i)).start();
        }

        for (int i = 1; i <= 5; i++) {
            final int temp = i;
            new Thread(() -> {
                myCache.read(temp + "");
            }, String.valueOf(i)).start();
        }
    }
}

class MyCacheWithLock {
    private volatile Map<String, Object> map = new HashMap<>();

    //读写锁:对数据更精准控制
    private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    private Lock lock = new ReentrantLock();

    //写数据:只希望有一个线程在执行
    public void write(String key, Object value) {
        readWriteLock.writeLock().lock();

        try {
            System.out.println(Thread.currentThread().getName() + "写入" + key);
            map.put(key, value);
            System.out.println(Thread.currentThread().getName() + "写入完成!");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            readWriteLock.writeLock().unlock();
        }
    }

    //读数据:可一条或者多条同时执行
    public void read(String key) {
        readWriteLock.readLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + "读取数据:" + key);
            Object o = map.get(key);
            System.out.println(Thread.currentThread().getName() + "读取数据完成-->" + o);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            readWriteLock.readLock().unlock();
        }
    }
    /**
     * 存入数据过程上锁,安全
     */
}


/**
 * 未上锁:
 */
class MyCache {
    private volatile Map<String, Object> map = new HashMap<>();

    //写数据:
    public void write(String key, Object value) {
        System.out.println(Thread.currentThread().getName() + "写入" + key);
        map.put(key, value);
        System.out.println(Thread.currentThread().getName() + "写入完成!");
    }

    //读数据:
    public void read(String key) {
        System.out.println(Thread.currentThread().getName() + "读取数据:" + key);
        Object o = map.get(key);
        System.out.println(Thread.currentThread().getName() + "读取数据完成-->" + o);
    }
    /**
 * 运行结果:
 * 写入线程会被 读取线程中断,造成脏读,对数据不安全
 */
}

10.阻塞队列

Interface BlockingQueue

是数据结构中队列Queue的延展使用。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

什么情况下我们会使用阻塞队列?

对线程并发处理,线程池!

学会使用队列的操作: 添加 和 移除

四组API

第一组第二组第三组第四组
实现结果有返回值会抛异常的方法有返回值但不抛异常的方法线程会阻塞和等待的方法(一直等待)超时等待的方法(过时不候)
添加元素add(E e)offer(E e)put(E e)(无返回值)offer(E e, long timeout, TimeUnit unit)
移除元素remove()poll()take()poll(long timeout, TimeUnit unit)
检测队首元素element()peek()无。上述两个方法会throws InterruptedException无。上述两个方法会throws InterruptedException

第一组:

@Test
public void test1(){
    ArrayBlockingQueue<Object> blockingQueue = new ArrayBlockingQueue<>(3);
    System.out.println(blockingQueue.add("a")); //true
    System.out.println(blockingQueue.add("b")); // true
    System.out.println(blockingQueue.add("c"));  //true
    //        System.out.println(blockingQueue.add("d"));
    //      会抛异常:java.lang.IllegalStateException: Queue full

    System.out.println("=============================");
    System.out.println(blockingQueue.remove()); //a
    System.out.println(blockingQueue.remove()); //b
    System.out.println(blockingQueue.remove()); //c
    //        System.out.println(blockingQueue.remove()); 		  
    //会抛出 NoSuchElementException异常
    System.out.println(blockingQueue.element()); //如无元素则会报NoSuchElementException异常
}

第二组:

@Test
public void test2(){
    ArrayBlockingQueue<Object> blockingQueue = new ArrayBlockingQueue<>(3);
    System.out.println(blockingQueue.offer("a"));//true
    System.out.println(blockingQueue.offer("b"));//true
    System.out.println(blockingQueue.offer("c"));//true
    System.out.println(blockingQueue.offer("d"));//false

    System.out.println("=============================");
    System.out.println(blockingQueue.poll()); //a
    System.out.println(blockingQueue.poll()); //b
    System.out.println(blockingQueue.poll()); //c
    System.out.println(blockingQueue.poll()); //null

    System.out.println("=============================");
    System.out.println(blockingQueue.offer("e"));//true
    System.out.println(blockingQueue.offer("f"));//true
    System.out.println(blockingQueue.peek());       //e  : 队首元素

}

第三组:

@Test
public void test3() throws InterruptedException {
    ArrayBlockingQueue<Object> blockingQueue = new ArrayBlockingQueue<>(3);
    blockingQueue.put("a");
    blockingQueue.put("b");
    blockingQueue.put("c");
    //        blockingQueue.put("d"); // 队列没有位置了,一直阻塞

    System.out.println("=============================");
    System.out.println(blockingQueue.take()); //a
    System.out.println(blockingQueue.take()); //b
    System.out.println(blockingQueue.take()); //c
    //        System.out.println(blockingQueue.take()); // 队列没有值可以取,一直阻塞

}

第四组:

@Test
public void test4() throws InterruptedException {
    ArrayBlockingQueue<Object> blockingQueue = new ArrayBlockingQueue<>(3);
    System.out.println(blockingQueue.offer("a"));//true
    System.out.println(blockingQueue.offer("b"));//true
    System.out.println(blockingQueue.offer("c"));//true
    System.out.println(blockingQueue.offer("d", 2, TimeUnit.SECONDS));//false

    System.out.println("=============================");
    System.out.println(blockingQueue.poll()); //a
    System.out.println(blockingQueue.poll()); //b
    System.out.println(blockingQueue.poll()); //c
    System.out.println(blockingQueue.poll(2,TimeUnit.SECONDS)); //null

}

SynchronousQueue 同步队列

没有容量==> 进去一个元素,必须等待取出来之后,才能再往里面放一个元素!

和其他的BlockingQueue 不一样, SynchronousQueue 不存储元素,put了一个元素,必须从里面先take取出来,否则不能再put进去值!

public class TestSynchronousQueue {
    public static void main(String[] args) {
        SynchronousQueue<String> bq = new SynchronousQueue<>();
        new Thread(() -> {
            try {
                System.out.println(Thread.currentThread().getName() + " put 1");
                bq.put("1");
                System.out.println(Thread.currentThread().getName() + " put 2");
                bq.put("2");
                System.out.println(Thread.currentThread().getName() + " put 3");
                bq.put("3");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "T1").start();
        new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(1);
                System.out.println(Thread.currentThread().getName() + " get =>" + bq.take());
                TimeUnit.SECONDS.sleep(1);
                System.out.println(Thread.currentThread().getName() + " get =>" + bq.take());
                TimeUnit.SECONDS.sleep(1);
                System.out.println(Thread.currentThread().getName() + " get =>" + bq.take());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "T2").start();
    }
}
/**
T1 put 1
T2 get =>1
T1 put 2
T2 get =>2
T1 put 3
T2 get =>3
*/

11.线程池(※)

学习目标:

  • 三大方法
  • 七大参数
  • 四种拒绝策略

11.0 池化技术

在系统开发过程中,我们经常会用到池化技术来减少系统消耗,提升系统性能。

说人数: 简单点来说,就是提前保存大量的资源,以备不时之需,池化技术就是通过复用来提升性能。

常见池

  • 对象池
    • 通过复用对象来减少创建对象、垃圾回收的开销;
  • 连接池
    • (数据库连接池、Redis连接池和HTTP连接池等)通过复用TCP连接来减少创建和释放连接的时间
  • 线程池通过复用线程提升性能

使用内存池的优点

  1. 降低资源消耗。这个优点可以从创建内存池的过程中看出,当我们在创建内存池的时候,分配的都是一块块比较规整的内存块,减少内存碎片的产生。
  2. 提高相应速度。这个可以从分配内存和释放内存的过程中看出。每次的分配和释放并不是去调用系统提供的函数或操作符去操作实际的内存,而是在复用内存池中的内存。
  3. 方便管理。

使用内存池的缺点

缺点就是很可能会造成内存的浪费,因为要使用内存池需要在一开始分配一大块闲置的内存,而这些内存不一定全部被用到。

11.1 三大方法

阿里巴巴 Java 开发手册中 对线程池的规范:

在这里插入图片描述

Executors 工具类中3大方法(详见API)

  • public static ExecutorService newSingleThreadExecutor()
        //创建一个线程池,根据需要创建新的线程,但在可用时将重用先前构建的线程。
    
  • public static ExecutorService newFixedThreadPool(int nThreads)
    //创建一个线程池,使用固定数量的线程操作了共享无界队列
    
  • public static ExecutorService newCachedThreadPool()
        //创建一个线程池,根据需要创建新的线程,但在可用时将重用先前构建的线程。
    
public class TestMethods {
    public static void main(String[] args) {
        //        ExecutorService threadPool = Executors.newSingleThreadExecutor();
        // 单个线程:此时只有pool-1-thread-1
        //         ExecutorService threadPool = Executors.newFixedThreadPool(5);
        // 创建一个固定的线程池的大小: 此时最多有pool-1-thread-5 ok
        ExecutorService threadPool = Executors.newCachedThreadPool(); 
        // 可伸缩的,遇强则强,遇弱则弱 : 此时最多开启到pool-1-thread-31 ok 去执行

        try {
            for (int i = 0; i < 100; i++) {
                // 使用了线程池之后,使用线程池来创建线程
                threadPool.execute(() -> {
                    System.out.println(Thread.currentThread().getName() + " ok");
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 线程池用完,程序结束,关闭线程池
            threadPool.shutdown();
        }
    }
}

11.2 七大参数

源码分析:

//创建单一线程的线程池
public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}
//创建固定线程数的线程池
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>(),
                                  threadFactory);
}
//创建代缓存的线程池:
public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}

其本质都是调用本质ThreadPoolExecutor创建线程池,也是 阿里巴巴规范中提及的方法:

public ThreadPoolExecutor(int corePoolSize,  //核心线程池大小
                          int maximumPoolSize, //最大核心线程池大小
                          long keepAliveTime, //超时了没有人调用就会释放
                          TimeUnit unit,  //超时单位
                          BlockingQueue<Runnable> workQueue,//阻塞队列
                          ThreadFactory threadFactory,// 线程工厂:创建线程的,一般不用动
                          RejectedExecutionHandler handler/*拒绝策略*/) {
    if (corePoolSize < 0 ||
        maximumPoolSize <= 0 ||
        maximumPoolSize < corePoolSize ||
        keepAliveTime < 0)
        throw new IllegalArgumentException();
    if (workQueue == null || threadFactory == null || handler == null)
        throw new NullPointerException();
    this.acc = System.getSecurityManager() == null ?
        null :
    AccessController.getContext();
    this.corePoolSize = corePoolSize;
    this.maximumPoolSize = maximumPoolSize;
    this.workQueue = workQueue;
    this.keepAliveTime = unit.toNanos(keepAliveTime);
    this.threadFactory = threadFactory;
    this.handler = handler;
}
11. 2.1 手动创建线程池:

在这里插入图片描述

银行办理业务举例:

  • 有5个柜台,每次办理一个人。
  • 当天值班柜员只有2人。
  • 剩余3个临时工等待超过一定时间会离开柜台。
  • 候客区有3个座位,可容纳3人等待。
public class TestDefPool {
    public static void main(String[] args) {
        // 自定义线程池!工作 ThreadPoolExecutor
        ExecutorService threadPool = new ThreadPoolExecutor(
            2,  //当天值班员工(核心线程池大小)
            5, //柜台总数:最大核心线程池大小
            3, //临时工的超时等待:超时了没有人调用就会释放
            TimeUnit.SECONDS, //超时等待单位
            new LinkedBlockingDeque<>(3),//候客区:阻塞队列
            Executors.defaultThreadFactory(),
            new ThreadPoolExecutor.DiscardOldestPolicy()); //满了后告诉客人办不了业务了:抛异常RejectedExecutionException
        try {
            // 最大承载:Deque + max 此处为:5+3=8
            // 超过 RejectedExecutionException
            for (int i = 1; i <= 9; i++) {
                // 使用了线程池之后,使用线程池来创建线程
                threadPool.execute(() -> {
                    System.out.println(Thread.currentThread().getName() + " ok");
                });
            }
        } catch (
            Exception e)

        {
            e.printStackTrace();
        } finally

        {
            // 线程池用完,程序结束,关闭线程池
            threadPool.shutdown();
        }
    }
}

11.4 四种拒绝策略

在这里插入图片描述

/**
1. new ThreadPoolExecutor.AbortPolicy() 
// 银行满了,还有人进来,不处理这个人的,抛出异常:RejectedExecutionException
 
2.new ThreadPoolExecutor.CallerRunsPolicy() // 哪来的去哪里!
//公司叫你来银行办业务,银行满人办不了,回公司找人办
//新开辟的线程搞不定调用主线程

3.new ThreadPoolExecutor.DiscardPolicy() 
//银行办不了了,不办你业务
//队列满了,丢掉任务,不会抛出异常!

4.new ThreadPoolExecutor.DiscardOldestPolicy() 
//排队人满了,你看看最早开始的客户搞定没,没搞定就被拒绝了。
//队列满了,尝试去和最早的竞争,也不会抛出异常!
*/

11.5小结:

根据上述参数对线程池调优:主要针对线程池大小调优

  • IO密集型

    一般来说:文件读写、DB读写、网络请求等。

    这类任务的特点是CPU消耗很少,任务的大部分时间都在等待IO操作完成(因为IO的速度远远低于CPU和内存的速度)。对于IO密集型任务,任务越多,CPU效率越高,但也有一个限度。常见的大部分任务都是IO密集型任务,比如Web应用。

  • CPU密集型

    一般来说:计算型代码、Bitmap转换、Gson转换等。

    这种计算密集型任务虽然也可以用多任务完成,但是任务越多,花在任务切换的时间就越多,CPU执行任务的效率就越低,所以,要最高效地利用CPU,计算密集型任务同时进行的数量应当等于CPU的核心数。

    // 获取CPU的核数
    System.out.println(Runtime.getRuntime().availableProcessors());
    

12.四大函数式接口

Java 1.8之前的程序员:泛型、枚举、反射

新时代的程序员:lambda表达式、链式编程、函数式接口、Stream流式计算

函数式接口:只有一个方法的接口(截至目前用的最多的是Runnable接口)

@FunctionalInterface
public interface Runnable {
public abstract void run();
}
// 超级多FunctionalInterface
// 简化编程模型,在新版本的框架底层大量应用!

四大函数式接口:

package java.util.function;
  • Consumer

    消费型接口

    在这里插入图片描述

    /**
     * Consumer 消费型接口: 只有输入,没有返回值
     */
    public class Demo03 {
        public static void main(String[] args) {
            //        Consumer<String> consumer = new Consumer<String>() {
            //            @Override
            //            public void accept(String str) {
            //                System.out.println(str);
            //            }
            //        };
            Consumer<String> consumer = (str)->{System.out.println(str);};
            //可以简化为:
            // “函数式接口 变量名 = 类实例::方法名” 的方式对该方法进行引用
            Consumer<String> consumer2 = System.out::println;
            consumer.accept("这是消费型接口----1");
            consumer2.accept("这是消费型接口----2");
        }
    }
    
双冒号

双冒号用法:Java8新特性之方法引用中的双冒号

System.out::print 与 System.out.print的区别

双冒号运算操作符是类方法的句柄:

lambda 表达式的一种简写,这种简写的学名叫 eta-conversion 或者叫 η-conversion。


双冒号运算就是 Java 中的[方法引用][方法引用]格式为:类名::方法名

//表达式1:
    person -> person.getAge();
//可以替换成1`:
    Person::getAge

//表达式2:
    () -> new HashMap<>();
//可以替换成2`
    HashMap::new
  • Function

    在这里插入图片描述

    /**
     * Function 函数型接口, 有一个输入参数,有一个输出
     * 只要是 函数型接口 可以 用 lambda表达式简化
     */
    public class Demo01 {
        public static void main(String[] args) {
            //
    //        Function<String,String> function = new Function<String,String>() {
    //            @Override
    //            public String apply(String str) {
    //                return str;
    //            }
    //        };
    
            Function<String,String> function = str-> str;
    
            System.out.println(function.apply("This is Function"));
        }
    }
    
  • Predicate

    断定型接口:有一个输入参数,返回值只能是 布尔值

    在这里插入图片描述

    /**
     * 断定型接口:有一个输入参数,返回值只能是 布尔值!
     */
    public class Demo02 {
        public static void main(String[] args) {
            // 判断字符串是否为空
            //        Predicate<String> predicate = new Predicate<String>(){
            //            @Override
            //            public boolean test(String str) {
            //                return str.isEmpty();
            //            }
            //        };
    
            Predicate<String> predicate = String::isEmpty;
            System.out.println(predicate.test(""));//true
            System.out.println(predicate.test("Test"));//false
        }
    }
    
  • Supplier

    供给型接口

    在这里插入图片描述

    /**
     * Supplier 供给型接口 没有参数,只有返回值
     */
    public class Demo04 {
        public static void main(String[] args) {
    //        Supplier supplier = new Supplier<Integer>() {
    //            @Override
    //            public Integer get() {
    //                System.out.println("get()");
    //                return 1024;
    //            }
    //        };
            Supplier supplier = ()-> 1024;
            System.out.println(supplier.get());
        }
    }
    

13. Stream流式计算

详见文章:JAVA流式计算

流的简单介绍

Java 8 中,引入了流(Stream)的概念,利用提供的Stream API,我们可以方便的操作集合数据,这种方式很类似于使用SQL对数据库的操作。

如何生成流

利用Stream API

1、所有继承自Collection的接口都可以直接转化为流:

List<Integer> l1 = Arrays.asList(1, 2, 3, 4, 5);
Stream<Integer> stream = l1.stream();

Map<String,Student> s1 = new HashMap<>();
Stream<Map.Entry<String, Student>> stream1 = s1.entrySet().stream();

2、利用Arrays类中的stream()方法:

Integer[] i1 = new Integer[]{1,2,3,4,5};  
Stream<Integer> stream = Arrays.stream(i1);

3、使用Stream类中的静态方法:

Stream<Integer> stream = Stream.of(1,2,3,4,5);

4、利用BufferedReader读取文本中的内容转化为流:

BufferedReader reader = new BufferedReader(new FileReader("D:\\stream.txt"));  
Stream<String> stream = reader.lines();

我们经常使用的还是方式1,将集合转化为流。

举例

大数据:存储 + 计算
集合、MySQL 本质就是存储东西的;
计算都应该交给流来操作!

/**
 * 题目要求:一分钟内完成此题,只能用一行代码实现!
 * 现在有5个用户!筛选:
 * 1、ID 必须是偶数
 * 2、年龄必须大于23岁
 * 3、用户名转为大写字母
 * 4、用户名字母倒着排序
 * 5、只输出一个用户!
 */
public class Test {
    public static void main(String[] args) {
        User u1 = new User(1,"a",21);
        User u2 = new User(2,"b",22);
        User u3 = new User(3,"c",23);
        User u4 = new User(4,"d",24);
        User u5 = new User(6,"e",25);
        // 集合就是存储
        List<User> list = Arrays.asList(u1, u2, u3, u4, u5);

        // 计算交给Stream流
        // lambda表达式、链式编程、函数式接口、Stream流式计算
        list.stream()
            .filter(u->{return u.getId()%2==0;})
            .filter(u->{return u.getAge()>23;})
            .map(u->{return u.getName().toUpperCase();})
            .sorted((uu1,uu2)->{return uu2.compareTo(uu1);})
            .limit(1)
            .forEach(System.out::println);

        //上述操作可以简写为:
        list.stream()
            .filter(u-> u.getId()%2==0)
            .filter(u-> u.getAge()>23)
            .map(u-> u.getName().toUpperCase())
            .sorted(Comparator.reverseOrder())
            .limit(1)
            .forEach(System.out::println);
    }
}

涉及方法说明:

  1. filter()

    Stream<T> filter(Predicate<? super T> predicate)
    
    //用于对数据进行过滤,筛选出符合条件的数据;  
    //接收一个Predicate的函数接口,用于进行条件的过滤;返回符合条件的数据组成的一个新的流;
    
  2. map()

    <R> Stream<R> map(Function<? super T, ? extends R> mapper);
    
    //对数据进行处理,将T类型的对象转化为R类型的对象,简单来说就是对流中的数据进行同一项操作;  //接收一个Function的函数接口,用于对数据处理;返回R类型的数据组成的一个新的流;
    
  3. sorted()

    Stream<T> sorted();  
    Stream<T> sorted(Comparator<? super T> comparator);
    
    //将流中的数据进行排序,然后排序后的数据组合成一个新的流;  
    //无参的方法,默认按照升序升序进行排列;  
    //有参的方法,需要传入Comparator接口的一个实现类,按照该实现进行排序操作;
    
  4. limit()

    Stream<T> limit(long maxSize)
    //返回由该流的元素组成的流,截断长度不能超过maxSize 。 
    
  5. foreach()

    void forEach(Consumer<? super T> action)
    //对此流的每个元素执行操作。    
    /**最后的System.out::println打印的是传入的参数,
    也就是Customer的参数泛型T,这个T就是从list的流对象获取的T,最终都是List对象创建时指定的泛型*/
    

14. Fork/Join

从JDK1.7开始,Java提供Fork/Join框架用于并行执行任务,它的思想就是讲一个大任务分割成若干小任务,最终汇总每个小任务的结果得到这个大任务的结果。

这种思想和MapReduce很像(input --> split --> map --> reduce --> output)

相关文章:

我们举个例子:如果要计算一个超大数组的和,最简单的做法是用一个循环在一个线程内完成:

┌─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┐
└─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┘

还有一种方法,可以把数组拆成两部分,分别计算,最后加起来就是最终结果,这样可以用两个线程并行执行:

┌─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┐
└─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┘
┌─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┐
└─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┘

如果拆成两部分还是很大,我们还可以继续拆,用4个线程并行执行:

┌─┬─┬─┬─┬─┬─┐
└─┴─┴─┴─┴─┴─┘
┌─┬─┬─┬─┬─┬─┐
└─┴─┴─┴─┴─┴─┘
┌─┬─┬─┬─┬─┬─┐
└─┴─┴─┴─┴─┴─┘
┌─┬─┬─┬─┬─┬─┐
└─┴─┴─┴─┴─┴─┘

这就是Fork/Join任务的原理:判断一个任务是否足够小,如果是,直接计算,否则,就分拆成几个小任务分别计算。这个过程可以反复“裂变”成一系列小任务。

简单来说就是种分而治之的思想

Fork/Join 特点:工作窃取(work-stealing)

img

假如我们需要做一个比较大的任务,我们可以把这个任务分割为若干互不依赖的子任务,为了减少线程间的竞争,于是把这些子任务分别放到不同的队列里,并为每个队列创建一个单独的线程来执行队列里的任务,线程和队列一一对应,比如A线程负责处理A队列里的任务。

但是有的线程会先把自己队列里的任务干完,而其他线程对应的队列里还有任务等待处理。干完活的线程与其等着,不如去帮其他线程干活,于是它就去其他线程的队列里窃取一个任务来执行。

而在这时它们会访问同一个队列,所以为了减少窃取任务线程和被窃取任务线程之间的竞争,通常会使用双端队列,被窃取任务线程永远从双端队列的头部拿任务执行,而窃取任务的线程永远从双端队列的尾部拿任务执行。

工作窃取算法:

  • 优点是充分利用线程进行并行计算,并减少了线程间的竞争,
  • 缺点是在某些情况下还是存在竞争,比如双端队列里只有一个任务时。并且消耗了更多的系统资源,比如创建多个线程和多个双端队列。

在这里插入图片描述

举例:

/**
 * 对比 遍历、Fork/Join、和流计算的运算速度
 */
public class Test {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        test1(); // 12224
        test2(); // 10038
        test3(); // 153
    }

    // 普通程序员:遍历
    public static void test1() {
        Long sum = 0L;
        long start = System.currentTimeMillis();
        for (Long i = 1L; i <= 10_0000_0000; i++) {
            sum += i;
        }
        long end = System.currentTimeMillis();
        System.out.println("sum =>" + sum + " 时间:" + (end - start));
    }

    // 会使用ForkJoin
    public static void test2() throws ExecutionException, InterruptedException {
        long start = System.currentTimeMillis();

        ForkJoinPool forkJoinPool = new ForkJoinPool();
        ForkJoinTask<Long> task = new ForkJoinDemo(0L, 10_0000_0000L);
        ForkJoinTask<Long> submit = forkJoinPool.submit(task);// 提交任务
        Long sum = submit.get();

        long end = System.currentTimeMillis();

        System.out.println("sum =>" + sum + " 时间:" + (end - start));
    }

    //流计算
    public static void test3() {
        long start = System.currentTimeMillis();
        // Stream并行流 ()  (]
        long sum = LongStream.rangeClosed(0L, 10_0000_0000L).parallel().reduce(0, Long::sum);
        long end = System.currentTimeMillis();
        System.out.println("sum =>" + sum +" 时间:" + (end - start));
    }

}

/**
 * 求和计算的任务!
 * 3000   6000(ForkJoin)  9000(Stream并行流)
 * // 如何使用 forkjoin
 * // 1、forkjoinPool 通过它来执行
 * // 2、计算任务 forkjoinPool.execute(ForkJoinTask task)
 * // 3. 计算类要继承 ForkJoinTask
 */
class ForkJoinDemo extends RecursiveTask<Long> {

    private Long start;  // 1
    private Long end;    // 1990900000

    // 临界值
    private Long temp = 10000L;

    public ForkJoinDemo(Long start, Long end) {
        this.start = start;
        this.end = end;
    }

    // 计算方法
    @Override
    protected Long compute() {
        if ((end - start) < temp) {
            Long sum = 0L;
            for (Long i = start; i <= end; i++) {
                sum += i;
            }
            return sum;
        } else { // forkjoin 递归
            long middle = (start + end) / 2; // 中间值
            ForkJoinDemo task1 = new ForkJoinDemo(start, middle);
            task1.fork(); // 拆分任务,把任务压入线程队列
            ForkJoinDemo task2 = new ForkJoinDemo(middle + 1, end);
            task2.fork(); // 拆分任务,把任务压入线程队列

            return task1.join() + task2.join();
        }
    }
}

小结

Fork/Join是一种基于“分治”的算法:通过分解任务,并行执行,最后合并结果得到最终结果。

ForkJoinPool线程池可以把一个大任务分拆成小任务并行执行,任务类必须继承自RecursiveTaskRecursiveAction

使用Fork/Join模式可以进行并行计算以提高效率。

15.异步回调:

Java内部的ajax方法:Future 设计==》对将来的某个事件的结果进行建模

狂神此处讲解的是CompletableFuture类,但是网上文章大多都以CallBack接口进行讲解,以后有空再研究。

在这里插入图片描述

/**
 * 异步调用: CompletableFuture
 * // 异步执行
 * // 成功回调
 * // 失败回调
 */
public class Demo01 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 没有返回值的 runAsync 异步回调
//        CompletableFuture<Void> completableFuture = CompletableFuture.runAsync(()->{
//            try {
//                TimeUnit.SECONDS.sleep(2);
//            } catch (InterruptedException e) {
//                e.printStackTrace();
//            }
//            System.out.println(Thread.currentThread().getName()+"runAsync=>Void");
//        });
//
//        System.out.println("1111");
//
//        completableFuture.get(); // 获取阻塞执行结果

        // 有返回值的 supplyAsync 异步回调
        // ajax,成功和失败的回调
        // 返回的是错误信息;
        CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(()->{
            System.out.println(Thread.currentThread().getName()+"supplyAsync=>Integer");
            int i = 10/0;
            return 1024;
        });

        System.out.println(completableFuture.whenComplete((t, u) -> {
            System.out.println("t=>" + t); // 正常的返回结果
            System.out.println("u=>" + u); // 错误信息:java.util.concurrent.CompletionException: java.lang.ArithmeticException: / by zero
        }).exceptionally((e) -> {
            System.out.println(e.getMessage());
            return 233; // 可以获取到错误的返回结果
        }).get());

        /**
         * succee Code 200
         * error Code 404 500
         */
    }
}
  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值