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

本文详细介绍了Java并发编程中的JUC(java.util.concurrent)包,包括线程和进程的概念、并发与并行的区别、线程状态以及wait/sleep的区别。重点讲解了Lock锁,对比了synchronized与Lock的差异,并通过生产者消费者问题展示了不同锁的使用。文章还涉及了集合类的安全问题,如ArrayList、HashSet、HashMap在并发环境下的不安全性及解决方案。此外,深入探讨了线程池、四大函数式接口、Stream流式计算、ForkJoin框架、异步回调、JMM内存模型、Volatile特性和单例模式。最后,讲解了CAS、原子引用、锁的类型以及死锁问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

文章目录

环境准备

创建普通的maven项目,确保我们的项目一定是在Java8的环境下,检查settings中的以下几点
在这里插入图片描述
在这里插入图片描述
添加一个 lombok 依赖

1.JUC 简介

什么是 JUC ?

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

在这里插入图片描述
为什么使用 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 文档
在这里插入图片描述
在这里插入图片描述
可以看到,Lock是一个接口,有三个实现类,现在我们使用 ReentrantLock 就够用了

查看 ReentrantLock 源码,构造器
在这里插入图片描述
公平非公平:

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

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();
        }
    }
}

运行,发现,多线程都有几率抢到票,且没有出现线程安全问题
在这里插入图片描述
综述,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个线程是否安全?

  • 不安全,会有虚假唤醒

在这里插入图片描述
查看 api 文档
在这里插入图片描述
解决办法:if 判断改为 while,防止虚假唤醒

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

修改代码为:

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

锁、等待、唤醒 都进行了更换
在这里插入图片描述
在这里插入图片描述
将代码改造为 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();
        }
    }
}

在这里插入图片描述
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();
        }
    }
}

在这里插入图片描述

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("打电话");
    }
}

经过测试,一直是先发短信
在这里插入图片描述

问题2:如果发短信延迟2秒,谁先执行
public class Test1 {
   
   
    public static void main(String[] args) {
   
   
        Phone phone = new Phone()
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值