Java多线程进阶之JUC并发编程教程详解(狂神说学习笔记)

文章目录

        • 环境准备
  • 1.JUC 简介
  • 2.线程和进程
        • 线程和进行
        • 并发和并行
        • 线程的状态
        • wait/sleep的区别
  • 3.Lock 锁(重点)
        • Synchronized 传统的锁
        • Lock锁
        • synchronized 和 lock 锁的区别
  • 4.生产者和消费者问题
        • Synchronized 版本
        • JUC 版本
  • 5.八个有关锁的问题
        • 问题1:两个同步方法,先执行发短信还是打电话?
        • 问题2:如果发短信延迟2秒,谁先执行
        • 问题3 加上一个没有锁的普通方法,谁先执行
        • 问题4:两个对象,一个调用发短信,一个调用打电话,谁先执行
        • 问题5:原来的两个同步方法,变为静态同步方法,一个对象调用,谁先执行
        • 问题6:创建两个实例,调用两个静态同步方法,谁先执行
        • 问题7:一个静态同步方法、一个同步方法、一个对象调用,谁先执行
        • 问题8:两个对象,一个调用静态同步方法,一个调用普通同步方法,谁先执行
        • 小结
  • 6.集合类的安全问题
        • List 不安全
        • Set 不安全
        • HashMap 不安全
  • 7.Callable(简单)
  • 8.JUC 常用辅助类
        • CountDownLatch
        • CyclickBarrier
        • Semaphore
  • 9.ReadWriteLock 读写锁
  • 10.阻塞队列
        • Blockqueue
        • SynchronizedQueue 同步队列
  • 11.线程池(重点)
        • 池化技术
        • 线程池:三大方法
        • 线程池:七大参数
        • 四种拒绝策略
        • 如何设置线程池的最大值(maximumPoolSize)
  • 12.四大函数式接口(重点)
        • Function 函数型接口
        • Predicate 断定型接口
        • Suppier 供给型接口
        • Consummer 消费型接口
        • 小结:函数式接口的作用
  • 13.Stream 流式计算
  • 14.ForkJoin 分之合并
        • ForkJoin 特点:工作窃取
        • 如何使用ForkJoin
  • 15.异步回调
  • 16.JMM
        • 谈谈对 Volatile 的理解
        • 什么是 JMM
  • 17.Volatile
        • 保证可见性
        • 不保证原子性
        • 禁止指令重排
        • 小结
  • 18.玩转单例模式
        • 饿汉模式
        • 懒汉模式
        • DCL 懒汉式
        • 静态内部类实现单例
        • 反射可以破坏单例
        • 枚举
  • 19.深入理解CAS
        • 什么是CAS
        • 小结
        • ABA 问题
  • 20.原子引用
  • 21.各种锁的理解
        • 公平锁、非公平锁
        • 可重入锁(递归锁)
        • 自旋锁
        • 死锁

环境准备

创建普通的maven项目,确保我们的项目一定是在Java8的环境下,检查settings中的以下几点
Java多线程进阶之JUC并发编程教程详解(狂神说学习笔记)_第1张图片
Java多线程进阶之JUC并发编程教程详解(狂神说学习笔记)_第2张图片
添加一个 lombok 依赖

1.JUC 简介

什么是 JUC ?

  • JUC 就是 java.util.concurrent 下面的类包,专门用于多线程的开发

Java多线程进阶之JUC并发编程教程详解(狂神说学习笔记)_第3张图片
为什么使用 JUC ?

  • 以往我们所学,普通的线程代码,都是用的thread或者runnable接口
  • 但是相比于callable来说,thread没有返回值,且效率没有callable高

2.线程和进程

线程和进行

  • 线程是进程中的一个实体,线程本身是不会独立存在的。
  • 进程是代码在数据集合上的一次运行活动, 是系统进行资源分配和调度的基本单位。
  • 线程则是进程的一个执行路径, 一个进程中至少有一个线程,进程中的多个线程共享进程的资源。
  • 操作系统在分配资源时是把资源分配给进程的, 但是CPU 资源比较特殊, 它是被分配到线程的, 因为真正要占用CPU 运行的是线程, 所以也说线程是CPU 分配的基本单位。
  • java默认有几个线程? 两个 main线程 gc线程
  • Java 中,使用 Thread、Runnable、Callable 开启线程。
  • Java 没有权限开启线程 、Thread.start() 方法调用了一个 native 方法 start0(),它调用了底层 C++ 代码。

查看源码可以发现,start方法底层调用了本地方法,本地方法就是C语言提供的

//本地方法,调用底层c++, java无法操作硬件
private native void start0();

并发和并行

并发(多线程操作同一个资源,交替执行)

  • CPU一核, 模拟出来多条线程,天下武功,唯快不破,快速交替

并行(多个人一起行走, 同时进行)

  • CPU多核,多个线程同时进行 ; 使用线程池操作

代码检测当前CPU核数

public class TestCore {
     
    public static void main(String[] args) {
     
        // 获取cpu核数
        // cpu密集型,IO密集型
        System.out.println(Runtime.getRuntime().availableProcessors());
    }
}

并发编程的本质: 充分利用CPU的资源

线程的状态

查看Thread源码,可以发现枚举类State

public enum State {
     
       // 新生
        NEW,
        // 运行
        RUNNABLE,
        // 阻塞
        BLOCKED,
        // 等待,死等
        WAITING,
        //超时等待
        TIMED_WAITING,
        //终止
        TERMINATED;
    }

wait/sleep的区别

  • 来自不同的类:wait来自object类, sleep来自线程类
  • 关于锁的释放:wait会释放锁, sleep不会释放锁
  • 使用范围不同:wait必须在同步代码块中,sleep可以在任何地方睡
  • 是否需要捕获异常:wait不需要捕获异常,sleep需要捕获异常

3.Lock 锁(重点)

Synchronized 传统的锁

之前我们所学的使用线程的传统思路是:

  • 单独创建一个线程类,继承Thread或者实现Runnable
  • 在这个线程类中,重写run方法,同时添加相应的业务逻辑
  • 在主线程所在方法中new上面的线程对象,调用start方法启动

比如,

//线程不安全:买票例子
//线程不安全,输出结果有买重票有负数票
public class UnsafeBuyTicket {
     
    public static void main(String[] args) {
     
        BuyTicket station = new BuyTicket();
        new Thread(station,"抢票的我").start();
        new Thread(station,"买票的你们").start();
        new Thread(station,"可恶的黄牛党").start();
    }
}
class BuyTicket implements Runnable{
     
    //票
    private int ticketNums = 10;
    //外部停止方式
    boolean flag = true;
    @Override
    public void run() {
     
        //买票
        while (true){
     
            if (flag){
     
                try {
     
                    buy();
                } catch (InterruptedException e) {
     
                    e.printStackTrace();
                }
            }else {
     
                break;
            }
        }
    }
    private void buy() throws InterruptedException {
     
        //判断是否有票
        if (ticketNums <= 0){
     
            flag = false;
            return;
        }
        //模拟延时,放大问题
        Thread.sleep(100);
        //买票
        System.out.println(Thread.currentThread().getName()+"--> 拿到第 "+ ticketNums--+" 张票");
    }
}

但是这样写代码有诸多问题,不太符合OOP思想,增加耦合性等问题,

实际工作的使用线程的思路是:

  • 创建一个独立的类只作为资源类,存放属性、方法,所以在多线程中我们需要锁这个公共资源
  • 线程类主要作为工具使用,用于开启多线程,把资源类实例丢到线程类的重写run方法中执行业务
  • 在我们的业务类中,比如主线程中,创建若干个线程类实例,去操作资源类

比如,这里我们使用了实现Runnable的方法创建线程,还是用了lambda表达式来创建Runnable实例

public class TestCore {
     
    public static void main(String[] args) {
     
        Ticket ticket = new Ticket();
        new Thread(()->{
     
            for (int i = 0; i < 100; i++) {
     
                ticket.sale();
            }
        }).start();
        new Thread(()->{
     
            for (int i = 0; i < 100; i++) {
     
                ticket.sale();
            }
        }).start();
        new Thread(()->{
     
            for (int i = 0; i < 100; i++) {
     
                ticket.sale();
            }
        }).start();
    }
}
//  这是一个资源类,存放属性、方法
class Ticket{
     
    private int number = 300;
    public synchronized void sale(){
     
        if(number>0){
     
            System.out.println(Thread.currentThread().getName()+"get "+number+"#");
            number--;
        }
    }
}

这需要我们在工作加以注意

Lock锁

查看 api 文档
Java多线程进阶之JUC并发编程教程详解(狂神说学习笔记)_第4张图片
Java多线程进阶之JUC并发编程教程详解(狂神说学习笔记)_第5张图片
可以看到,Lock是一个接口,有三个实现类,现在我们使用 ReentrantLock 就够用了

查看 ReentrantLock 源码,构造器
Java多线程进阶之JUC并发编程教程详解(狂神说学习笔记)_第6张图片
公平非公平:

  • 公平锁::十分公平, 可以先来后到,一定要排队
  • 非公平锁::十分不公平,可以插队(默认)

ReentrantLock 构造器

  • ReentrantLock 默认的构造方法是非公平锁(可以插队)。
  • 如果在构造方法中传入 true 则构造公平锁(不可以插队,先来后到)。

我们将上面的抢票代码改造为

public class SaleTicketDemo {
     
    public static void main(String[] args) {
     
        Ticket ticket = new Ticket();
        new Thread(()->{
     for(int i = 0; i < 40; i++) ticket.sale();}, "a").start();
        new Thread(()->{
     for(int i = 0; i < 40; i++) ticket.sale();}, "b").start();
        new Thread(()->{
     for(int i = 0; i < 40; i++) ticket.sale();}, "c").start();
    }
}
class Ticket {
     
    private int ticketNum = 30;
    private Lock lock = new ReentrantLock();
    public void sale() {
     
        lock.lock();
        try {
     
            if (this.ticketNum > 0) {
     
                System.out.println(Thread.currentThread().getName() + "购得第" + ticketNum-- + "张票, 剩余" + ticketNum + "张票");
            }
            //增加错误的发生几率
            Thread.sleep(10);
        } catch (Exception e) {
     
            e.printStackTrace();
        } finally {
     
            lock.unlock();
        }
    }
}

运行,发现,多线程都有几率抢到票,且没有出现线程安全问题
Java多线程进阶之JUC并发编程教程详解(狂神说学习笔记)_第7张图片
综述,Lock 锁实现步骤:

  1. 创建锁,new ReentrantLock()
  2. 加锁,lock.lock()
  3. 解锁,lock.unlock()
  4. 基本结构固定,中间的业务自己灵活修改

synchronized 和 lock 锁的区别

  1. synchronized 是内置的 Java 关键字,Lock 是一个 Java 类
  2. synchronized 无法判断获取锁的状态,Lock可以判断是否获取到了锁
  3. synchronized 会自动释放锁,Lock 必须要手动释放锁!如果不释放锁,会产生死锁
  4. synchronized 假设线程1(获得锁,然后发生阻塞),线程2(一直等待); Lock 锁就不一定会等待下去,可使用 tryLock 尝试获取锁
  5. synchronized 可重入锁,不可以中断的,非公平的;Lock锁,可重入的,可以判断锁,是否公平(可自己设置)
  6. synchronized 适合锁少量的代码同步问题,Lock 适合锁大量的同步代码

总体来说,synchronized 本来就是一个关键字,很多规则都是定死的,灵活性差;Lock 是一个类,灵活性高

思考问题:什么是锁?锁的是什么?

4.生产者和消费者问题

面试高频考点:

  • 单例模式、八大排序、生产者消费者、死锁

Synchronized 版本

解决线程之间的通信问题,比如线程操作一个公共的资源类

基本流程可以总结为:

  • 等待:判断是否需要等待
  • 业务:执行相应的业务
  • 通知:执行完业务通知其他线程
public class ConsumeAndProduct {
     
    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();
                }
            }
        },"A").start();
        // 创建一个消费者
        new Thread(()->{
     
            for (int i = 0; i < 10; i++) {
     
                try {
     
                    data.decrement();
                } catch (InterruptedException e) {
     
                    e.printStackTrace();
                }
            }
        },"B").start();
    }
}
//这是一个缓冲类,生产和消费之间的仓库,公共资源类
class Data{
     
    // 这是仓库的资源,生产者生产资源,消费者消费资源
    private int num = 0;
    // +1,利用关键字加锁
    public synchronized void increment() throws InterruptedException {
     
        // 首先查看仓库中的资源(num),如果资源不为0,就利用 wait 方法等待消费,释放锁
        if(num!=0){
     
            this.wait();
        }
        num++;
        System.out.println(Thread.currentThread().getName()+"=>"+num);
        // 通知其他线程 +1 执行完毕
        this.notifyAll();
    }
    // -1
    public synchronized void decrement() throws InterruptedException {
     
        // 首先查看仓库中的资源(num),如果资源为0,就利用 wait 方法等待生产,释放锁
        if(num==0){
     
            this.wait();
        }
        num--;
        System.out.println(Thread.currentThread().getName()+"=>"+num);
        // 通知其他线程 -1 执行完毕
        this.notifyAll();
    }
}

在这里插入图片描述
思考问题:如果存在ABCD4个线程是否安全?

  • 不安全,会有虚假唤醒

Java多线程进阶之JUC并发编程教程详解(狂神说学习笔记)_第8张图片
查看 api 文档
Java多线程进阶之JUC并发编程教程详解(狂神说学习笔记)_第9张图片
解决办法:if 判断改为 while,防止虚假唤醒

  • 因为 if 只会执行一次,执行完会接着向下执行 if() 外边的代码
  • 而 while 不会,直到条件满足才会向下执行 while() 外边的代码

修改代码为:

		// ...
  		// 使用 if 存在虚假唤醒
        while (num!=0){
     
            this.wait();
        }
        // ...
        while(num==0){
     
            this.wait();
        }

JUC 版本

锁、等待、唤醒 都进行了更换
Java多线程进阶之JUC并发编程教程详解(狂神说学习笔记)_第10张图片
Java多线程进阶之JUC并发编程教程详解(狂神说学习笔记)_第11张图片
将代码改造为 JUC 版本的生产者和消费者模式,这里我们使用四个线程,ABCD,两个生产者,两个消费者,

改造之后,确实可以实现01切换,但是ABCD是无序的,不满足我们的要求,

package com.swy.pc;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @author SuperSong
 * @version 1.0.0
 * @ClassName juc-study.com.swy.pc.ConsumeAndProduct.java
 * @Description TODO
 * @createTime 2021年04月25日 07:47:00
 */
public class ConsumeAndProductLock {
     
    public static void main(String[] args) {
     
        Data2 data = new Data2();
        new Thread(()->{
     
            for (int i = 0; i < 10; i++) {
     
                try {
     
                    data.increment();
                } catch (InterruptedException e) {
     
                    e.printStackTrace();
                }
            }
        },"A").start();
        new Thread(()->{
     
            for (int i = 0; i < 10; i++) {
     
                try {
     
                    data.decrement();
                } catch (InterruptedException e) {
     
                    e.printStackTrace();
                }
            }
        },"B").start();
        new Thread(()->{
     
            for (int i = 0; i < 10; i++) {
     
                try {
     
                    data.increment();
                } catch (InterruptedException e) {
     
                    e.printStackTrace();
                }
            }
        },"C").start();
        new Thread(()->{
     
            for (int i = 0; i < 10; i++) {
     
                try {
     
                    data.decrement();
                } catch (InterruptedException e) {
     
                    e.printStackTrace();
                }
            }
        },"D").start();
    }
}
class Data2{
     
    private int num = 0;
    Lock lock = new ReentrantLock();
    Condition condition = lock.newCondition();
    public void increment() throws InterruptedException {
     
        lock.lock();
        try {
     
            while (num != 0) {
     
                condition.await();
            }
            num++;
            System.out.println(Thread.currentThread().getName() + "=>" + num);
            condition.signalAll();
        } catch (Exception e) {
     
            e.printStackTrace();
        } finally {
     
            lock.unlock();
        }
    }
    public void decrement() throws InterruptedException {
     
        lock.lock();
        try {
     
            while (num == 0) {
     
                condition.await();
            }
            num--;
            System.out.println(Thread.currentThread().getName() + "=>" + num);
            condition.signalAll();
        } catch (Exception e) {
     
            e.printStackTrace();
        } finally {
     
            lock.unlock();
        }
    }
}

Java多线程进阶之JUC并发编程教程详解(狂神说学习笔记)_第12张图片
Condition 的优势在于,精准的通知和唤醒线程!比如,指定通知下一个进行顺序。

重新举个例子,

三个线程 A执行完调用B,B执行完调用C,C执行完调用A,分别用不同的监视器,执行完业务后指定唤醒哪一个监视器,实现线程的顺序执行

锁是统一的,但监视器是分别指定的,分别唤醒,signal,之前使用的是 signalAll

// A执行完调用B,B执行完调用C,C执行完调用A
public class ConditionDemo {
     
    public static void main(String[] args) {
     
        Data3 data3 = new Data3();
        new Thread(()->{
     
            for (int i = 0; i < 10; i++) {
     
                data3.printA();
            }
        },"A").start();
        new Thread(()->{
     
            for (int i = 0; i < 10; i++) {
     
                data3.printB();
            }
        },"B").start();
        new Thread(()->{
     
            for (int i = 0; i < 10; i++) {
     
                data3.printC();
            }
        },"C").start();
    }
}
class Data3 {
     
    private Lock lock = new ReentrantLock();
    private Condition condition1 = lock.newCondition();
    private Condition condition2 = lock.newCondition();
    private Condition condition3 = lock.newCondition();
    private int num = 1; // 1A 2B 3C
    public void printA(){
     
        lock.lock();
        try {
     
            while (num != 1){
     
                condition1.await();
            }
            System.out.println(Thread.currentThread().getName() + " Im A ");
            num = 2;
            condition2.signal();
        } catch (InterruptedException e) {
     
            e.printStackTrace();
        } finally {
     
            lock.unlock();
        }
    }
    public void printB(){
     
        lock.lock();
        try {
     
            while (num != 2){
     
                condition2.await();
            }
            System.out.println(Thread.currentThread().getName() + " Im B ");
            num = 3;
            condition3.signal();
        } catch (InterruptedException e) {
     
            e.printStackTrace();
        } finally {
     
            lock.unlock();
        }
    }
    public void printC(){
     
        lock.lock();
        try {
     
            while (num != 3) {
     
                condition3.await();
            }
            System.out.println(Thread.currentThread().getName() + " Im C ");
            num = 1;
            condition1.signal();
        } catch (InterruptedException e) {
     
            e.printStackTrace();
        } finally {
     
            lock.unlock();
        }
    }
}

Java多线程进阶之JUC并发编程教程详解(狂神说学习笔记)_第13张图片

5.八个有关锁的问题

深入理解锁

关于锁的八个问题

问题1:两个同步方法,先执行发短信还是打电话?

标准情况下,两个线程,先发短信还是先打电话?

public class Test1 {
     
    public static void main(String[] args) {
     
        Phone phone = new Phone();
        new Thread(()->{
     
            phone.sendMsg();
        }).start();
        try {
     
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
     
            e.printStackTrace();
        }
        new Thread(()->{
     
            phone.call();
        }).start();
    }
}
// 可视作资源类
class Phone{
     
    public synchronized void sendMsg(){
     
        System.out.println("发短信");
    }
    public synchronized void call(){
     
        System.out.println("打电话");
    }
}

经过测试,一直是先发短信
Java多线程进阶之JUC并发编程教程详解(狂神说学习笔记)_第14张图片

问题2:如果发短信延迟2秒,谁先执行

public class Test1 {
     
    public static void main(String[] args) {
     
        Phone phone = new Phone();
        new Thread(()->{
     
            phone.sendMsg();
        }).start();
        try {
     
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
     
            e.printStackTrace();
        }
        new Thread(()->{
     
            phone.call();
        }).start();
    }
}
// 可视作资源类
class Phone{
     
    public synchronized void sendMsg(){
     
        try {
     
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
     
            e.printStackTrace();
        }
        System.out.println("发短信");
    }
    public synchronized void call(){
     
        System.out.println("打电话");
    }
}

结果依旧是先发短信,后打电话

分析:

  • 并不是由于发短信在前导致的
  • 本案例中,方法前加synchronized,锁的其实该方法的调用者,也就是 phone 实例,两个方法共用同一个 phone 对象的锁,谁先拿到,谁先执行
  • 在主线程中,先调用发短信,所以先执行,打电话等释放锁再执行

问题3 加上一个没有锁的普通方法,谁先执行

public class Test2 {
     
    public static void main(String[] args) {
     
        Phone2 phone2 = new Phone2();
        new Thread(()->{
     
            phone2.sendMsg();
        }).start();
        try {
     
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
     
            e.printStackTrace();
        }
        new Thread(()->{
     
            phone2.hello();
        }).start();
    }
}
// 可视作资源类
class Phone2 {
     
    public synchronized void sendMsg(){
     
        try {
     
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
     
            e.printStackTrace();
        }
        System.out.println("发短信");
    }
    public synchronized void call(){
     
        System.out.println("打电话");
    }
    public void hello(){
     
        System.out.println("hello");
    }
}

观察发现,先执行了 hello
Java多线程进阶之JUC并发编程教程详解(狂神说学习笔记)_第15张图片

分析原因:

  • hello 是一个普通方法,不受 synchronized 锁的影响,不用等待锁释放。

问题4:两个对象,一个调用发短信,一个调用打电话,谁先执行

public class Test2 {
     
    public static void main(String[] args) {
     
        Phone2 phone1 = new Phone2();
        Phone2 phone2 = new Phone2();
        new Thread(()->{
     
            phone1.sendMsg();
        }).start();
        try {
     
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
     
            e.printStackTrace();
        }
        new Thread(()->{
     
            phone2.call();
        }).start();
    }
}
// 可视作资源类
class Phone2 {
     
    public synchronized void sendMsg(){
     
        try {
     
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
     
            e.printStackTrace();
        }
        System.out.println("发短信");
    }
    public synchronized void call(){
     
        System.out.println("打电话");
    }
    public void hello(){
     
        System.out.println("hello");
    }
}

结论,先打电话,后发短信
Java多线程进阶之JUC并发编程教程详解(狂神说学习笔记)_第16张图片

分析原因:

  • 两个对象两把锁,互不影响,1拿到锁还需要等待3秒,2拿到对象立刻就能打电话

问题5:原来的两个同步方法,变为静态同步方法,一个对象调用,谁先执行

public class Test3 {
     
    public static void main(String[] args) {
     
        Phone3 phone = new Phone3();
        new Thread(() -> {
     
            phone.sendMessage();
        }, "A").start();
        try {
     
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
     
            e.printStackTrace();
        }

        new Thread(() -> {
     
            phone.call();
        }, "B").start();
    }
}
class Phone3 {
     
    public static synchronized void sendMessage() {
     
        try {
     
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
     
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "发短信");
    }
    public static synchronized void call() {
     
        System.out.println(Thread.currentThread().getName() + "打电话");
    }
}

结果,始终是先发短信,后打电话
在这里插入图片描述
分析原因:

  • 静态方法前面加锁,锁的其实是这个方法所在的Class类对象(非静态那个是实例对象,注意区分)
  • Class类对象也是全局唯一,使用的是通一把锁,所以先发短信,后打电话
  • 虽然和上面的实例对象都是对应了全局唯一的锁,但原理还是有所不同
  • 主线程先执行了发短信,打电话就必须等锁释放再执行

问题6:创建两个实例,调用两个静态同步方法,谁先执行

public class Test3 {
     
    public static void main(String[] args) {
     
        Phone3 phone2 = new Phone3();
        Phone3 phone3 = new Phone3();
        new Thread(() -> {
     
            phone2.sendMessage();
        }, "A").start();
        try {
     
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
     
            e.printStackTrace();
        }

        new Thread(() -> {
     
            phone3.call();
        }, "B").start();
    }
}
class Phone3 {
     
    public static synchronized void sendMessage() {
     
        try {
     
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
     
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "发短信");
    }
    public static synchronized void call() {
     
        System.out.println(Thread.currentThread().getName() + "打电话");
    }
}

结果,现发短信,后打电话
Java多线程进阶之JUC并发编程教程详解(狂神说学习笔记)_第17张图片
原因分析:

  • 虽然实例对象是两个,但是两个静态同步方法对应的锁是Class类对象的锁,还是全局唯一

问题7:一个静态同步方法、一个同步方法、一个对象调用,谁先执行

public class Test4 {
     
    public static void main(String[] args) {
     
        Phone4 phone = new Phone4();
        new Thread(()->{
     
            phone.sendMsg();
        }).start();
        try {
     
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
     
            e.printStackTrace();
        }
        new Thread(()->{
     
            phone.call();
        }).start();
    }
}
// 可视作资源类
class Phone4 {
     
    public static synchronized void sendMsg(){
     
        try {
     
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
     
            e.printStackTrace();
        }
        System.out.println("发短信");
    }
    public synchronized void call(){
     
        System.out.println("打电话");
    }
    public void hello(){
     
        System.out.println("hello");
    }
}

结果:先打电话,后发短信
Java多线程进阶之JUC并发编程教程详解(狂神说学习笔记)_第18张图片
原因分析:

  • 静态同步方法和普通同步方法分别对应了不同的锁,互不干扰
  • 发短信需要延迟3秒,所以打电话先执行了

问题8:两个对象,一个调用静态同步方法,一个调用普通同步方法,谁先执行

public class Test4 {
     
    public static void main(String[] args) {
     
        Phone4 phone1 = new Phone4();
        Phone4 phone2 = new Phone4();
        new Thread(()->{
     
            phone1.sendMsg();
        }).start();
        try {
     
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
     
            e.printStackTrace();
        }
        new Thread(()->{
     
            phone2.call();
        }).start();
    }
}
// 可视作资源类
class Phone4 {
     
    public static synchronized void sendMsg(){
     
        try {
     
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
     
            e.printStackTrace();
        }
        System.out.println("发短信");
    }
    public synchronized void call(){
     
        System.out.println("打电话");
    }
    public void hello(){
     
        System.out.println("hello");
    }
}

结果,先打电话,后发短信
Java多线程进阶之JUC并发编程教程详解(狂神说学习笔记)_第19张图片
分析原因:

  • 同问题7相同,两个方法对应了不同的锁,互不干扰
  • 发短信还需要等待3秒,所以打电话先执行完了

小结

  • 无外乎两种锁,一个是new实例的锁,一个是Class对象的锁
  • 实例的锁,与当前的实例唯一对应,Class对象的锁与这个类唯一对应
  • 如果两个方法等同一个锁,必须一个先执行完,释放锁,另一个才可以执行
  • 如果两个方法等不同的锁,互不影响,谁先谁后看具体情况
  • 在主线程中,代码是顺序执行的,再结合锁的原理,综合判断线程执行的顺序

6.集合类的安全问题

在 JUC 并发编程情况下,适用于单线程的集合类将出现并发问题

List 不安全

代码举例

public class ListTest {
     
    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
Java多线程进阶之JUC并发编程教程详解(狂神说学习笔记)_第20张图片
原因:ArrayList 在并发情况下是不安全的

解决方案1:

  • ArrayList 换成 Vector,Vector 方法里加了锁
  • Vector出现比较早,由于锁导致方法执行效率太低,不推荐使用

Java多线程进阶之JUC并发编程教程详解(狂神说学习笔记)_第21张图片
解决方案2:

  • 使用 Collection 静态方法,返回一个带锁的 List 实例
List<String> list = Collections.synchronizedList(new ArrayList<>());

测试正常使用
Java多线程进阶之JUC并发编程教程详解(狂神说学习笔记)_第22张图片
解决方案3:

  • 使用 JUC 提供的适合并发使用的 CopyOnWriteArrayList
List<String> list = new CopyOnWriteArrayList<>();

运行测试,没有问题

分析:

  • CopyOnWrite 表示写入时复制,简称COW,计算机程序设计领域的一种优化策略
  • 多线程调用list时,读取时没有问题,写入的时候会复制一份,避免在写入时被覆盖
  • 这也是一种读写分离的思想
  • CopyOnWriteArrayList 比 Vector 强在哪里?前者是写入、复制,且使用 lock 锁,效率比 Vector 的synchronized 锁要高很多

CopyOnWriteArrayList 方法,先复制,再修改,再set回去
Java多线程进阶之JUC并发编程教程详解(狂神说学习笔记)_第23张图片

Set 不安全

Set 和 List 同理可得:多线程情况下,普通的 Set 集合是线程不安全的

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

注意这里,不一定会立刻出现并发问题,需要多试几次,增加循环线程数量,容易出现问题

解决方案有两种;

  • 使用 Collection 工具类的 synchronized 包装的 Set 类
Set<String> set = Collections.synchronizedSet(new HashSet<>());
  • 使用 JUC 提供的 CopyOnWriteArraySet 写入复制
Set<String> set = new CopyOnWriteArraySet<>();

思考,HashSet 底层到底是什么?

  • hashSet底层就是一个HashMap;hashSet只使用了hashMap的key

Java多线程进阶之JUC并发编程教程详解(狂神说学习笔记)_第24张图片
在这里插入图片描述

HashMap 不安全

查看 HashMap 源码

Java多线程进阶之JUC并发编程教程详解(狂神说学习笔记)_第25张图片
默认初始容量 24=16,最大容量为 230,加载因子 0.75

HashMap 在多线程操作情况下,也有并发修改异常的问题

解决方案:

  • Map map = Collections.synchronizedMap(new HashMap<>());
  • Map map = new ConcurrentHashMap<>();

7.Callable(简单)

Java多线程进阶之JUC并发编程教程详解(狂神说学习笔记)_第26张图片
得到的信息:

  • 可以有返回值
  • 可以抛出异常
  • 方法不同,run() => call()

使用时注意

  • Callable 的泛型也是 call 方法的返回值类型
  • Callable 的实现类无法直接放在 Thread 中,还需要先放在 FutureTask 中,再放在 Thread 中
  • FutureTask 就相当于适配类,起到牵线的作用
public class CallableTest {
     
    public static void main(String[] args) throws ExecutionException, InterruptedException {
     
        FutureTask<Integer> futureTask = new FutureTask<>(new MyThread());
        new Thread(futureTask,"a").start();
        System.out.println(futureTask.get());
    }
}
class MyThread implements Callable<Integer> {
     
    @Override
    public Integer call() throws Exception {
     
        System.out.println("call()方法被调用了");
        return 1024;
    }
}

注意:

  • 运行结果会产生缓存,目的是为了提高效率
  • get方法可能会产生阻塞,所以放在了最后

8.JUC 常用辅助类

CountDownLatch

减法计数器

//计数器
public class CountDownLatchDemo {
     
    public static void main(String[] args) throws InterruptedException {
     
        // 倒计时总数是6, 必须要执行任务的时候,再使用!
        CountDownLatch countDownLatch = new CountDownLatch(6);
        for (int i = 0; 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");
    }
}

原理:

  • countDownLatch.countDown(); //数量减1
  • countDownLatch.await();// 等待计数器归零,然后再向下执行

每次有线程调用countDown()数量-1,假设计数器变为0,countDownLatch.await();就会被唤醒,继续执行

CyclickBarrier

加法计数器,与 CountDownLatch 正好相反

相当于设定一个目标,线程数达到目标值之后才会执行

public class CyclicBarrierDemo {
     
    public static void main(String[] args) {
     
        // 主线程,计数器到达7即满足条件
        CyclicBarrier cyclicBarrier = new CyclicBarrier(7, () -> {
     
            System.out.println("召唤神龙");
        });
        for (int i = 1; i <= 7; i++) {
     
            // 子线程
            int finalI = i;// 下面thread中拿不到i,所以这里提取出来
            new Thread(()->{
     
                System.out.println(Thread.currentThread().getName()+"收集了第"+finalI+"颗龙珠");
                try {
     
                    cyclicBarrier.await();//加法计数等待
                } catch (InterruptedException e) {
     
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
     
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

Java多线程进阶之JUC并发编程教程详解(狂神说学习笔记)_第27张图片

Semaphore

计数信号量,比如说,有6辆车,3个停车位,汽车需要轮流等待车位

常用在需要限流的场景中,

public class SemaphoreDemo {
     
    public static void main(String[] args) {
     
        // 线程数量,停车位,限流
        Semaphore semaphore = new Semaphore(3);
        for (int i = 0; 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();// 释放
                }
            }).start();
        }
    }
}

Java多线程进阶之JUC并发编程教程详解(狂神说学习笔记)_第28张图片

原理:

  • semaphore.acquire() 获得资源,如果资源已经使用完了,就等待资源释放后再进行使用!
  • semaphore.release() 释放,会将当前的信号量释放+1,然后唤醒等待的线程!

用途:

  • 多个共享资源互斥的使用!
  • 并发限流,控制最大的线程数!

9.ReadWriteLock 读写锁

Java多线程进阶之JUC并发编程教程详解(狂神说学习笔记)_第29张图片
代码演示

public class ReadWriteLockDemo {
     
    public static void main(String[] args) {
     
        // 线程操作资源类
        MyCache myCache = new MyCache();
        int num = 6;
        for (int i = 1; i < num; i++) {
     
            int finalI = i;
            new Thread(()->{
     
                myCache.write(String.valueOf(finalI), String.valueOf(finalI));
            },String.valueOf(i)).start();
        }
        for (int i = 1; i < num; i++) {
     
            int finalI = i;
            new Thread(()->{
     
                myCache.read(String.valueOf(finalI));
            },String.valueOf(i)).start();
        }
    }
}
// 自定义缓存
class MyCache{
     
    private volatile Map<String,String> map = new HashMap<>();
    // 存,写
    public void write(String key, String value) {
     
        System.out.println(Thread.currentThread().getName() + "线程开始写入");
        map.put(key, value);
        System.out.println(Thread.currentThread().getName() + "线程开始写入ok");
    }
    // 取,读
    public void read(String key) {
     
        System.out.println(Thread.currentThread().getName() + "线程开始读取");
        map.get(key);
        System.out.println(Thread.currentThread().getName() + "线程读取ok");
    }
}

运行结果会出现问题,还没等线程写完或读完,就被其他线程写入,
Java多线程进阶之JUC并发编程教程详解(狂神说学习笔记)_第30张图片
如何解决这样的问题,需要加锁

之前我们使用的 Lock 锁,但粒度比较粗,只是普通的锁,

这里我们加读写锁,ReadWriteLock,这是一个更加细粒度的锁

public class ReadWriteLockDemo {
     
    public static void main(String[] args) {
     
        // 线程操作资源类
        MyCache myCache = new MyCache();
        int num = 6;
        for (int i = 1; i < num; i++) {
     
            int finalI = i;
            new Thread(()->{
     
                myCache.write(String.valueOf(finalI), String.valueOf(finalI));
            },String.valueOf(i)).start();
        }
        for (int i = 1; i < num; i++) {
     
            int finalI = i;
            new Thread(()->{
     
                myCache.read(String.valueOf(finalI));
            },String.valueOf(i)).start();
        }
    }
}
// 自定义缓存
class MyCache{
     
    private volatile Map<String,String> map = new HashMap<>();
    private ReadWriteLock readWriteLock= new ReentrantReadWriteLock();
    // 存,写,写入的时候只希望只有一个线程在写
    public void write(String key, String value) {
     
        readWriteLock.writeLock().lock();
        try {
     
            System.out.println(Thread.currentThread().getName() + "线程开始写入");
            map.put(key, value);
            System.out.println(Thread.currentThread().getName() + "线程开始写入ok");
        } catch (Exception e) {
     
            e.printStackTrace();
        } finally {
     
            readWriteLock.writeLock().unlock();
        }
    }
    // 取,读,所有线程都可以读
    public void read(String key) {
     
        readWriteLock.readLock().lock();
        try {
     
            System.out.println(Thread.currentThread().getName() + "线程开始读取");
            map.get(key);
            System.out.println(Thread.currentThread().getName() + "线程读取ok");
        } catch (Exception e) {
     
            e.printStackTrace();
        } finally {
     
            readWriteLock.readLock().unlock();
        }
    }
}

运行可以发现,写入时不可以被插队,读取时可以被多人执行
Java多线程进阶之JUC并发编程教程详解(狂神说学习笔记)_第31张图片
小结:

  • 读-读 可以共存
  • 读-写 不能共存
  • 写-写 不能共存

也可以这样称呼,含义都是一样,名字不同而已

  • 独占锁(写锁)一次只能由一个线程占有
  • 享锁(读锁)一次可以有多个线程占有

10.阻塞队列

Java多线程进阶之JUC并发编程教程详解(狂神说学习笔记)_第32张图片
阻塞队列
Java多线程进阶之JUC并发编程教程详解(狂神说学习笔记)_第33张图片

Blockqueue

Java多线程进阶之JUC并发编程教程详解(狂神说学习笔记)_第34张图片
阻塞队列 BlockQueue 是Collection 的一个子类

应用场景:多线程并发处理、线程池
Java多线程进阶之JUC并发编程教程详解(狂神说学习笔记)_第35张图片

BlockingQueue 有四组 API

方式 抛出异常 不会抛出异常,有返回值 阻塞等待 超时等待
添加操作 add() offer() 供应 put() offer(obj,int,timeunit.status)可设置时间
移除操作 remove() poll() 获得 take() poll(int,timeunit.status)可设置时间
判断队列首部 element() peek() 偷看,偷窥

代码演示:四种情况

public class BlockingQueueDemo {
     
    // 抛异常 add remove
    @Test
    public void test01(){
     
        ArrayBlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(3);
        System.out.println(blockingQueue.add("a"));
        System.out.println(blockingQueue.add("b"));
        System.out.println(blockingQueue.add("c"));
        // 查看队首
        System.out.println(blockingQueue.element());
        // 如果再添加就会抛出异常 java.lang.IllegalStateException: Queue full
        System.out.println(blockingQueue.remove());
        System.out.println(blockingQueue.remove());
        System.out.println(blockingQueue.remove());
        // 如果再移除也会造成 java.util.NoSuchElementException
    }

    // 不抛出异常,offer、poll、peek
    @Test
    public void test02(){
     
        ArrayBlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(3);
        System.out.println(blockingQueue.offer("a"));
        System.out.println(blockingQueue.offer("b"));
        System.out.println(blockingQueue.offer("c"));
        // 再添加不会抛出异常,而是返回false
        System.out.println(blockingQueue.offer("d"));
        // 检测队首元素
        System.out.println(blockingQueue.peek());
        System.out.println(blockingQueue.poll());
        System.out.println(blockingQueue.poll());
        System.out.println(blockingQueue.poll());
        // 再移除不会抛出异常,而是返回null
        System.out.println(blockingQueue.poll());
    }

    // 如果队列满了,等待,一直阻塞
    @Test
    public void test03() throws InterruptedException {
     
        ArrayBlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(3);
        // 一直阻塞,不会返回值
        blockingQueue.put("a");
        blockingQueue.put("b");
        blockingQueue.put("c");
        // 队列已满,再添加,则阻塞等待添加,程序一直在阻塞中,不结束
        System.out.println(blockingQueue.take());
        System.out.println(blockingQueue.take());
        System.out.println(blockingQueue.take());
        // 如果再移除不会抛出异常,但是会一直阻塞在这里了
    }
    // 这种情况也会发生阻塞等待,但会有超时结束
    @Test
    public void test04() throws InterruptedException {
     
        ArrayBlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(3);
        blockingQueue.offer("a");
        blockingQueue.offer("b");
        blockingQueue.offer("c");
        System.out.println("开始等待");
        blockingQueue.offer("d",2, TimeUnit.SECONDS);
        System.out.println("等待结束");
        System.out.println("=================取值=====================");
        System.out.println(blockingQueue.poll());
        System.out.println(blockingQueue.poll());
        System.out.println(blockingQueue.poll());
        System.out.println("取值开始等待");
        blockingQueue.poll(2,TimeUnit.SECONDS);//超过两秒,我们就不等待了
        System.out.println("取值结束等待");
    }
}

SynchronizedQueue 同步队列

同步队列没有容量,进去一个元素,必须等待取出来之后,才能再往里面放一个元素

  • SynchronizedQueue 使用 put 方法和 take 方法
  • Synchronized 和 其他的 BlockingQueue 不一样 它不存储元素;
  • put了一个元素,就必须从里面先 take 出来,否则不能再 put 进去值!
  • 并且 SynchronousQueue 的 take 是使用了 lock 锁保证线程安全的。
public class SynchronousQueueDemo {
     
    public static void main(String[] args) {
     
        BlockingQueue<String> synchronousQueue = new SynchronousQueue<>();
        new Thread(()->{
     
            try {
     
                System.out.println(Thread.currentThread().getName()+"put 01");
                synchronousQueue.put("1");
                System.out.println(Thread.currentThread().getName()+"put 02");
                synchronousQueue.put("2");
                System.out.println(Thread.currentThread().getName()+"put 03");
                synchronousQueue.put("3");
            } catch (InterruptedException e) {
     
                e.printStackTrace();
            }
        }).start();
        new Thread(()->{
     
            try {
     
                System.out.println(Thread.currentThread().getName()+"take"+synchronousQueue.take());
                System.out.println(Thread.currentThread().getName()+"take"+synchronousQueue.take());
                System.out.println(Thread.currentThread().getName()+"take"+synchronousQueue.take());
            } catch (InterruptedException e) {
     
                e.printStackTrace();
            }
        }).start();
    }
}

运行发现,两个线程一个存,一个取,即使我们没有规定顺序,执行起来也是存一次取一次,交替执行,这是 SynchronousQueue 的特性决定的

结论:put进入一个元素,必须先取出来,否则不能再put
Java多线程进阶之JUC并发编程教程详解(狂神说学习笔记)_第36张图片

11.线程池(重点)

池化技术

线程池重点:三大方式、七大参数、四种拒绝策略

程序的运行的本质:占用系统的资源 ! 优化CPU资源的使用 ===>池化技术(线程池、连接池、内存池、对象池…)

池化技术:实现准备好一些资源,有人要用,就来我这里拿,用完之后还给我

线程池的好处:

  • 降低资源消耗
  • 提高响应速度
  • 方便管理

如何优化:

  • 线程复用,可以控制最大并发数,管理线程

线程池:三大方法

查看阿里巴巴开发手册
Java多线程进阶之JUC并发编程教程详解(狂神说学习笔记)_第37张图片

  • ExecutorService threadPool = Executors.newSingleThreadExecutor();//单个线程
  • ExecutorService threadPool2 = Executors.newFixedThreadPool(5); //创建一个固定的线程池的大小
  • ExecutorService threadPool3 = Executors.newCachedThreadPool(); //可伸缩的

之前我们所学知识,直接创建线程,现在我们通过线程池来创建线程,使用池化技术

代码演示

//Executors 工具类
//使用了线程池之后要使用线程池创建线程
public class Demo01 {
     
    public static void main(String[] args) {
     
        // ExecutorService service = Executors.newSingleThreadExecutor();//单个线程
        // ExecutorService service = Executors.newFixedThreadPool(5);//创建一个固定的线程池的大小
        ExecutorService service = Executors.newCachedThreadPool();//可伸缩的,遇强则强,遇弱则弱
        try {
     
            for (int i = 0; i < 10; i++) {
     
                service.execute(() -> {
     
                    System.out.println(Thread.currentThread().getName() + "ok");
                });
            }
            //线程池用完要关闭线程池
        } finally {
     
            service.shutdown();
        }
    }
}

通过测试我们发现,固定线程池的方式至多只能创建固定数量,可伸缩的方式就会视情况而定

固定线程池(循环次数10)
Java多线程进阶之JUC并发编程教程详解(狂神说学习笔记)_第38张图片
可伸缩线程池,(循环次数100),
Java多线程进阶之JUC并发编程教程详解(狂神说学习笔记)_第39张图片
出现了 30多个线程

线程池:七大参数

源码分析:

newSingleThreadExecutor

public static ExecutorService newSingleThreadExecutor() {
     
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}

newFixedThreadPool

public static ExecutorService newFixedThreadPool(int nThreads) {
     
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}

newCachedThreadPool

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.corePoolSize = corePoolSize;
    this.maximumPoolSize = maximumPoolSize;
    this.workQueue = workQueue;
    this.keepAliveTime = unit.toNanos(keepAliveTime);
    this.threadFactory = threadFactory;
    this.handler = handler;
}

通过观察,上面的三种创建方式其实就是换了不同的参数

分析参数,才会有阿里巴巴规范所说的,不允许使用 Executors 创建线程池,而是通过 ThreadPoolExecutor 创建,因为参数可控
Java多线程进阶之JUC并发编程教程详解(狂神说学习笔记)_第40张图片
从这张图片来分析
Java多线程进阶之JUC并发编程教程详解(狂神说学习笔记)_第41张图片

  • 客户来银行办业务,一开始银行不会开放全部窗口,只开一部分(1.2),对应了core;
  • 随着人逐渐增多,等候的人呆在了候客区,也就是阻塞队列;
  • 人数进一步增多,开始开放其他窗口(3.4.5),用于营业,银行最大数量对应了 max;
  • 银行已经达到最大接客数量,再有客户进来,银行选择了拒绝策略;

理解了以上理论,我们将按照阿里巴巴手册的规范,手动创建线程池,改造代码

//自定义线程池,创建默认的线程工厂
ExecutorService threadPool = new ThreadPoolExecutor(
        3, // 核心线程池大小,也是初始默认大小
        5, // 最大并发数
        10, // 超时时间,超过时间没有业务就会释放多余的线程回到默认数量
        TimeUnit.SECONDS, // 时间单位
        new LinkedBlockingQueue<>(3),// 线程等候队列,这里表示队列超过3个等待就会触发最大线程数
        Executors.defaultThreadFactory(), //线程创建工厂
        new ThreadPoolExecutor.AbortPolicy());//拒绝策略,有四种拒绝策略,这个是默认的,表示如果队列满了就不再处理,并抛出异常

四种拒绝策略

Java多线程进阶之JUC并发编程教程详解(狂神说学习笔记)_第42张图片

  • new ThreadPoolExecutor.AbortPolicy: // 拒绝策略含义:银行满了,还有人进来,不处理这个人的,并抛出异常
  • new ThreadPoolExecutor.CallerRunsPolicy(): // 拒绝策略含义:哪来的去哪里,这里就是回到main线程进行处理
  • new ThreadPoolExecutor.DiscardPolicy(): // 拒绝策略含义:如果队列满了,丢掉任务,不会抛出异常。
  • new ThreadPoolExecutor.DiscardOldestPolicy(): // 拒绝策略含义:如果队列满了,尝试去和最早的进程竞争,成功则执行,失败则丢掉,也不会抛出异常
  • 这里的四大拒绝策略其实也对应了 BlockingQueue 四组 API 中抛异常策略

分析:

  • 线程池最大承载数 = 线程最大并发数 + 队列最大数,在这里就是 5+3=8
  • 超过最大承载数量就会触发拒绝策略

如何设置线程池的最大值(maximumPoolSize)

两种方案:

  • CPU密集型:CPU有几核,maximumPoolSize就是几,可以保证CPU的效率最高
  • IO 密集型:找出当前程序中十分耗 I/O 的线程数量,maximumPoolSize大于这个数的两倍即可

12.四大函数式接口(重点)

新时代程序员(JDK8新特性):lambda 表达式、链式编程、函数式接口、Stream 流式计算

函数式接口:只有一个方法的接口,比如 Runnable 接口

优点:简化编程,在新版本的框架底层大量使用

Java多线程进阶之JUC并发编程教程详解(狂神说学习笔记)_第43张图片
剩下的都是一些复合类型

Function 函数型接口

传入参数T,返回类型R
Java多线程进阶之JUC并发编程教程详解(狂神说学习笔记)_第44张图片
代码演示

public class FunctionDemo {
     
    public static void main(String[] args) {
     
        Function<String,String> function = (str)->{
     
            return str;
        };
        System.out.println(function.apply("aaaaaaa"));
    }
}

测试效果,传入什么,返回什么,像是一个工具类
Java多线程进阶之JUC并发编程教程详解(狂神说学习笔记)_第45张图片

Predicate 断定型接口

只能传入一个参数,返回值是 boolean
Java多线程进阶之JUC并发编程教程详解(狂神说学习笔记)_第46张图片
代码演示

public class PredicateDemo {
     
    public static void main(String[] args) {
     
        Predicate<String> predicate = (str)->{
     
            return str.isEmpty();
        };
        System.out.println(predicate.test("aaa"));
        System.out.println(predicate.test(""));
    }
}

测试效果,我们可以利用这一特性,对传入的参数进行判断,返回 true or false,

这里就是判断输入字符串是否为空
Java多线程进阶之JUC并发编程教程详解(狂神说学习笔记)_第47张图片

Suppier 供给型接口

没有输入,只有返回值,输出
Java多线程进阶之JUC并发编程教程详解(狂神说学习笔记)_第48张图片
代码演示

public class SupplierDemo {
     
    public static void main(String[] args) {
     
        Supplier<String> supplier = ()->{
     
            return "1024";
        };
        System.out.println(supplier.get());
    }
}

测试效果,这个实例展示输出的返回值
Java多线程进阶之JUC并发编程教程详解(狂神说学习笔记)_第49张图片

Consummer 消费型接口

只输入,没有返回值
Java多线程进阶之JUC并发编程教程详解(狂神说学习笔记)_第50张图片
代码演示

public class ConsumerDemo {
     
    public static void main(String[] args) {
     
        Consumer<String> consumer = (str)->{
     
            System.out.println(str);
        };
        consumer.accept("abc");
    }
}

测试结果,这里我们实例就是打印一下消费了什么内容
Java多线程进阶之JUC并发编程教程详解(狂神说学习笔记)_第51张图片

小结:函数式接口的作用

简化编程模型

这里的接口只是起一个规范,具体怎么来编写业务代码,需要我们自己重写里面的方法

13.Stream 流式计算

大数据:存储+计算

集合、MySQL 本质就是存储数据,而计算应该交给流来操作

首先查看 package java.util.stream包下的 interface Stream 接口

可以看到,Stream 中的大量方法都是用了函数式接口

Java多线程进阶之JUC并发编程教程详解(狂神说学习笔记)_第52张图片
题目要求:

  1. ID必须是偶数
  2. 年龄必须大于23岁
  3. 用户名转为大写字母
  4. 用户名倒着排序
  5. 只输出一个用户

根据上述需求,编写演示代码

public class StreamDemo {
     
    public static void main(String[] args) {
     
        User user1 = new User(1, "a", 21);
        User user2 = new User(2, "b", 22);
        User user3 = new User(3, "c", 23);
        User user4 = new User(4, "d", 24);
        User user5 = new User(5, "e", 25);
        // 集合用于存储
        List<User> list = Arrays.asList(user1, user2, user3, user4, user5);
        // 计算交给Stream流
        // 一行代码使用了 lambda表达式  链式编程  函数式接口  Stream流式计算
        list.stream()
                .filter(user -> {
      return user.getId() % 2 == 0;})
                .filter(user -> {
      return user.getAge() > 23;})
                .map(user -> {
      return user.getName().toUpperCase();})
                .sorted((u1,u2)->{
      return u2.compareTo(u1);})
                .limit(1)
                .forEach(System.out::println);
    }
}

执行效果,最终筛选出 D
Java多线程进阶之JUC并发编程教程详解(狂神说学习笔记)_第53张图片

理解分析:

  • Collection有一个方法 stream() 可以返回 Stream 流,然后我们使用 Stream 调用接口里的方法开启流式计算
  • 理解这些方法需要我们首先理解函数式接口,然后对照 API 查看各个方法的含义

比如,第一个方法 filter
在这里插入图片描述

  • 这个方法的参数需要我们使用 Predicate 断定型接口的实例,因此我们重写这个接口方法并使用了 lambda 表达式
user -> {
     return user.getId() % 2 == 0;}
  • 流中的所有user只有满足ID为偶数的才会返回true,通过筛选,filter返回的流中只有ID为偶数的user

  • 这里的流在计算判断时,会将所有的user全部都过一遍,然后按照我们的业务规则进行筛选

  • 这些代码就是通过泛型统一的元素类型为 User

总结一下:

  • list转为流,流调用方法,方法里的参数用函数式接口,函数式接口有我们定义业务规则,然后方法返回的就是符合条件的元素构成的流,继续链式编程

这种方式不仅简化代码,底层优化也使得代码效率得到提高,是不是感觉很强大!

14.ForkJoin 分之合并

ForkJoin 在JDK1.7,并行执行任务!提高效率~。在大数据量速率会更快!

大数据中:MapReduce 核心思想->把大任务拆分为小任务!

Java多线程进阶之JUC并发编程教程详解(狂神说学习笔记)_第54张图片

ForkJoin 特点:工作窃取

实现原理:双端队列!从上面和下面都可以去拿到任务进行执行!
Java多线程进阶之JUC并发编程教程详解(狂神说学习笔记)_第55张图片

如何使用ForkJoin

用法
Java多线程进阶之JUC并发编程教程详解(狂神说学习笔记)_第56张图片

使用要点:

  1. 通过 ForkJoinPool 来执行,把 ForkJoinTask 作为参数丢进去
  2. 计算任务 execute(ForkJoinTask task)(execute是同步执行,submit是异步执行)
  3. 写一个计算类要去继承 RecursiveTask(底层还是ForkJoinTask),重写方法,定义计算规则

准备计算类

public class ForkJoinDemo extends RecursiveTask<Long> {
     
    private long start;     // 1
    private long end;       // 20_0000_0000
    private long temp = 1_0000L;
    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 {
     
            // 大于临界值则采用分支合并计算
            long middle = (end + start) / 2;  //中间值
            ForkJoinDemo task1 = new ForkJoinDemo(start, middle);
            task1.fork(); // 拆分任务,把任务压入线程队列
            ForkJoinDemo task2 = new ForkJoinDemo(middle + 1, end);
            task2.fork();// 拆分任务,把任务压入线程队列
            return task1.join() + task2.join();
        }
    }
}

写测试类

public class ForkJoinTest {
     
    private static final long SUM = 20_0000_0000;
    public static void main(String[] args) throws ExecutionException, InterruptedException {
     
        test1();
        test2();
        test3();
    }
    // 使用普通方法
    public static void test1(){
     
        long star = System.currentTimeMillis();
        long sum = 0L;
        for (int i = 0; i < SUM; i++) {
     
            sum+=i;
        }
        long end = System.currentTimeMillis();
        System.out.println(sum);
        System.out.println("时间:" + (end-star));
        System.out.println("---------------");
    }
    // 使用 ForkJoin 方法
    public static void test2() throws ExecutionException, InterruptedException {
     
        long star = System.currentTimeMillis();
        ForkJoinPool forkJoinPool = new ForkJoinPool();
        ForkJoinTask<Long> task = new ForkJoinDemo(0L,SUM);
        ForkJoinTask<Long> submit = forkJoinPool.submit(task);
        Long along = submit.get();
        System.out.println(along);
        long end = System.currentTimeMillis();
        System.out.println("时间:" + (end - star));
        System.out.println("---------------");
    }
    // 使用流计算
    public static void test3(){
     
        long star = System.currentTimeMillis();
        long sum = LongStream.range(0L,20_0000_0000L).parallel().reduce(0,Long::sum);
        System.out.println(sum);
        long end = System.currentTimeMillis();
        System.out.println("时间:" + (end - star));
        System.out.println("-------------");
    }
}

运行测试
Java多线程进阶之JUC并发编程教程详解(狂神说学习笔记)_第57张图片
流式计算的效率最高,

为什么我的 forkjoin 计算比普通计算时间更长???

分析:

  • 使用 forkjoin 进行计算时,底层会自动调用 compute 方法计算,并使用我们重写的计算规则来判断
  • 比如本例子,当compute发现数值较大需要拆分,则拆分任务并压入线程队列
  • 然后再compute,如果发现仍然需要拆分,则再次拆分压入队列,直到不用再拆分才开始将当前队列开始计算
  • new ForkJoinDemo 中再次 new ForkJoinDemo 这本质上也是递归操作

注意:compute是 forkjoin 计算时底层自己调用的,这也是为什么很多人不理解他是递归的原因,我们查看源码

在这里插入图片描述
进入get
Java多线程进阶之JUC并发编程教程详解(狂神说学习笔记)_第58张图片
进入 getRawResult
Java多线程进阶之JUC并发编程教程详解(狂神说学习笔记)_第59张图片
选择 实现类
在这里插入图片描述
可以看到最终结果就是由底层 compute得来的
Java多线程进阶之JUC并发编程教程详解(狂神说学习笔记)_第60张图片

15.异步回调

Future 设计的初衷: 对将来的某个事件的结果进行建模

Java也可以实现异步调用,与ajax是一个道理

Java多线程进阶之JUC并发编程教程详解(狂神说学习笔记)_第61张图片

我们平时都使用 CompletableFuture

没有返回值的runAsync异步回调

public class CompletableFutureDemo {
     
    public static void main(String[] args) throws ExecutionException, InterruptedException {
     
//        发起一个请求
        System.out.println(System.currentTimeMillis());
        System.out.println("-----------");
        
        CompletableFuture<Void> future = CompletableFuture.runAsync(()->{
     
//            发起一个异步任务
            try {
     
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
     
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"......");
        });
        
        System.out.println(System.currentTimeMillis());
        System.out.println("-------------------------");
        System.out.println(future.get());//获取执行结果
    }
}

有返回值的异步回调supplyAsync

public class CompletableFutureDemo {
     
    public static void main(String[] args) throws ExecutionException, InterruptedException {
     
//        发起一个请求
        System.out.println(System.currentTimeMillis());
        System.out.println("-----------");

        CompletableFuture<Void> future = CompletableFuture.runAsync(()->{
     
//            发起一个异步任务
            try {
     
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
     
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"......");
        });

        System.out.println(System.currentTimeMillis());
        System.out.println("-------------------------");
        System.out.println(future.get());//获取执行结果
    }
    @Test
    public void test02() throws ExecutionException, InterruptedException {
     
        CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(()->{
     
            System.out.println(Thread.currentThread().getName());
            try {
     
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
     
                e.printStackTrace();
            }
            return 1024;
        });
        System.out.println(completableFuture.whenComplete((t,u)->{
     
//            success 回调
            System.out.println("t=>" +t);//正常的返回结果
            System.out.println("u=>" +u);//抛出异常的错误信息
        }).exceptionally((e)->{
     
//            error 回调
            System.out.println(e.getMessage());
            return 404;
        }).get());
    }
}

whenComplete: 有两个参数,一个是t 一个是u

T:是代表的 正常返回的结果;

U:是代表的 抛出异常的错误信息;

如果发生了异常,get可以获取到exceptionally返回的值;

16.JMM

谈谈对 Volatile 的理解

Volate是Java虚拟机提供轻量级的同步机制

  1. 保证可见性
  2. 不保证原子性
  3. 禁止指令重排

什么是 JMM

JMM: JAVA 内存模型,不存在的东西,是一个概念,也是一个约定!

关于JMM的一些同步的约定:

  1. 线程解锁前,必须把共享变量立刻刷回主存;
  2. 线程加锁前,必须读取主存中的最新值到工作内存中;
  3. 加锁和解锁是同一把锁;

线程中分为 工作内存、主内存

JMM约定有八种操作:

  • Read(读取):作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用;
  • load(载入):作用于工作内存的变量,它把read操作从主存中变量放入工作内存中;
  • Use(使用):作用于工作内存中的变量,它把工作内存中的变量传输给执行引擎,每当虚拟机遇到一个需要使用到变量的值,就会使用到这个指令;
  • assign(赋值):作用于工作内存中的变量,它把一个从执行引擎中接受到的值放入工作内存的变量副本中;
  • store(存储):作用于主内存中的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用;
  • write(写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中;
  • lock(锁定):作用于主内存的变量,把一个变量标识为线程独占状态;
  • unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定;

Java多线程进阶之JUC并发编程教程详解(狂神说学习笔记)_第62张图片
Java多线程进阶之JUC并发编程教程详解(狂神说学习笔记)_第63张图片
问题来了,b刷新了主内存,a没有及时发现

因此,JMM规定了八个规定

  • 不允许read和load、store和write操作之一单独出现。即使用了read必须load,使用了store必须write
  • 不允许线程丢弃他最近的assign操作,即工作变量的数据改变了之后,必须告知主存
  • 不允许一个线程将没有assign的数据从工作内存同步回主内存
  • 一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是对变量实施use、store操作之前,必须经过assign和load操作
  • 一个变量同一时间只有一个线程能对其进行lock。多次lock后,必须执行相同次数的unlock才能解锁
  • 如果对一个变量进行lock操作,会清空所有工作内存中此变量的值,在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值
  • 如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量
  • 对一个变量进行unlock操作之前,必须把此变量同步回主内存

17.Volatile

了解 JMM 八种操作和八个规定,我们开始理解 Volatile,也就是Java虚拟机提供轻量级的同步机制

保证可见性

public class VolatileDemo {
     
    // 如果不加 volatile 程序会死循环
    // 加了volatile 是可以保证可见性的
    private volatile static Integer number = 0;
    public static void main(String[] args) {
     // main 线程
        // 子线程1
        new Thread(()->{
     
            while (number==0){
     }
        }).start();
        try {
     
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
     
            e.printStackTrace();
        }
        number = 1;
        System.out.println(number);
    }
}

测试运行

加了volatile 是可以保证可见性的
Java多线程进阶之JUC并发编程教程详解(狂神说学习笔记)_第64张图片
如果将 volatile 去掉,主线程将number修改为1,子线程不知道,还在按照 number=0一直做死循环
Java多线程进阶之JUC并发编程教程详解(狂神说学习笔记)_第65张图片

不保证原子性

原子性:不可分割;

线程A在执行任务的时候,不能被打扰的,也不能被分割的,要么同时成功,要么同时失败。

代码举例

public class VDemo02 {
     
    private static int number = 0;
    public static void add(){
     
        number++;
        // ++ 不是一个原子操作,是2~3个操作
    }
    public static void main(String[] args) {
     
        // 理论上 number === 20000
        for (int i = 1; i <= 20; i++) {
     
            new Thread(()->{
     
                for (int j = 1; j <= 1000 ; j++) {
     
                    add();
                }
            }).start();
        }
        // java 默认有两个线程 main 和 gc  超过2个说明还有其他线程存活
        // 这里的含义就是 如果main和gc之外还有其他线程,主线程就会礼让,保证其他线程执行完
        while(Thread.activeCount()>2){
     
            Thread.yield();
        }
        System.out.println(Thread.currentThread().getName()+",num + "+number);
    }
}

运行测试,理论上number应该为20000,实际上达不到,因为number++不是原子操作,存在并发问题
Java多线程进阶之JUC并发编程教程详解(狂神说学习笔记)_第66张图片
解决办法之一就是给 add 加锁

public synchronized static void add(){
     
number++;
}

Java多线程进阶之JUC并发编程教程详解(狂神说学习笔记)_第67张图片
去掉synchronized ,给 number 加 volatile

private volatile static int number = 0;

运行测试,发现总数达不到20000,说明 volatile不能保证原子性
在这里插入图片描述
问题:

  • 如果不加 lock 和 synchronized,如何保证原子性?

首先分析 number++的底层实现

通过命令行进入到class文件所在目录,执行指令,查看他的字节码指令逻辑

javap -c VDemo02.class

Java多线程进阶之JUC并发编程教程详解(狂神说学习笔记)_第68张图片
说明 number++ 在底层有多步骤实现

回到这个问题,如果不加 lock 和 synchronized,如何保证原子性

使用原子类解决原子性问题

Java多线程进阶之JUC并发编程教程详解(狂神说学习笔记)_第69张图片
使用原子类定义 number,修改 number++

public class VDemo02 {
     
    // private volatile static int number = 0;
    private static volatile AtomicInteger number = new AtomicInteger();
    public static void add(){
     
        // number++;
        // ++ 不是一个原子操作,是2~3个操作
        number.incrementAndGet();//底层是 CAS 保证原子性
    }
    public static void main(String[] args) {
     
        // 理论上 number === 20000
        for (int i = 1; i <= 20; i++) {
     
            new Thread(()->{
     
                for (int j = 1; j <= 1000 ; j++) {
     
                    add();
                }
            }).start();
        }
        // java 默认有两个线程 main 和 gc  超过2个说明还有其他线程存活
        // 这里的含义就是 如果main和gc之外还有其他线程,主线程就会礼让,保证其他线程执行完
        while(Thread.activeCount()>2){
     
            Thread.yield();
        }
        System.out.println(Thread.currentThread().getName()+",num + "+number);
    }
}

Java多线程进阶之JUC并发编程教程详解(狂神说学习笔记)_第70张图片
为什么能保证原子性,进入源码查看,
在这里插入图片描述
在这里插入图片描述

为什么要使用原子类?

  • 因为他比锁要高效很多

这些类的底层都直接和操作系统挂钩!是在内存中修改值。Unsafe 类是一个很特殊的存在;

禁止指令重排

什么是指令重排?

我们写的程序,计算机并不是按照我们自己写的那样去执行的

源代码–>编译器优化重排–>指令并行也可能会重排–>内存系统也会重排–>执行

int x=1; //1
int y=2; //2
x=x+5;   //3
y=x*x;   //4

//我们期望的执行顺序是 1_2_3_4  可能执行的顺序会变成2134 1324
//可不可能是 4123? 不可能的

因为,处理器在进行指令重排的时候,会考虑数据之间的依赖性!

这个例子是指令重排没有造成影响,也有造成影响的时候

比如,假设abxy默认都是0,执行以下线程操作

线程A 线程B
x = a y = b
b = 1 a = 2

正常的结果应该是 x = 0; y =0;

然而可能会出现以下的执行顺序

线程A 线程B
b=1 a=2
x=a y=b

可能在线程A中会出现,先执行b=1,然后再执行x=a;

在B线程中可能会出现,先执行a=2,然后执行y=b;

那么就有可能结果如下:x=2; y=1.

这种情况可能很难出现,但是还是有一定概率,需要注意

volatile可以避免指令重排

volatile中会加一道内存的屏障,这个内存屏障可以保证在这个屏障中的指令顺序

内存屏障:CPU指令。作用:

1、保证特定的操作的执行顺序;

2、可以保证某些变量的内存可见性(利用这些特性,就可以保证volatile实现的可见性)

Java多线程进阶之JUC并发编程教程详解(狂神说学习笔记)_第71张图片

小结

  • volatile 可以保证可见性
  • 不能保证原子性
  • 由于内存屏障,可以保证避免指令重排的现象产生
  • 面试官:那么你知道在哪里用这个内存屏障用得最多呢?单例模式

18.玩转单例模式

饿汉模式

public class SingleHungryDemo {
     
    /*
    * 可能会浪费空间
    * */
    private byte[] data1 = new byte[1024*1024];
    private byte[] data2 = new byte[1024*1024];
    private byte[] data3 = new byte[1024*1024];
    private byte[] data4 = new byte[1024*1024];
    
    private SingleHungryDemo(){
     
        
    }
    private final static SingleHungryDemo hungry = new SingleHungryDemo();
    public static SingleHungryDemo getInstance(){
     
        return hungry;
    }
}

程序一加载就可能占用大量资源

懒汉模式

// 懒汉式单例,单线程安全
public class LazyMan {
     
    // 构造器私有
    private LazyMan() {
     
        System.out.println(Thread.currentThread().getName() + "ok");
    }
    private static LazyMan lazyMan;
    public static LazyMan getInstance(){
     
        if(lazyMan == null){
     
            lazyMan = new LazyMan();
        }
        return lazyMan;
    }
    // 多线程会出现并发问题
    public static void main(String[] args) {
     
        for (int i= 0;i<10;i++) {
     
            new Thread(()->{
     
                LazyMan.getInstance();
            }).start();
        }
    }
}

但是多线程下有并发问题,这里仍然出现了多个线程,本应该只有一个
Java多线程进阶之JUC并发编程教程详解(狂神说学习笔记)_第72张图片

DCL 懒汉式

因此我们需要加锁

// 懒汉式单例
public class LazyMan {
     
    // 构造器私有
    private LazyMan() {
     
        System.out.println(Thread.currentThread().getName() + "ok");
    }
    private static LazyMan lazyMan;
    // 双重检测模式 简称 DCL
    public static LazyMan getInstance() {
     
        if (lazyMan == null) {
     
            synchronized (LazyMan.class) {
     
                if (lazyMan == null) {
     
                    lazyMan = new LazyMan();
                }
            }
        }
        return lazyMan;
    }
    // 多线程会出现并发问题
    public static void main(String[] args) {
     
        for (int i= 0;i<10;i++) {
     
            new Thread(()->{
     
                LazyMan.getInstance();
            }).start();
        }
    }
}

Java多线程进阶之JUC并发编程教程详解(狂神说学习笔记)_第73张图片
但是 DCL 懒汉式仍然有问题,极端情况下会出现问题

lazyMan = new LazyMan(); 不是原子性操作,里面会经历三步操作

  1. 分配内存空间
  2. 2.执行构造方法,初始化对象
  3. 把这个对象指向这个空间

这就有可能出现指令重排问题

  • 比如执行的顺序是 1 3 2 等
  • 另一个线程也可能会干扰这个线程,导致比如返回的 lazyman为空

为了安全,我们要避免指令重排,我们就可以添加 volatile 保证指令重排问题

静态内部类实现单例

// 静态内部类实现单例
public class Holder {
     
    // 单例模式一定是构造器私有
    private Holder() {
     

    }
    public static Holder getInstance() {
     
        return InnerClass.HOLDER;
    }
    public static class InnerClass {
     
        private static final Holder HOLDER = new Holder();
    }
}

反射可以破坏单例

但是目前,单例仍然不安全,因为可以使用反射

// 懒汉式单例
public class LazyMan {
     
    // 构造器私有
    private LazyMan() {
     
        System.out.println(Thread.currentThread().getName() + "ok");
    }
    private static LazyMan lazyMan;
    // 双重检测模式 简称 DCL
    public static LazyMan getInstance() {
     
        if (lazyMan == null) {
     
            synchronized (LazyMan.class) {
     
                if (lazyMan == null) {
     
                    lazyMan = new LazyMan();
                }
            }
        }
        return lazyMan;
    }
    // 反射
    public static void main(String[] args) throws Exception {
     
        LazyMan instance = LazyMan.getInstance();
        Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
        declaredConstructor.setAccessible(true);// 无视私有构造器
        LazyMan instance2 = declaredConstructor.newInstance();

        System.out.println(instance);
        System.out.println(instance2);
    }
}

运行发现,两个对象不一样,不是单例
Java多线程进阶之JUC并发编程教程详解(狂神说学习笔记)_第74张图片
如何解决这种问题?我们可以在构造器中加锁判断,防止别人使用反射破坏单例

// 构造器私有
    private LazyMan() {
     
         synchronized (LazyMan.class) {
     
             if (lazyMan != null) {
     
                 throw new RuntimeException("不要试图使用反射破坏异常");
             }
         }
        System.out.println(Thread.currentThread().getName() + "ok");
    }

Java多线程进阶之JUC并发编程教程详解(狂神说学习笔记)_第75张图片

但是,如果两个instance都是通过反射创建的,如何判断呢?

LazyMan instance1 = declaredConstructor.newInstance();
LazyMan instance2 = declaredConstructor.newInstance();

我们可以设置一个标志位,来判断对象是否已经被创建了无需再创建,

添加标志位,将构造器改造为

private static boolean swy = false;
    // 构造器私有
    private LazyMan() {
     
         synchronized (LazyMan.class) {
     
             if (swy == false) {
     
                 swy = true;
             } else {
     
                 throw new RuntimeException("不要试图使用反射破坏异常");
             }
         }
        System.out.println(Thread.currentThread().getName() + "ok");
    }

只要构造器被调用,表示位就会变化,然后无法再次创建对象,

除非对方有反编译器看到了标志位,否则仅仅使用反射也不能创建多例,标志也可以再进一步加密,进一步提高了安全性

假设对手更高级,找到了标志位,并通过反射将标志位破坏了

	// 反射
    public static void main(String[] args) throws Exception {
     
        Field swy = LazyMan.class.getDeclaredField("swy");
        swy.setAccessible(true);

        Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
        declaredConstructor.setAccessible(true);// 无视私有构造器
        LazyMan instance = declaredConstructor.newInstance();
        // 通过反射破坏标志位
        swy.set(instance, false);
        
        LazyMan instance2 = declaredConstructor.newInstance();

        System.out.println(instance);
        System.out.println(instance2);
    }

这样就又一次破坏了单例
Java多线程进阶之JUC并发编程教程详解(狂神说学习笔记)_第76张图片
最终得出结论:道高一尺,魔高一丈

都是反射惹的祸,这就需要引入枚举

枚举

反射不能破坏枚举

// 枚举本身是一个类,JDK1.5出现
public enum  EnumSingle {
     
    INSTANCE;
    public EnumSingle getInstance() {
     
        return INSTANCE;
    }
}
class Test {
     
    public static void main(String[] args) {
     
        EnumSingle instance1 = EnumSingle.INSTANCE;
        EnumSingle instance2 = EnumSingle.INSTANCE;
        System.out.println(instance1);
        System.out.println(instance2);
    }
}

Java多线程进阶之JUC并发编程教程详解(狂神说学习笔记)_第77张图片
如果还是要用反射操作呢?

查看target目录中 EnumSingle 编译后的 class 代码
Java多线程进阶之JUC并发编程教程详解(狂神说学习笔记)_第78张图片
发现有无参构造,因此我们尝试再次使用反射

// 枚举本身是一个类,JDK1.5出现
public enum  EnumSingle {
     
    INSTANCE;
    public EnumSingle getInstance() {
     
        return INSTANCE;
    }
}
class Test {
     
    public static void main(String[] args) throws Exception {
     
        EnumSingle instance1 = EnumSingle.INSTANCE;
        Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(null);
        declaredConstructor.setAccessible(true);// 破除私有
        EnumSingle instance2 = declaredConstructor.newInstance();// 反射创建对象
        System.out.println(instance1);
        System.out.println(instance2);
    }
}

运行结果报异常
Java多线程进阶之JUC并发编程教程详解(狂神说学习笔记)_第79张图片
异常的含义是,我们的枚举类中没有空参数构造器

但是我们在IDEA中 查看 EnumSingle.class 显示有无参构造,为什么?

我们使用命令行反编译查看一下,还是无参构造,还是有问题,这个代码也欺骗了我们,因为异常枚举类没有无空参构造
Java多线程进阶之JUC并发编程教程详解(狂神说学习笔记)_第80张图片
我们使用反编译神器 jad.exe

链接:https://pan.baidu.com/s/155pwsY7F4cLnN4KUxhK-ag

提取码:ztyp

把它复制到,class文件所在目录,在这个目录下执行命令行jad -sjava EnumSingle.class,把它反编译成Java文件
在这里插入图片描述
打开这个Java文件,得到枚举类型的最终反编译源码

package com.swy.single;


public final class EnumSingle extends Enum
{
     

    public static EnumSingle[] values()
    {
     
        return (EnumSingle[])$VALUES.clone();
    }

    public static EnumSingle valueOf(String name)
    {
     
        return (EnumSingle)Enum.valueOf(com/swy/single/EnumSingle, name);
    }

    private EnumSingle(String s, int i)
    {
     
        super(s, i);
    }

    public EnumSingle getInstance()
    {
     
        return INSTANCE;
    }

    public static final EnumSingle INSTANCE;
    private static final EnumSingle $VALUES[];

    static 
    {
     
        INSTANCE = new EnumSingle("INSTANCE", 0);
        $VALUES = (new EnumSingle[] {
     
            INSTANCE
        });
    }
}

发现这里使用了一个有参构造器

private EnumSingle(String s, int i)
    {
     
        super(s, i);
    }

所以我们再次改造上面的反射创建枚举对象的代码

class Test {
     
    public static void main(String[] args) throws Exception {
     
        EnumSingle instance1 = EnumSingle.INSTANCE;
        // 通过反编译器发现,枚举使用的是有参构造
        Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(String.class,int.class);
        declaredConstructor.setAccessible(true);// 破除私有
        EnumSingle instance2 = declaredConstructor.newInstance();// 反射创建对象
        System.out.println(instance1);
        System.out.println(instance2);
    }
}

运行测试,终于得到我们想要的结果Java多线程进阶之JUC并发编程教程详解(狂神说学习笔记)_第81张图片
枚举不能通过反射创造对象,所以,枚举不能被反射破坏单例

19.深入理解CAS

为什么深入理解CAS?大厂必须深入研究底层!!!!修内功!操作系统、计算机网络原理、组成原理、数据结构

什么是CAS

CAS:compareAndSet 比较并交换

传统原子类

public class CASDemo {
     
    // CAS: compareAndSet 比较并交换
    public static void main(String[] args){
     
        AtomicInteger atomicInteger = new AtomicInteger(2020);

        // boolean compareAndSet(int expect, int update)
        // 第一个参数:期望值,第二个参数:更新值
        // 如果实际值 和 我期望值相同,那么就更新
        // 如果实际制 和 我期望值不同,那么就不更新
        System.out.println(atomicInteger.compareAndSet(2020,2021));
        System.out.println(atomicInteger.get());

        // 因为期望值是 2020 ,实际值却变成了2021 所以会修改失败
        // CAS 是 CPU 的并发原语
        atomicInteger.getAndIncrement();//++操作
        System.out.println(atomicInteger.compareAndSet(2020,2021));
        System.out.println(atomicInteger.get());
    }
}

Java多线程进阶之JUC并发编程教程详解(狂神说学习笔记)_第82张图片
CAS 是 CPU 的并发原语,意思就是CPU的指令

进入 getAndIncrement 查看
在这里插入图片描述
进入 unsafe

Java多线程进阶之JUC并发编程教程详解(狂神说学习笔记)_第83张图片
Unsafe 是什么?

  • Java无法操作内存
  • Java可以调用C++ 的本地方法 native,操作内存
  • Unsafe 就相当于Java 的后门,通过这个类的方法来操作内存

valueOffset:内存地址的偏移值,而且value也被volatile修饰,保证不被指令重排

Unsafe 类提供的都是本地方法(native),包括上面的 getAndAddInt(+1的操作)
Java多线程进阶之JUC并发编程教程详解(狂神说学习笔记)_第84张图片
Java多线程进阶之JUC并发编程教程详解(狂神说学习笔记)_第85张图片
这个的方法的含义:如果var1这个对象,对应的值var2,是var5,那么就给var5+var4

这是一个内存操作,效率很高

Java 的 CAS 底层就是上述这个CAS,

do {
     
    var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

这段代码本身也是一个自旋锁

小结

CAS:

  • 比较当前工作内存中的值 和 主内存中的值,如果这个值是期望的,那么则执行操作!如果不是就一直循环,使用的是自旋锁。

CAS 缺点:

  • 循环会耗时;(即使这样,也比Java操作好)
  • 一次性只能保证一个共享变量的原子性;
  • 它会存在ABA问题

ABA 问题

  • 了解到CAS,就一定要知道 ABA 问题
  • 一句话解释:狸猫换太子

解释一下:

  • A 期望这是一块蛋糕,如果是,A就进行更新值,这是线程B来操作了蛋糕,换成了蛋糕,然后又换回蛋糕,这个过程A是不知道的,A继续操作这个蛋糕,并且认为这还是以前的蛋糕

Java多线程进阶之JUC并发编程教程详解(狂神说学习笔记)_第86张图片
看代码演示

public class CASDemo {
     
    // CAS: compareAndSet 比较并交换
    public static void main(String[] args){
     
        AtomicInteger atomicInteger = new AtomicInteger(2020);
        
        // 捣乱的线程
        System.out.println(atomicInteger.compareAndSet(2020,2021));
        System.out.println(atomicInteger.get());

        System.out.println(atomicInteger.compareAndSet(2021, 2020));
        System.out.println(atomicInteger.get());

        // 期望的线程
        System.out.println(atomicInteger.compareAndSet(2020,666));
        System.out.println(atomicInteger.get());
    }
}

Java多线程进阶之JUC并发编程教程详解(狂神说学习笔记)_第87张图片
可以看到A不知情,并且正常进行了更新值,(这也是乐观锁的原理)

这是我们不期望的,如何解决这个问题?

20.原子引用

解决 ABA 问题,对应的思想:就是使用了乐观锁

解决办法:带版本号的原子操作!

public class CASDemo {
     
    // CAS: compareAndSet 比较并交换
    public static void main(String[] args){
     
        // AtomicInteger atomicInteger = new AtomicInteger(2020);

        // 给值的同时 添加一个版本号,这里我们定为1
        AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(1, 1);
        // 开启两个线程
        new Thread(()->{
     
            int stamp = atomicStampedReference.getStamp();// 获得版本号
            System.out.println("a1=>"+stamp);
            // 为保证两者的版本号是同一个,我们暂停一下
            try {
     
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
     
                e.printStackTrace();
            }
            // 开始CAS 操作
            // 参数1:期望值,参数2:更新值,参数3:版本号,参数4:版本号下一步的操作
            System.out.println(atomicStampedReference.compareAndSet(1, 2, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1));
            System.out.println("a2=>"+stamp);
            // 再将期望值改回去,但是版本号还在累加
            System.out.println(atomicStampedReference.compareAndSet(2, 1, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1));
            System.out.println("a2=>"+stamp);
        }, "a").start();
        // b线程也对atomicStampedReference操作
        // 和乐观锁原理相同
        new Thread(()->{
     
            int stamp = atomicStampedReference.getStamp();// 获得版本号
            System.out.println("b1=>"+stamp);
            // 为保证两者的版本号是同一个,暂停时间更长一些
            try {
     
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
     
                e.printStackTrace();
            }
            System.out.println(atomicStampedReference.compareAndSet(1, 6, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1));
            System.out.println("b2=>"+stamp);
        }, "b").start();
    }
}

这样,在操作过程中,即使期望值发生了变化,我们也可以通过版本号得知发生了变化
Java多线程进阶之JUC并发编程教程详解(狂神说学习笔记)_第88张图片

这里有个小坑:

  • AtomicStampedReference
  • Integer 使用了对象缓存机制,默认范围是 -128~127,推荐使用静态工厂方法 valueOf 获取对象实例,而不是 new ,因为 valueOf 使用了缓存,而 new 一定会创建新的对象分配新的内存空间。
  • 所以我们的期望值包装类,需要使用范围在 -128~127 才确保是同一个内存空间
  • 当然实际使用时可能是个对象,不会有这个问题
  • 即使是数字的话,我们也可重写equals来解决这个问题

21.各种锁的理解

公平锁、非公平锁

公平锁: 非常公平,先来后到,不允许插队

非公平锁: 非常不公平, 允许插队,默认都是非公平

 	public ReentrantLock() {
     
        sync = new NonfairSync(); //无参默认非公平锁
    }
    // 重载方法可以传参
 	public ReentrantLock(boolean fair) {
     
        sync = fair ? new FairSync() : new NonfairSync();//传参为true为公平锁
    }

可重入锁(递归锁)

释义:

  • 可重入锁指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁(前提得是同一个对象或者class),这样的锁就叫做可重入锁,而且是自动获得的
    Java多线程进阶之JUC并发编程教程详解(狂神说学习笔记)_第89张图片
    synchronized版本的可重入锁
public class Demo01 {
     
    public static void main(String[] args) {
     
        TestPhone phone = new TestPhone();
        new Thread(()->{
     
            //在调用sendMessage的方法时已经为phone加上了一把锁
            //而call方法又为其加上了一把锁
            phone.sendMessage();
        }, "A").start();
        new Thread(()->{
     
            phone.sendMessage();
        }, "B").start();
    }
}
class TestPhone {
     
    public synchronized void sendMessage() {
     
        System.out.println(Thread.currentThread().getName() + "sendMessage");
        call();// 这里也有所
    }
    public synchronized void call() {
     
        System.out.println(Thread.currentThread().getName() + "call");
    }
}

按照正常逻辑,A执行完发短信,应该释放锁,B也有机会在A打电话之前就能发短信,

然而运行测试发现,每次都是A发短信、打电话之后,B才有机会开始发短信

这说明,A进入到内层锁的时候仍然保留着外层的锁

Java多线程进阶之JUC并发编程教程详解(狂神说学习笔记)_第90张图片
Lock 版本的锁

public class Demo02 {
     
    public static void main(String[] args) {
     
        Phone2 phone2  = new Phone2();
        new Thread(()->{
     
            phone2.sms();
        }).start();
        new Thread(()->{
     
            phone2.sms();
        }).start();
    }
}
class Phone2{
     
    Lock lock = new ReentrantLock();
    public void sms(){
     
        lock.lock();//细节:这个两把锁,两个钥匙
        // lock 锁必须配对,否则就是死锁在里面,配对指的就是每加一把锁就要对应一个解锁
        try {
     
            System.out.println(Thread.currentThread().getName()+"=>sms");
            call();
        } catch (Exception e) {
     
            e.printStackTrace();
        } finally {
     
            lock.unlock();
        }
    }
    public void call(){
     
        try {
     
            lock.lock();
            System.out.println(Thread.currentThread().getName()+"=>call");
        } catch (Exception e) {
     
            e.printStackTrace();
        } finally {
     
            lock.unlock();
        }
    }
}

运行测试,也会得到同样的效果,需要注意的是lock/unlock必须成对出现,否则程序会死在里面
Java多线程进阶之JUC并发编程教程详解(狂神说学习笔记)_第91张图片

自旋锁

在上面的案例中,已经看到自旋锁
Java多线程进阶之JUC并发编程教程详解(狂神说学习笔记)_第92张图片
不断地的循环去尝试,直到成功为止

手写代码实现一个自旋锁

public class SpinlockDemo {
     
    // 默认
    // int为 0
    // thread 为 null
    AtomicReference<Thread> atomicReference=new AtomicReference<>();
    // 加锁
    public void myLock(){
     
        Thread thread = Thread.currentThread();
        System.out.println(thread.getName()+"===> mylock");
        //自旋锁
        while (!atomicReference.compareAndSet(null,thread)){
     
            System.out.println(Thread.currentThread().getName()+" ==> 自旋中~");
        }
    }
    // 解锁
    public void myUnlock(){
     
        Thread thread=Thread.currentThread();
        System.out.println(thread.getName()+"===> myUnlock");
        atomicReference.compareAndSet(thread,null);
    }
}

开始使用这个自旋锁,t2直到发现满足条件才 可以执行,一直在自旋

public class TestSpinLock {
     
    public static void main(String[] args) throws InterruptedException {
     
        // 这是Java提供的锁,我们暂时不用
        ReentrantLock reentrantLock = new ReentrantLock();
        reentrantLock.lock();
        reentrantLock.unlock();
        // 使用CAS实现我们写的自旋锁
        SpinlockDemo spinlockDemo = new SpinlockDemo();
        new Thread(()->{
     
            spinlockDemo.myLock();
            try {
     
                TimeUnit.SECONDS.sleep(2);
            } catch (Exception e) {
     
                e.printStackTrace();
            } finally {
     
                spinlockDemo.myUnlock();
            }
        },"t1").start();
        TimeUnit.SECONDS.sleep(1);
        new Thread(()->{
     
            spinlockDemo.myLock();
            try {
     
                TimeUnit.SECONDS.sleep(2);
            } catch (Exception e) {
     
                e.printStackTrace();
            } finally {
     
                spinlockDemo.myUnlock();
            }
        },"t2").start();
    }
}

死锁

比如说,两个线程互相抢夺资源,谁都不释放锁就会形成死锁
Java多线程进阶之JUC并发编程教程详解(狂神说学习笔记)_第93张图片
怎么排除死锁,让死锁的四个条件中至少有一个不成立

public class DeadLock {
     
    public static void main(String[] args) {
     
        String lockA= "lockA";
        String lockB= "lockB";
        new Thread(new MyThread(lockA,lockB),"t1").start();
        new Thread(new MyThread(lockB,lockA),"t2").start();
    }
}
class MyThread implements Runnable{
     
    private String lockA;
    private String lockB;
    public MyThread(String lockA, String lockB) {
     
        this.lockA = lockA;
        this.lockB = lockB;
    }
    @Override
    public void run() {
     
        synchronized (lockA){
     
            System.out.println(Thread.currentThread().getName()+" lock"+lockA+"===>get"+lockB);
            try {
     
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
     
                e.printStackTrace();
            }
            synchronized (lockB){
     
                System.out.println(Thread.currentThread().getName()+" lock"+lockB+"===>get"+lockA);
            }
        }
    }
}

程序卡死,两个线程都拿着对方想要的锁不释放
Java多线程进阶之JUC并发编程教程详解(狂神说学习笔记)_第94张图片
当程序卡着不输出,如何排查是死锁问题?

使用Java自带工具

  1. 使用 jps 定位进程号

在命令行输入 jps -l 查看目前运行的Java进程
Java多线程进阶之JUC并发编程教程详解(狂神说学习笔记)_第95张图片
2. 使用 jstack 进程号查看进程信息,找到死锁问题
在这里插入图片描述
查看详细的堆栈信息,通常可以在最后找到
Java多线程进阶之JUC并发编程教程详解(狂神说学习笔记)_第96张图片
面试中,工作如何排查

大部分人会说查看日志,但是你可以说查看堆栈信息!

你可能感兴趣的:(Java进阶,juc,java,多线程,锁)