JUC基础

JUC基础

一、JUC概述

1、JUC简介

在 Java 5.0 提供了 java.util.concurrent (简称

JUC )包,在此包中增加了在并发编程中很常用

的实用工具类,用于定义类似于线程的自定义子

系统,包括线程池、异步 IO 和轻量级任务框架。

提供可调的、灵活的线程池。还提供了设计用于

多线程上下文中的 Collection 实现等。

2、线程和进程的概念

进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系
统进行资源分配和调度的基本单位,是操作系统结构的基础。在当代面向线程
设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的
描述,进程是程序的实体。是计算机中的程序关于某数据集合上的一次运行活
动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。程序是
指令、数据及其组织形式的描述,进程是程序的实体。

总结:

进程:指的是在系统中正在运行的一个应用程序,程序一旦运行就是进程,进程是进行资源调度和分配的最小单位。

线程:系统分配处理器时间资源的基本单元,或者说进程之内独立执行的一个单元执行流。线程是程序执行和调度的最小单位。

3、线程的状态

3.1、线程状态枚举类

Thread.State

public enum State {
   /**
     * Thread state for a thread which has not yet started.
     */
    NEW,(新建)

    /**
     * Thread state for a runnable thread.  A thread in the runnable
     * state is executing in the Java virtual machine but it may
     * be waiting for other resources from the operating system
     * such as processor.
     */
    RUNNABLE,(准备就绪)

    /**
     * Thread state for a thread blocked waiting for a monitor lock.
     * A thread in the blocked state is waiting for a monitor lock
     * to enter a synchronized block/method or
     * reenter a synchronized block/method after calling
     * {@link Object#wait() Object.wait}.
     */
    BLOCKED,(阻塞等待)

    /**
     * Thread state for a waiting thread.
     * A thread is in the waiting state due to calling one of the
     * following methods:
     * 
    *
  • {@link Object#wait() Object.wait} with no timeout
  • *
  • {@link #join() Thread.join} with no timeout
  • *
  • {@link LockSupport#park() LockSupport.park}
  • *
* *

A thread in the waiting state is waiting for another thread to * perform a particular action. * * For example, a thread that has called Object.wait() * on an object is waiting for another thread to call * Object.notify() or Object.notifyAll() on * that object. A thread that has called Thread.join() * is waiting for a specified thread to terminate. */ WAITING,(不见不散) /** * Thread state for a waiting thread with a specified waiting time. * A thread is in the timed waiting state due to calling one of * the following methods with a specified positive waiting time: *

    *
  • {@link #sleep Thread.sleep}
  • *
  • {@link Object#wait(long) Object.wait} with timeout
  • *
  • {@link #join(long) Thread.join} with timeout
  • *
  • {@link LockSupport#parkNanos LockSupport.parkNanos}
  • *
  • {@link LockSupport#parkUntil LockSupport.parkUntil}
  • *
*/
TIMED_WAITING,(过时不候) /** * Thread state for a terminated thread. * The thread has completed execution. */ TERMINATED;(终结) }
3.2、wait/sleep的区别

wait ()和sleep ()的关键的区别在于,wait ()是用于线程间通信的,而sleep ()是用于短时间暂停当前线程。

sleep 一般用于当前线程休眠,或者轮循暂停操作,wait 则多用于多线程之间的通信。

(1)sleep是Thread的静态方法,wait是Object的方法,任何对象实例都
能调用。
(2)sleep不会释放锁,它也不需要占用锁。wait会释放锁,但调用它的前提
是当前线程占有锁(即代码要在synchronized中)。+
(3)它们都可以被interrupted方法中断。

(4)wait 还需要额外的方法 notify/ notifyAll 进行唤醒,它们同样需要放在 synchronized 块里面,且获取对象的锁

4、并发和并行
4.1、串行模式

串行表示所有任务都一一按先后顺序进行。串行意味着必须先装完一车柴才能
运送这车柴,只有运送到了,才能卸下这车柴,并且只有完成了这整个三个步
骤,才能进行下一个步骤。
串行是一次只能取得一个任务,并执行这个任务。

4.2、并行模式

并行意味着可以同时取得多个任务,并同时去执行所取得的这些任务。并行模
式相当于将长长的一条队列,划分成了多条短队列,所以并行缩短了任务队列的长度。并行的效率从代码层次上强依赖于多进程/多线程代码,从硬件角度上
则依赖于多核CPU。

4.3、并发

并发(concurrent)指的是多个程序可以同时运行的现象,更细化的是多进程可
以同时运行或者多指令可以同时运行。但这不是重点,在描述并发的时候也不
会去扣这种字眼是否精确,=并发的重点在于它是一种现象==,并发描述
的是多进程同时运行的现象
。但实际上,对于单核心CPU来说,同一时刻
只能运行一个线程。所以,这里的"同时运行"表示的不是真的同一时刻有多个
线程运行的现象,这是并行的概念,而是提供一种功能让用户看来多个程序同
时运行起来了,但实际上这些程序中的进程不是一直霸占CPU的,而是执行一
会停一会。

要解决大并发问题,通常是将大任务分解成多个小任务,由于操作系统对进程的
调度是随机的,所以切分成多个小任务后,可能会从任一小任务处执行。这可
能会出现一些现象:

  1. 可能出现一个小任务执行了多次,还没开始下个任务的情况。这时一般会采用队列或类以的数据结构来存放各个小任务的成果

  2. 可能出现还没准备好第一步就执行第二步的可能。这时,一般采用多路复用或异步的方式,比如只有准备好产生了事件通知才执行某个任务。

  3. 可以多进程/多线程的方式并行执行这些小任务。也可以单进程/单线程执行这
    些小任务,这时很可能要配合多路复用才能达到较高的效率

4.4、小结(重点)

并发:同一时刻多个线程在访问同一个资源,多个线程对一个点
例子:春运抢票电商秒杀…
并行:多项工作一起执行,之后再汇总
例子:泡方便面,电水壶烧水,一边撕调料倒入桶中。

并发:指两个或多个事件在同一时间间隔内发生

并行:指两个或多个事件在同一时刻发生

5、管程

管程的理解_ZhangJiQun&MXP的博客-CSDN博客

管程 (Moniters,也称为监视器)
一.管程的概念
  1. 是一种程序结构,结构内的多个子程序(对象或模块)形成的多个工作线程互斥访问共享资源。这些共享资源一般是硬件设备或一群变量。管程实现了在一个时间点,最多只有一个线程在执行管程的某个子程序。
  2. 与那些通过修改数据结构实现互斥访问的并发程序设计相比,管程实现很大程度上简化了程序设计。
  3. 管程提供了一种机制,线程可以临时放弃互斥访问,等待某些条件得到满足后,重新获得执行权恢复它的互斥访问。即:在管程中的线程可以临时放弃管程的互斥访问,让其他线程进入到管程中来。
  4. 管程包含:

​ 多个彼此可以交互并共用资源的线程

​ 多个与资源使用有关的变量

​ 一个互斥锁

​ 一个用来避免竞态条件的不变量

  1. 一个管程的程序在运行一个线程前会先取得互斥锁,直到完成线程或是线程等待某个条件被满足才会放弃互斥锁。若每个执行中的线程在放弃互斥锁之前都能保证不变量成立,则所有线程皆不会导致竞态条件成立。
  2. 管程是一种高级的同步原语。**任意时刻管程中只能有*一个活跃进程*。**它是一种编程语言的组件,所以编译器知道它们很特殊,并可以采用与其他过程调用不同的方法来处理它们。典型地,当一个进程调用管程中的过程,前几条指令将检查在管程中是否有其他的活跃进程。如果有,调用进程将挂起,直到另一个进程离开管程。如果没有,则调用进程便进入管程。
  3. 对管程的实现互斥由编译器负责!在Java中,只要将关键字synchronized加入到方法声明中,Java保证一旦某个线程执行该方法,就不允许其他线程执行该方法,就不允许其他线程执行该类中的任何其他方法。
  4. 注意:管程是一个编程语言概念。编译器必须要识别出管程并用某种方式对互斥做出安排。C、Pascal及多数其他语言都没有管程,所以指望这些编译器来实现互斥规则是不可靠的。
  5. 管程可以看做一个软件模块,它是将共享的变量和对于这些共享变量的操作封装起来,形成一个具有一定接口的功能模块,进程可以调用管程来实现进程级别的并发控制。
  6. 进程只能互斥地使用管程,即当一个进程使用管程时,另一个进程必须等待。当一个进程使用完管程后,它必须释放管程并唤醒等待管程的某一个进程。
  7. 在管程入口处的等待队列称为入口等待队列,由于进程会执行唤醒操作,因此可能有多个等待使用管程的队列,这样的队列称为紧急队列,它的优先级高于等待队列。
二、 管程的特征
  1. 模块化。

管程是一个基本的软件模块,可以被单独编译。

  1. 抽象数据类型。

管程中封装了数据及对于数据的操作,这点有点像面向对象编程语言中的类。

  1. 信息隐藏。

管程外的进程或其他软件模块只能通过管程对外的接口来访问管程提供的操作,管程内部的实现细节对外界是透明的。

  1. 使用的互斥性。

任何一个时刻,管程只能由一个进程使用。进入管程时的互斥由编译器负责完成。

三、 enter过程、leave过程、条件型变量c、wait© 、signal©
  1. enter过程

一个进程进入管程前要提出申请,一般由管程提供一个外部过程–enter过程。如Monitor.enter()表示进程调用管程Monitor外部过程enter进入管程。

  1. leave过程

当一个进程离开管程时,如果紧急队列不空,那么它就必须负责唤醒紧急队列中的一个进程,此时也由管程提供一个外部过程—leave过程,如Monitor.leave()表示进程调用管程Monitor外部过程leave离开管程。

  1. 条件型变量c

条件型变量c实际上是一个指针,它指向一个等待该条件的PCB队列。如notfull表示缓冲区不满,如果缓冲区已满,那么将要在缓冲区写入数据的进程就要等待notfull,即wait(notfull)。相应的,如果一个进程在缓冲区读数据,当它读完一个数据后,要执行signal(notempty),表示已经释放了一个缓冲区单元。

  1. wait©

wait©表示为进入管程的进程分配某种类型的资源,如果此时这种资源可用,那么进程使用,否则进程被阻塞,进入紧急队列。

  1. signal©

signal©表示进入管程的进程使用的某种资源要释放,此时进程会唤醒由于等待这种资源而进入紧急队列中的第一个进程。

6、用户线程和守护线程

用户线程:自定义线程

守护线程:垃圾回收…

二者其实基本上是一样的。唯一的区别在于JVM何时离开。

用户线程:当存在任何一个用户线程未离开,JVM是不会离开的。

守护线程:如果只剩下守护线程未离开,JVM是可以离开的。

在Java中,制作守护线程非常简单,直接利用.setDaemon(true)

6.1、用户线程

定义:平时用到的普通线程均是用户线程,当在Java程序中创建一个线程,它就被称为用户线程

不需要内核支持而在用户程序中实现的线程,其不依赖于操作系统核心,应用进程利用线程库提供创建、同步、调度和管理线程的函数来控制用户线程。这种线程甚至在象 DOS 这样的操作系统中也可实现,但线程的调度需要用户程序完成,这有些类似 Windows 3.x 的协作式多任务。

优点

1.线程位于用户空间(即不需要模式切换)。
2.完全控制线程调度器(例如:网站服务器)。
3.独立于操作系统(线程可以在不支持它们的操作系统上运行)。
4.运行时系统(run time system)可以切换用户空间中的本地阻塞线程(例如:等待另一个线程完成).

缺点

1.系统调度中,对一个线程的阻塞将会导致整个进程阻塞;(例如:当一个线程因 I/O 而处于等待状态时,整个进程就会被调度程序切换为等待状态,其他线程得不到运行的机会)
2.网站服务器中,一个页面的错误将导致整个进程阻塞。
3.非真正意义的线程并行(一个进程安排在单个CPU上)。
4.不存在时钟中断(例如,如果用户线程是非抢占式(non-preemptive)的,将无法被“进程调度”(schedulers)以round-robin的调度算法调用,因为round-robin调度算法中限制了cpu时间片)。
用户线程不需要额外的内核开支,并且用户态线程的实现方式可以被定制或修改以适应特殊应用的要求,但是;而内核线程则没有这个限制,有利于发挥多处理器的并发优势,但却占用了更多的系统开支。

抢占式和非抢占式

非抢占式是一种进程调度的方式,让原来正在运行的进程继续运行,直至该进程完成或发生某种事件(如I/O请求),才主动放弃处理机,让进程运行直到结束或阻塞的调度方式,容易实现,抢占式(Preemptive)
允许将逻辑上可继续运行的在运行过程暂停的调度方式
可防止单一进程长时间独占CPU
系统开销大(降低途径:硬件实现进程切换,或扩充主存以贮存大部分程序)

抢占式与非抢占式的区分
一个新创建的进程首先被放置在Ready队列,它一直等待执行的机会。一旦内核调度器将CPU分配给它开始执行时,有四种可能:
(1)进程主动发起I/O请求,但I/O设备还没有准备好,所以会发生I/O阻塞,进程进入Wait状态。
(2)内核分配给进程的时间片已经耗尽了,进程进入Ready状态,等待内核重新分配时间片后的执行机会。
(3)进程创建了子进程,并调用wait()等待子进程执行完毕,进程就重新进入Ready状态等待阻塞结束。
(4)I/O设备可以在任意时刻发生中断,CPU会停下当前正在执行的进程去处理中断,因此进程进入Ready状态。
区分一个多任务分时系统是抢占式的还是非抢占式的,则要看进程调度能否在(4)发生中断,CPU停止当前手头的工作(正在执行的进程),保存下当前工作的现场后,转入中断处理程序。如果在中断处理程序的执行中能否发生调度,即中断处理程序还没有执行完,又切换到其他进程。这里要说明的是,系统调用也是通过中断机制来实现的。所以,也就是说要看系统调用的执行过程中,或者中断处理程序的执行过程中能否发生调度(抢占)。

6.2、守护线程

定义:是个服务线程,准确地来说就是服务其他的线程,这是它的作用——而其他的线程只有一种,那就是用户线程。所以java里线程分2种,
1、守护线程,比如垃圾回收线程,就是最典型的守护线程。
2、用户线程,就是应用程序里的自定义线程

6.3、小例子
package com.lxg.juc;

/**
 * @auther xiaolin
 * @creatr 2023/4/13 11:20
 */
public class ThreadDemo {
    public static void main(String[] args) {
        Thread aa = new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "::" + Thread.currentThread().isDaemon());
            while (true) {

            }
        }, "aa");
        
        aa.start();
        System.out.println(Thread.currentThread().getName()+" is over");
    }
}

主线程结束了,但是还有用户进程存在,jvm就仍然存活,程序还未停止

package com.lxg.juc;

/**
 * @auther xiaolin
 * @creatr 2023/4/13 11:20
 */
public class ThreadDemo {
    public static void main(String[] args) {
        Thread aa = new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "::" + Thread.currentThread().isDaemon());
            while (true) {

            }
        }, "aa");
        //设置为守护线程
        aa.setDaemon(true);
        aa.start();
        System.out.println(Thread.currentThread().getName()+" is over");
    }
}

主线程结束了,只剩下守护进程,jvm也离开了,程序停止

二、Lock接口

1、复习Synchronized

1.1、Synchronized关键字回顾

synchronized是Java中的关键字,是一种同步锁。它修饰的对象有以下几种:

  1. 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号}括起来的代码,作用的对象是调用这个代码块的对象;

  2. 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象

    虽然可以使用synchronized来定义方法,但synchronized并不属于方法定
    义的一部分,因此,synchronized关键字不能被继承。如果在父类中的某个方
    法使用了synchronized关键字,而在子类中覆盖了这个方法,在子类中的这
    个方法默认情况下并不是同步的,而必须显式地在子类的这个方法中加上
    synchronized关键字才可以。当然,还可以在子类方法中调用父类中相应的方
    法,这样虽然子类中的方法不是同步的,但子类调用了父类的同步方法,因此
    子类的方法也就相当于同步了。

  3. 修改一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的
    所有对象

  4. 修改一个类,其作用的范围是synchronized后面括号括起来的部分,作用主
    的对象是这个类的所有对象。

1.2、多线程编程步骤

第一:创建资源类,创建属性和操作方法

第二:在资源类操作方法

  1. 判断
  2. 干活
  3. 通知

第三:创建多线程调用资源类的方法

第四:防止虚假唤醒问题

1.3、Synchronized实现卖票例子
package com.lxg.juc.sync;

/**
 * @auther xiaolin
 * @creatr 2023/4/13 12:47
 */

//第一步 创建资源类,定义属性和操作方法
class Ticket{
    //票数
    private int number = 30;
    public int getNumber(){
        return number;
    }
    //卖票的方法
    public synchronized void sale(){
        //判断:是否有票
        if(number > 0){
            System.out.println(Thread.currentThread().getName()+"\t卖出第:"+(number--)+"张票\t还剩下:"+number+"张票");
        }else{
            System.out.println("你来晚了,票已经卖完了!");
        }
    }
}



public class SaleTicket {
    //第二步 创建多个线程,调用资源类的操作方法
    public static void main(String[] args) {
        //创建Ticket对象
        Ticket ticket = new Ticket();
        new Thread(()->{

            //调用卖票方法
            while (ticket.getNumber()>0){
                try {
                    Thread.sleep(100);
                    //t1休息100毫秒
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                ticket.sale();
            }
        },"售票员1号").start();
        new Thread(()->{
            //调用卖票方法
            while(ticket.getNumber()>0){
                try {
                    Thread.sleep(100);
                    //t1休息100毫秒
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                ticket.sale();
            }
        },"售票员2号").start();
        new Thread(()->{
            //调用卖票方法
            while(ticket.getNumber()>0){
                try {
                    Thread.sleep(100);
                    //t1休息100毫秒
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                ticket.sale();
            }
        },"售票员3号").start();
    }
}

2、什么是Lock接口

java.util.concurrent.locks包下常用的类与接口(lock是jdk 1.5后新增的)

JUC基础_第1张图片

Lock和ReadWriteLock是两大锁的根接口,Lock代表实现类是ReentrantLock(可重入锁),ReadWriteLock(读写锁)的代表实现类是ReentrantReadWriteLock

Condition 接口描述了可能会与锁有关联的条件变量。这些变量在用法上与使用 Object.wait 访问的隐式监视器类似,但提供了更强大的功能。需要特别指出的是,单个 Lock 可能与多个 Condition 对象关联

2.1、可重入锁

可重入锁是一种支持同一个线程重复获取锁的锁,也叫做递归锁。当一个线程获取了一个可重入锁后,它可以在不释放锁的情况下再次获取该锁,而其他线程则无法获取该锁。可重入锁避免了死锁的发生,并且在多层嵌套调用中能够保证锁的正确性。常见的可重入锁实现包括ReentrantLock和synchronized关键字。

假设有两个线程A和B,它们都需要获取资源R1和R2才能执行相应的操作。如果A先获取了R1,B先获取了R2,那么它们就会互相等待对方释放资源,从而导致死锁。但是,如果使用可重入锁来保护资源R1和R2,那么当A获取了R1后,它可以再次获取R2的锁,而不会被阻塞,因为它已经持有了R1的锁。同样的,当B获取了R2的锁后,也可以再次获取R1的锁,而不会被阻塞。这样就避免了死锁的发生。

如下:两个方法同时被加锁,如果首先执行method1,则该线程拥有Test对象的锁,但如果synchronized不是可重入锁,当method1方法调用method2,发现method2需要等待method1锁释放,但是method1的执行又必须依赖method2,于是循环等待,形成死锁。

此时由于synchronized是可重入锁,method1调用method2方法时再次申请仍然可以得到Test对象的锁,避免了死锁问题。

public class Test{
    public synchronized void method1(){
        method2();
    }
 
    public synchronized void method2(){
 
    }
}

ReentrantLock:

import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockDemo {
    private final ReentrantLock lock = new ReentrantLock();

    public void outer() {
        lock.lock(); // 第一次获取锁
        try {
            System.out.println("outer method");
            inner(); // 调用inner方法
        } finally {
            lock.unlock(); // 释放锁
        }
    }

    public void inner() {
        lock.lock(); // 第二次获取锁,因为是同一个线程,所以可以再次获取
        try {
            System.out.println("inner method");
        } finally {
            lock.unlock(); // 释放锁
        }
    }

    public static void main(String[] args) {
        ReentrantLockDemo demo = new ReentrantLockDemo();
        demo.outer();
    }
}
package com.lxg.juc.sync;

import java.util.concurrent.locks.ReentrantLock;

//可重入锁案例
public class ReentrantLockExample {
    private static ReentrantLock lock = new ReentrantLock(); // 创建一个可重入锁实例
    private static int counter = 0; // 共享资源,计数器

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> { // 创建线程 t1
            for (int i = 0; i < 100000; i++) {
                lock.lock(); // 获取锁
                try {
                    counter++; // 对共享资源进行操作
                } finally {
                    lock.unlock(); // 释放锁
                }
            }
        });

        Thread t2 = new Thread(() -> { // 创建线程 t2
            for (int i = 0; i < 100000; i++) {
                lock.lock(); // 获取锁
                try {
                    counter--; // 对共享资源进行操作
                } finally {
                    lock.unlock(); // 释放锁
                }
            }
        });

        t1.start(); // 启动线程 t1
        t2.start(); // 启动线程 t2

        try {
            t1.join(); // 等待线程 t1 结束
            t2.join(); // 等待线程 t2 结束
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("Counter value: " + counter); // 输出最终的共享资源值
    }
}

结果为0,不会死锁

2.2、Lock

L0ck锁实现提供了比使用同步方法和语句可以获得的更广泛的锁操作。它们允
许更灵活的结构,可能具有非常不同的属性,并且可能支持多个关联的条件对
象。Lock提供了比synchronized更多的功能。

Lock与的Synchronized区别:
Lock不是Java语言内置的,synchronized是Java语言的关键字,因此是内
置特性。Lock是一个类,通过这个类可以实现同步访问;
Lock和synchronized有一点非常大的不同,采用synchronized不需要用户
去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,
系统会自动让线程释放对锁的占用;而Lock则必须要用户去手动释放锁,如
果没有主动释放锁,就有可能导致出现死锁现象。

Lock和synchronized有以下几点不同:

1.Lock是一个接口,而synchronized是Java中的关键字,synchronized是内
置的语言实现F
2.synchronized在发生异常时,会自动释放线程占有的锁,因此比不会导致死锁现
象发生;而Lock在发生异常时,如果没有主动通过unLockO去释放锁,则很
可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;+
3.Lock可以让等待锁的线程响应中断,而synchronized却不行,使用
synchronized时,等待的线程会一直等待下去,不能够响应中断;
4.通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。
5.Lock可以提高多个线程进行读操作的效率。

在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源
非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于
synchronized。

3、创建线程的多种方式

  1. 继承Thread类并重写run方法
public class MyThread extends Thread {
    public void run() {
        System.out.println("Hello from MyThread!");
    }
}

// 创建并启动线程
MyThread thread = new MyThread();
thread.start();
  1. 实现Runnable接口
public class MyRunnable implements Runnable {
    public void run() {
        System.out.println("Hello from MyRunnable!");
    }
}

// 创建并启动线程
MyRunnable runnable = new MyRunnable();
Thread thread = new Thread(runnable);
thread.start();
  1. 使用Executor框架
ExecutorService executor = Executors.newSingleThreadExecutor();
executor.submit(() -> {
    System.out.println("Hello from Executor!");
});
executor.shutdown();
  1. 使用Callable和Future接口
Callable<String> callable = () -> {
    return "Hello from Callable!";
};

ExecutorService executor = Executors.newSingleThreadExecutor();
Future<String> future = executor.submit(callable);
System.out.println(future.get());
executor.shutdown();

4、使用Lock实现卖票例子

package com.lxg.juc.lock;

import java.util.concurrent.locks.ReentrantLock;

/**
 * @auther xiaolin
 * @creatr 2023/4/13 13:46
 */
//第一步 创建资源类,定义属性和操作方法
class LockTicket{
    //票数
    private int number = 50;
    public int getNumber(){
        return number;
    }
    //创建lock可重入锁
    private final ReentrantLock lock = new ReentrantLock();
    //卖票
    public void sale(){
        //上锁
        lock.lock();
        try{
            if(number>0){
                System.out.println(Thread.currentThread().getName()+" 正在售卖第"+(number--)+"张票,"
                        +"剩下"+number+"张票");
            }else{
                System.out.println("你手速慢了,票已经全部卖完!");
            }
        }catch(Exception e){
            e.printStackTrace();
        }finally{
            //解锁
            lock.unlock();
        }
    }
}

public class LockSaleTicket {

    public static void main(String[] args) {
        LockTicket lockTicket = new LockTicket();
        //第二步 创建多个线程,调用资源类的操作方法
        new Thread(()->{
            while(lockTicket.getNumber()>0){
                //售票中
                try {
                    Thread.sleep(100);
                    //t1休息100毫秒
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                lockTicket.sale();
            }
        },"1号售票员").start();
        new Thread(()->{
            while(lockTicket.getNumber()>0){
                //售票中
                try {
                    Thread.sleep(100);
                    //t1休息100毫秒
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                lockTicket.sale();
            }
        },"2号售票员").start();
        new Thread(()->{
            while(lockTicket.getNumber()>0){
                //售票中
                try {
                    Thread.sleep(100);
                    //t1休息100毫秒
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                lockTicket.sale();
            }
        },"3号售票员").start();
    }
}

三、线程间通信

1、Synchronized

1.1、首发版本
package com.lxg.juc.sync;

/**
 * @auther xiaolin
 * @creatr 2023/4/13 23:21
 */

//第一步 创建资源类,定义属性和操作方法
class Share {
    //初始值
    private int number = 0;

    //+1操作
    public synchronized void increase() throws InterruptedException {
        //第二步 判断 干活 通知
        if (number != 0) { //判断number的值是否是0.如果不是0,等待
            this.wait();//从哪里睡,从哪里醒
        }
        //如果是0,就+1
        number++;
        System.out.println(Thread.currentThread().getName() + "::" + number);
        //通知其他线程
        this.notifyAll();
    }

    //-1操作
    public synchronized void decrease() throws InterruptedException {
        //第二步 判断 干活 通知
        if (number != 1) { //判断number的值是否是1.如果不是1,等待
            this.wait();
        }
        //如果是1,就-1
        number--;
        System.out.println(Thread.currentThread().getName() + "::" + number);
        //通知其他线程
        this.notifyAll();
    }

}

public class ThreadDemo1 {

    //第三步 创建多个线程,调用资源类的操作方法
    public static void main(String[] args) {
        Share share = new Share();
        new Thread(()->{
            for (int i = 1; i <= 10; i++) {
                try {
                    share.increase();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        },"A").start();
        new Thread(()->{
            for (int i = 1; i <= 10; i++) {
                try {
                    share.decrease();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        },"B").start();

        new Thread(()->{
            for (int i = 1; i <= 10; i++) {
                try {
                    share.increase();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        },"C").start();
        new Thread(()->{
            for (int i = 1; i <= 10; i++) {
                try {
                    share.decrease();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        },"D").start();
    }

}

1.2、虚假唤醒

虚假唤醒就是在多线程执行过程中,线程间的通信未按照我们幻想的顺序唤醒,在不该唤醒的情况下唤醒了。故出现数据不一致等不符合我们预期的结果

wait在哪里睡就会在哪里被唤醒

虚假唤醒会导致线程在不需要执行的情况下被唤醒,从而浪费CPU资源,降低程序的性能。为了避免虚假唤醒,可以在使用wait()方法时,将wait()方法放在while循环中,并且在while循环中添加线程的条件判断。这样可以在发生虚假唤醒时,让线程继续等待。

1.3、改进版本
package com.lxg.juc.sync;

/**
 * @auther xiaolin
 * @creatr 2023/4/13 23:21
 */

//第一步 创建资源类,定义属性和操作方法
class Share {
    //初始值
    private int number = 0;

    //+1操作
    public synchronized void increase() throws InterruptedException {
        //第二步 判断 干活 通知
        while (number != 0) { //判断number的值是否是0.如果不是0,等待
            this.wait();//从哪里睡,从哪里醒
        }
        //如果是0,就+1
        number++;
        System.out.println(Thread.currentThread().getName() + "::" + number);
        //通知其他线程
        this.notifyAll();
    }

    //-1操作
    public synchronized void decrease() throws InterruptedException {
        //第二步 判断 干活 通知
        while (number != 1) { //判断number的值是否是1.如果不是1,等待
            this.wait();
        }
        //如果是1,就-1
        number--;
        System.out.println(Thread.currentThread().getName() + "::" + number);
        //通知其他线程
        this.notifyAll();
    }

}

public class ThreadDemo1 {

    //第三步 创建多个线程,调用资源类的操作方法
    public static void main(String[] args) {
        Share share = new Share();
        new Thread(()->{
            for (int i = 1; i <= 10; i++) {
                try {
                    share.increase();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        },"A").start();
        new Thread(()->{
            for (int i = 1; i <= 10; i++) {
                try {
                    share.decrease();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        },"B").start();

        new Thread(()->{
            for (int i = 1; i <= 10; i++) {
                try {
                    share.increase();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        },"C").start();
        new Thread(()->{
            for (int i = 1; i <= 10; i++) {
                try {
                    share.decrease();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        },"D").start();
    }

}

将if改为while

2、notify和nitifyall

Java中notify和notifyAll的区别
  1. Java提供了两个方法notify和notifyAll来唤醒在某些条件下等待的线程,你可以使用它们中的任何一个,但是Java中的notify和notifyAll之间存在细微差别,这使得它成为Java中流行的多线程面试问题之一。当你调用notify时,只有一个等待线程会被唤醒而且它不能保证哪个线程会被唤醒,这取决于线程调度器。虽然如果你调用notifyAll方法,那么等待该锁的所有线程都会被唤醒,但是在执行剩余的代码之前,所有被唤醒的线程都将争夺锁定,这就是为什么在循环上调用wait,因为如果多个线程被唤醒,那么线程是将获得锁定将首先执行,它可能会重置等待条件,这将迫使后续线程等待。因此,notify和notifyAll之间的关键区别在于notify()只会唤醒一个线程,而notifyAll方法将唤醒所有线程。
何时在Java中使用notify和notifyAll
  1. 如果所有线程都在等待相同的条件,并且一次只有一个线程可以从条件变为true,则可以使用notify over notifyAll。
  2. 在这种情况下,notify是优于notifyAll 因为唤醒所有这些因为我们知道只有一个线程会受益而所有其他线程将再次等待,所以调用notifyAll方法只是浪费CPU。
  3. 虽然这看起来很合理,但仍有一个警告,即无意中的接收者吞下了关键通知。通过使用notifyAll,我们确保所有收件人都会收到通知
总结
  1. 如果我使用notify(),将通知哪个线程?

    无法保证,ThreadScheduler将从等待该监视器上的线程的池中选择一个随机线程。保证只有一个线程会被通知:(随机性)

  1. 我怎么知道有多少线程在等待,所以我可以使用notifyAll()?

    它取决于程序逻辑,在编码时需要考虑一段代码是否可以由多个线程运行。理解线程间通信的一个很好的例子是在Java中实现生产者 - 消费者模式。

  2. 如何调用notify()?

    Wait()和notify()方法只能从synchronized方法或块中调用,需要在其他线程正在等待的对象上调用notify方法。

  3. 什么是这些线程等待被通知等?

    线程等待某些条件,例如在生产者 - 消费者问题中,如果共享队列已满,则生产者线程等待,如果共享队列为空,则生成者线程等待。由于多个线程正在使用共享资源,因此它们使用wait和notify方法相互通信。

Java中notify和notifyAll的区别 - 何时以及如何使用_notify不可以任何时候被任何线程调用_angerYang的博客-CSDN博客

3、Lock ----Condition

package com.lxg.juc.lock;

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

/**
 * @auther xiaolin
 * @creatr 2023/4/14 0:21
 */
//创建资源类,定义属性和操作方法
class Share{
    private int number=0;

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

    //+1
    public void increase(){
        //上锁
        lock.lock();
        try{
            //判断
            while (number!=0){
                condition.await();
            }
            //干活
            number++;
            System.out.println(Thread.currentThread().getName() + "::" + number);
            //通知
            condition.signalAll();

        }catch(Exception e){
            e.printStackTrace();
        }finally{
            //解锁
            lock.unlock();
        }
    }
    //-1
    public void decrease(){
        //上锁
        lock.lock();
        try{
            //判断
            while (number!=1){
                condition.await();
            }
            //干活
            number--;
            System.out.println(Thread.currentThread().getName() + "::" + number);
            //通知
            condition.signalAll();

        }catch(Exception e){
            e.printStackTrace();
        }finally{
            //解锁
            lock.unlock();
        }
    }
}

public class ThreadDemo2 {

    public static void main(String[] args) {
        Share share = new Share();
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try{
                    share.increase();
                }catch(Exception e){
                    e.printStackTrace();
                }
            }
        },"AA").start();
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try{
                    share.decrease();
                }catch(Exception e){
                    e.printStackTrace();
                }
            }
        },"BB").start();
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try{
                    share.increase();
                }catch(Exception e){
                    e.printStackTrace();
                }
            }
        },"CC").start();
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try{
                    share.decrease();
                }catch(Exception e){
                    e.printStackTrace();
                }
            }
        },"DD").start();
    }

}

四、线程间定制化通信

在上面的代码中,运行结果都是不确定顺序的

1、通过标志位方式

JUC基础_第2张图片

package com.lxg.juc.lock;

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

/**
 * @auther xiaolin
 * @creatr 2023/4/14 0:21
 */
//创建资源类
class ShareResourcce{

    //定义标志位
    private int flag=1;

    //创建lock
    private Lock lock = new ReentrantLock();

    //创建三个condition,可以通知指定condition上的线程
    private Condition c1 = lock.newCondition();
    private Condition c2 = lock.newCondition();
    private Condition c3 = lock.newCondition();

    //打印5次,参数:第几轮
    public void print5(int loop){
        //上锁
        lock.lock();
        try{
            //判断
            while (flag!=1){
                //等待
                c1.await();
            }
            //干活
            for (int i = 1; i <= 5; i++) {
                System.out.println(Thread.currentThread().getName()+"::"+i+" 轮数:"+loop);
            }
            //通知
            flag=2;
            c2.signal();//通知bb线程
        }catch(Exception e){
            e.printStackTrace();
        }finally{
            //释放锁
            lock.unlock();
        }
    }
    //打印10次,参数:第几轮
    public void print10(int loop){
        //上锁
        lock.lock();
        try{
            //判断
            while (flag!=2){
                //等待
                c2.await();
            }
            //干活
            for (int i = 1; i <= 10; i++) {
                System.out.println(Thread.currentThread().getName()+"::"+i+" 轮数:"+loop);
            }
            //通知
            flag=3;
            c3.signal();//通知唤醒c3上的一个线程,只有cc线程
        }catch(Exception e){
            e.printStackTrace();
        }finally{
            //释放锁
            lock.unlock();
        }
    }
    //打印15次,参数:第几轮
    public void print15(int loop){
        //上锁
        lock.lock();
        try{
            //判断
            while (flag!=3){
                //等待
                c3.await();
            }
            //干活
            for (int i = 1; i <= 15; i++) {
                System.out.println(Thread.currentThread().getName()+"::"+i+" 轮数:"+loop);
            }
            //通知
            flag=1;
            c1.signal();//通知bb线程
        }catch(Exception e){
            e.printStackTrace();
        }finally{
            //释放锁
            lock.unlock();
        }
    }
}

public class ThreadDemo3 {

    public static void main(String[] args) {
        ShareResourcce shareResourcce = new ShareResourcce();
        new Thread(()->{
            for (int i = 1; i <= 10; i++) {
                try{
                    shareResourcce.print5(i);
                }catch(Exception e){
                    e.printStackTrace();
                }
            }
        },"AA").start();
        new Thread(()->{
            for (int i = 1; i <= 10; i++) {
                try{
                    shareResourcce.print10(i);
                }catch(Exception e){
                    e.printStackTrace();
                }
            }
        },"BB").start();
        new Thread(()->{
            for (int i = 1; i <= 10; i++) {
                try{
                    shareResourcce.print15(i);
                }catch(Exception e){
                    e.printStackTrace();
                }
            }
        },"CC").start();
    }

}

五、集合的线程安全

1、用synchronized就一定线程安全吗?

[synchronized的四种用法 - Rooker - 博客园 (cnblogs.com)](https://www.cnblogs.com/lukelook/p/9946065.html#:~:text= Synchronized还可作用于一个类,用法如下: A.,无论synchronized关键字加在方法上还是对象上,如果它作用的对象是非静态的,则它取得的锁是对象;如果synchronized作用的对象是一个静态方法或一个类,则它取得的锁是对类,该类所有的对象同一把锁。 B. 每个对象只有一个锁(lock)与之相关联,谁拿到这个锁谁就可以运行它所控制的那段代码。)

答案是否定的,因为synchronized锁的对象有可能不一样,比如synchronized作用在代码块、方法、类、静态方法上的锁对象。

  • 在定义接口方法时不能使用synchronized关键字。
  • 构造方法不能使用synchronized关键字,但可以使用synchronized代码块来进行同步。
  • synchronized 关键字不能被继承 。如果子类覆盖了父类的 被 synchronized 关键字修饰的方法,那么子类的该方法只要没有 synchronized 关键字,那么就默认没有同步,也就是说,不能继承父类的 synchronized。

类:作用的对象是这个类的所有对象,和静态方法一致

实例方法:作用的对象是this

静态方法:作用的对象是类名.calss

代码块:作用的对象是括号中的对象

2、ArrayList线程不安全举例

package com.lxg.juc.lock;

/**
 * @auther xiaolin
 * @creatr 2023/4/14 9:19
 */

import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

/**
 * list集合线程不安全
 */
public class ThreadDemo4 {
    public static void main(String[] args) {
        //创建ArrayList集合
        List<String> list = new ArrayList<>();

        for (int i = 0; i < 100; i++) {
            new Thread(()->{
               //向集合添加内容
               list.add(UUID.randomUUID().toString().substring(0,8));
               //从集合获取内容
                System.out.println(list);
            },String.valueOf(i)).start();
        }
    }
}

JUC基础_第3张图片

ConcurrentModificationException是Java中的一种异常,表示在使用迭代器(Iterator)遍历集合(Collection)时,出现了并发修改的情况。例如,在使用迭代器遍历集合时,另一个线程修改了集合的内容,就会抛出ConcurrentModificationException异常。这个异常通常发生在多线程环境下,因为多个线程可能同时访问同一个集合,而对集合的修改可能会相互影响,导致遍历时出现异常。为了避免这个异常,可以使用线程安全的集合类,或者在遍历时使用synchronized关键字进行同步。

2.1、解决方案-Vector
List<String> list = new Vector<>();

从Java2平台1.2开始,该类改进了List接口,使其成为Java Collections Framework的成员,与新的集合实现不同,Vector被同步。如果不需要线程安全的实现,建议使用ArrayList代替Vector

Vector是线程安全的,但是性能比较低,因为每个线程在访问集合时都需要先获取锁。

2.2、解决方案-Collections类的方法

synchronizedList(List list) 方法用于将一个非线程安全的 List 转换为线程安全的 List,以便在多线程环境下使用。

以下是使用 synchronizedList(List list) 方法的示例:

List<String> list = new ArrayList<>();
List<String> synchronizedList = Collections.synchronizedList(list);

在这个示例中,我们创建了一个 ArrayList 类型的 list,然后使用 Collections.synchronizedList(list) 方法将其转换为线程安全的 synchronizedList。现在,synchronizedList 可以在多线程环境下使用,而不用担心线程安全问题。

这种方式的缺点是,虽然能够解决线程安全问题,但是每个线程在访问集合时都需要先获取锁,这会导致性能下降。

2.3、解决方案-CopyOnWriteArrayList
List<String> list = new CopyOnWriteArrayList<>();

写时复制技术

JUC基础_第4张图片

写时复制技术(Copy-On-Write,简称COW)是一种计算机编程技术,通常用于优化内存管理和提高程序性能。写时复制技术是一种延迟复制技术,它允许多个进程或线程共享相同的资源,例如内存或文件,而不需要立即创建副本。

在写时复制技术中,当多个进程或线程需要访问相同的资源时,它们会共享同一个指向该资源的指针。当任何一个进程或线程需要修改这个资源时,它会复制这个资源的副本,并将修改写入副本中,而不是原始资源。这样,其他进程或线程仍然可以访问原始资源,而不受修改的影响。

写时复制技术通常用于优化内存管理,例如在创建进程时,操作系统可以使用写时复制技术来避免复制整个进程空间。当进程需要修改内存时,操作系统会创建一个新的副本,并在副本上进行修改,而不会影响原始进程空间。

写时复制技术还可以用于提高程序性能,例如在多线程编程中,多个线程可以共享相同的数据结构,而不需要每个线程都创建自己的副本。当一个线程需要修改数据结构时,它会创建一个新的副本,并在副本上进行修改,而其他线程仍然可以访问原始数据结构,而不受修改的影响。

CopyOnWriteArrayList是Java中的一种并发容器,它的实现原理是“写时复制”,即在进行修改操作时,先将原有的数据进行复制,然后在副本上进行修改。这样,原有的数据不会被修改,从而保证了数据的一致性和可靠性。

具体来说,CopyOnWriteArrayList内部维护了一个volatile类型的数组,这个数组是线程安全的。当进行写操作时,CopyOnWriteArrayList会先复制一份当前数组,然后在副本上进行修改操作,最后将修改后的副本替换原来的数组。在这个过程中,其他线程仍然可以并发地读取原来的数组,因为原来的数组没有被修改。这就保证了读操作的线程安全性。

需要注意的是,CopyOnWriteArrayList适用于读多写少的场景,因为每次写操作都要复制一份数组,所以写操作的开销比较大。另外,由于每次写操作都会生成一个新的数组,所以CopyOnWriteArrayList的内存占用会比较高。因此,如果读操作和写操作的比例相差不大,或者写操作比较频繁,就不适合使用CopyOnWriteArrayList。

3、HashSet线程不安全举例

//HashSet
Set<String> set = new HashSet<>();

for (int i = 0; i < 100; i++) {
    new Thread(()->{
        //向集合添加内容
        set.add(UUID.randomUUID().toString().substring(0,8));
        //从集合获取内容
        System.out.println(set);
    },String.valueOf(i)).start();
}
3.1、解决方案-CopyOnWriteArraySet
Set<String> set = new CopyOnWriteArraySet<>();

CopyOnWriteArraySet是线程安全的,而且支持并发读取,适用于读多写少的场景。但是每次写操作都需要复制一份集合,因此写操作的开销比较大。

3.2、解决方案-Collections.synchronizedSet
Set<String> set = Collections.synchronizedSet(new HashSet<>());

这种方式的缺点是,虽然能够解决线程安全问题,但是每个线程在访问集合时都需要先获取锁,这会导致性能下降。

3.3、ConcurrentHashMap.newKeySet
Set<String> set = ConcurrentHashMap.newKeySet();

ConcurrentHashMap是线程安全的,而且性能比较高,因此使用ConcurrentHashMap代替HashSet可以解决线程安全问题,而且不会影响性能。

4、HashMap线程不安全举例

Map<String,String> map = new HashMap<>();

for (int i = 0; i < 100; i++) {
    String key = String.valueOf(i);
    new Thread(()->{
        //向集合添加内容
        map.put(key,UUID.randomUUID().toString().substring(0,8));
        //从集合获取内容
        System.out.println(map);
    },String.valueOf(i)).start();
}
4.1、解决方案-ConcurrentHashMap
Map<String,String> map = new ConcurrentHashMap<>();

ConcurrentHashMap是线程安全的,而且性能比较高,因此使用ConcurrentHashMap代替HashMap可以解决线程安全问题,而且不会影响性能。

4.2、解决方案-Hashtable

还有一种解决方案是使用HashTable。HashTable是Java中的一种线程安全的集合,它的实现方式是使用synchronized关键字来保证线程安全。HashTable的使用方式和HashMap类似,但是由于需要加锁,因此性能比较低,而且在Java 1.8之后,官方推荐使用ConcurrentHashMap代替HashTable。因此,如果需要线程安全的集合,可以使用ConcurrentHashMap来代替HashTable。

  • 该类实现了一个哈希表,它将键映射到值,任何非null对象都可以用作键或者值
  • 从Java2平台1.2开始,该类进行了改进,实现了Map接口,使其成为Java Collections Framework的成员。与新的集合实现不同,Hashtable被同步。如果不需要线程安全的实现,建议使用HashMap代替Hashtable
Hashtable<String,String > ht=new Hashtable();
HashMap<String ,String> hm=new HashMap<>();
4.3、解决方案-Collections.synchronizedMap
Map<String, String> map = Collections.synchronizedMap(new HashMap<>());

这种方式的缺点是,虽然能够解决线程安全问题,但是每个线程在访问集合时都需要先获取锁,这会导致性能下降。

5、小Tip

以上集合的解决方案还有一种就是使用ThreadLocal

例如:

private static ThreadLocal<Map<String, Object>> threadLocal = new ThreadLocal<>();
Map<String, Object> map = threadLocal.get();
map.put("key", "value");
Object value = map.get("key");

六、多线程锁

1、锁的八种情况

package com.lxg.juc.lock;

import java.util.concurrent.TimeUnit;

/**
 * @auther xiaolin
 * @creatr 2023/4/16 11:03
 */

class Phone{
    public  synchronized void sendSMS() throws Exception{
        //停留4秒
        TimeUnit.SECONDS.sleep(4);
        System.out.println("------sendSMS");
    }

    /*public static   synchronized void sendSMS() throws Exception{
        //停留4秒
        TimeUnit.SECONDS.sleep(4);
        System.out.println("------sendSMS");
    }*/

    public synchronized void sendEmail() throws Exception{
        System.out.println("------sendEmail");
    }

   /* public static synchronized void sendEmail() throws Exception{
        System.out.println("------sendEmail");
    }*/

    public void sayHello() throws Exception{
        System.out.println("------sayHello");
    }


}

/**
 * 8锁
 * 1 标准访问,请问先打印邮件还是短信
 * ------sendSMS
 * ------sendEmail
 * 锁是phone实例对象
 * 谁先拿到锁,谁先执行
 *
 * 2 停4秒在邮件方法,请问先打印邮件还是短信
 * ------sendSMS
 * ------sendEmail
 * 锁是phone实例对象
 * 有锁,先执行谁,谁先拿到锁,没拿到锁的就等待
 *
 * 3 新增普通sayHello方法,请问先打印邮件还是hello
 * ------sayHello
 * ------sendSMS
 * 锁是phone实例对象
 * 但是sayHello没有锁,所以不受锁的影响
 *
 *
 * 4 停4秒在邮件方法,两部手机,请问先打印邮件还是短信
 * ------sendEmail
 * ------sendSMS
 * 这里有两个锁,两个对象,所以不受影响
 *
 * 5 两个静态同步方法,同一部手机,请问先打印邮件还是短信
 * ------sendSMS
 * ------sendEmail
 * 锁是class对象
 * 两个方法都是静态的,所以锁的是class对象
 * 谁先拿到锁,谁先执行
 *
 * 6 两个静态同步方法,2部手机,请问先打印邮件还是短信
 * ------sendSMS
 * ------sendEmail
 * 锁是class对象
 * 两个方法都是静态的,所以锁的是class对象
 * 谁先拿到锁,谁先执行
 *
 * 7 1个静态同步方法,1个普通同步方法,1部手机,请问先打印邮件还是短信
 * ------sendEmail
 * ------sendSMS
 * 两把锁,一个是class对象,一个是phone对象
 * 互不影响
 *
 * 8 1个静态同步方法,1个普通同步方法,2部手机,请问先打印邮件还是短信
 * ------sendEmail
 * ------sendSMS
 * 两把锁,一个是class对象,一个是phone对象
 * 互不影响
 */
public class Lock_8 {
    public static void main(String[] args) throws InterruptedException {
        Phone phone = new Phone();
        Phone phone2 = new Phone();

        new Thread(()->{
            try {
                phone.sendSMS();
            } catch (Exception e) {
                e.printStackTrace();
            }
        },"AA").start();

       Thread.sleep(100);

        new Thread(()->{
            try {
//                phone.sendEmail();
//                phone.sayHello();
                phone2.sendEmail();
            } catch (Exception e) {
                e.printStackTrace();
            }
        },"BB").start();
    }
}

synchronized实现同步的基础:Java中的每一个对象都可以作为锁。
具体表现为以下3种形式。
对于普通同步方法,锁是当前实例对象。
对于静态同步方法,锁是当前类的c1ass对象。
对于同步方法块,锁是Synchonized括号里配置的对象

2、公平锁和非公平锁

private final ReentrantLock lock = new ReentrantLock(true);

除了在 ReentrantLock 类的构造函数中指定锁的类型之外,还可以使用 ReentrantLock 类的 fairunfair 方法来创建公平锁和非公平锁。

public static ReentrantLock fairLock() // 创建一个公平锁
public static ReentrantLock unfairLock() // 创建一个非公平锁

这两个方法分别返回一个公平锁和一个非公平锁的实例。例如,要创建一个公平锁,可以使用以下代码:

ReentrantLock fairLock = ReentrantLock.fairLock();

要创建一个非公平锁,可以使用以下代码:

ReentrantLock nonFairLock = ReentrantLock.unfairLock();

这种方式创建公平锁和非公平锁的效果与在 ReentrantLock 类的构造函数中指定锁的类型是一样的。需要注意的是,这两个方法只是提供了一种更加简便的方式来创建公平锁和非公平锁,本质上还是调用了 ReentrantLock 类的构造函数来创建锁的实例。

为false非公平锁,true公平锁

非公平锁和公平锁是指在多线程环境下,对资源的访问方式不同。具体区别如下:

  1. 非公平锁:当多个线程同时请求同一个资源时,非公平锁不保证先到达的线程先获取到锁,而是直接尝试获取锁,如果获取成功就可以访问资源,如果获取失败就进入等待队列。如果获取失败的线程在等待队列中等待一段时间后,重新尝试获取锁,仍然有可能会被后到达的线程抢占资源。

  2. 公平锁:当多个线程同时请求同一个资源时,公平锁会按照请求的先后顺序依次获取锁,保证先到达的线程先获取到锁。如果某个线程请求锁时发现锁已经被其他线程获取了,那么该线程会进入等待队列,等待其他线程释放锁后再进行获取。

总的来说,公平锁相对于非公平锁更加公平,但是由于需要维护等待队列,所以性能可能会受到一定的影响。而非公平锁则可以更快地获取锁,但是可能会导致某些线程长时间无法获取到锁,从而导致饥饿现象。在实际应用中,需要根据具体情况选择使用哪种类型的锁。

3、可重入锁

synchronized(隐式)和Lock(显式)都是可重入锁

前往

要注意:同一线程才可以重复获得锁,不同线程不行

同一线程可以重复获得锁,这种情况下称为重入锁。但是不同线程需要等待其他线程解锁后才能获得锁。这是因为在多线程并发的情况下,如果不同线程都可以同时获得锁,就会导致数据的不一致性和竞态条件等问题。因此,锁的作用就是保证在同一时间内只有一个线程可以访问共享资源。

4、死锁

4.1、什么是死锁

死锁是指两个或多个线程在执行过程中,因互相等待对方释放资源而造成的一种阻塞状态,使得程序无法继续执行下去。

JUC基础_第5张图片

4.2、产生死锁的原因
  1. 系统资源不足
  2. 进行运行推进顺序不合适
  3. 资源分配不当

死锁的产生通常是由于多个线程互相竞争资源造成的。常见的死锁产生原因有以下几种:

  1. 竞争资源:多个线程同时竞争有限的资源,例如内存、锁等。

  2. 循环等待:多个线程互相持有对方需要的锁或资源,形成循环等待的局面。

  3. 不可抢占:某些资源无法被其他线程抢占,只能等待当前线程释放。

  4. 占用且等待:一个线程已经持有了一个资源,但又想申请其他资源,而其他资源又被其他线程占用。

  5. 无法释放:线程持有资源后无法释放,例如线程被阻塞、崩溃等。

以上原因通常都是由于多个线程同时竞争资源导致的,因此在编写多线程程序时需要注意资源的竞争情况,尽可能避免多个线程同时持有多个资源。同时,也需要注意线程的调度顺序,避免出现循环等待的情况。

4.3、死锁案例

下面是一个死锁的代码举例:

public class DeadLockDemo {
    private static Object lock1 = new Object();
    private static Object lock2 = new Object();

    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (lock1) {
                System.out.println(Thread.currentThread().getName() + "获取了锁1");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lock2) {
                    System.out.println(Thread.currentThread().getName() + "获取了锁2");
                }
            }
        }, "线程1").start();

        new Thread(() -> {
            synchronized (lock2) {
                System.out.println(Thread.currentThread().getName() + "获取了锁2");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lock1) {
                    System.out.println(Thread.currentThread().getName() + "获取了锁1");
                }
            }
        }, "线程2").start();
    }
}

以上代码中,两个线程分别通过 synchronized 关键字获取 lock1 和 lock2 两个锁,但是线程1 获取了锁1 后等待锁2 的释放,而线程2 获取了锁2 后等待锁1 的释放,导致两个线程互相等待,进入了死锁状态。

为了避免死锁,可以采取以下措施:

  1. 避免循环等待:尽量避免同时申请多个锁。

  2. 避免持有锁的时间过长:在使用完锁后及时释放,减少持有锁的时间。

  3. 使用定时锁:在获取锁时设置一个超时时间,在等待超时后释放锁。

  4. 破坏占用且等待条件:一次性获取所有需要的锁,避免在持有一个锁的情况下等待其他锁的释放。

  5. 破坏不可抢占条件:允许锁的抢占,当其他线程需要锁时可以将锁抢占过去。

  6. 破坏循环等待条件:按照一定的顺序申请锁,避免循环等待的情况。

使用 Lock 的死锁示例如下:

public class LockDeadLockDemo {
    private static Lock lock1 = new ReentrantLock();
    private static Lock lock2 = new ReentrantLock();

    public static void main(String[] args) {
        new Thread(() -> {
            lock1.lock();
            System.out.println(Thread.currentThread().getName() + "获取了锁1");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            lock2.lock();
            System.out.println(Thread.currentThread().getName() + "获取了锁2");
            lock2.unlock();
            lock1.unlock();
        }, "线程1").start();

        new Thread(() -> {
            lock2.lock();
            System.out.println(Thread.currentThread().getName() + "获取了锁2");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            lock1.lock();
            System.out.println(Thread.currentThread().getName() + "获取了锁1");
            lock1.unlock();
            lock2.unlock();
        }, "线程2").start();
    }
}

以上代码中,两个线程分别通过 Lock 接口的 lock() 方法获取 lock1 和 lock2 两个锁,但是线程1 获取了锁1 后等待锁2 的释放,而线程2 获取了锁2 后等待锁1 的释放,导致两个线程互相等待,进入了死锁状态。

解决 Lock 的死锁问题,可以采取和解决 synchronized 的死锁问题相同的措施,例如避免循环等待、减少持有锁的时间、使用定时锁等。同时,Lock 接口提供了 tryLock() 方法,可以在获取锁时设置超时时间,如果在指定时间内未能获取锁,则放弃锁的申请,避免长时间等待导致的死锁。

4.4、验证是否是死锁
  1. jps

    jps命令类似于Linux系统中的ps命令,它们都是用于查看系统中正在运行的进程的命令。不过,它们的输出格式略有不同,jps命令输出的是Java进程的进程ID和类名,而ps命令输出的是进程的进程ID、进程状态、进程所属用户、进程占用的CPU和内存等信息。

  2. jstack

    jstack是Java运行时环境自带的一种命令行工具,用于生成Java进程的线程快照,可以用来定位Java进程的线程问题,如死锁、线程阻塞等。

    使用jstack命令可以生成Java进程的线程快照,如下所示:

    $ jstack 
    

    其中,pid是Java进程的进程ID,执行该命令后,jstack会输出Java进程中所有线程的堆栈信息,可以通过分析堆栈信息来定位线程问题。

    jstack命令可以用来查看Java进程中哪些线程正在运行、哪些线程被阻塞、哪些线程处于等待状态等,对于线程相关的问题诊断非常有用。

4.5、解决死锁的策略

以下是几种解决死锁问题的常见策略:

  1. 预防死锁:通过设计良好的算法和数据结构,避免出现死锁的情况。比如,可以按照同一顺序请求资源,避免出现循环等待的情况。

  2. 避免死锁:通过动态地分配资源,避免出现死锁的情况。比如,可以使用银行家算法,动态地检查资源分配是否会导致死锁,并根据检查结果来决定是否分配资源。

  3. 检测死锁:通过周期性地检查系统中是否存在死锁,及时采取措施避免死锁的发生。比如,可以使用死锁检测算法,检查系统中是否存在死锁,如果存在死锁,则采取措施避免死锁的发生。

  4. 解除死锁:通过打破死锁中的某些条件,解除死锁的情况。比如,可以采用抢占式资源分配策略,当某个线程长时间占用资源时,可以强制中断该线程,以解除死锁的情况。

总之,解决死锁问题需要综合考虑系统的特点和需求,选择合适的策略来避免或解决死锁的情况。

七、Callable接口

1、创建线程的多种方式

前往

  • 继承Thread类
  • 实现Runnable接口
  • Callable接口
  • 线程池方式
1.1、Callable介绍

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

现在我们学习的是创建线程的第三种方案–Callable接口

Callable接口的特点如下(重点)

  • 为了实现Runnable,需要实现不返回任何内容的run()方法,而对于Callable,需要实现在完成时返回结果的cal()方法。:
  • call()方法可以引发异常,而run()则不能。
  • 为实现Callable而必须重写cal方法

call()方法可以引发异常,而run()则不能。这个区别的作用在于Callable接口可以更好地处理任务执行过程中的错误和异常。

因为Runnable接口的run方法不能抛出checked exception,所以当任务执行过程中出现了checked exception时,我们只能在任务内部使用try-catch块来处理异常,而不能将异常抛出到任务的外部。这使得我们很难对任务执行过程中的错误和异常进行统一的处理,例如记录日志、发送邮件、终止任务的执行等操作。

而Callable接口的call方法可以抛出checked exception,这使得我们可以在任务执行过程中抛出checked exception,并将异常抛出到任务的外部。这样,我们就可以在任务外部使用try-catch块来处理异常,并采取相应的措施来恢复任务的执行或者终止任务的执行。这使得我们可以更好地处理任务执行过程中的错误和异常,提高任务的健壮性和可靠性。

需要注意的是,Callable接口抛出checked exception的能力并不是没有代价的。因为任务抛出了checked exception,我们需要在调用Future对象的get方法时使用try-catch块来处理异常,或者在方法签名中使用throws关键字来声明抛出异常。这使得我们在使用Callable接口时需要更加小心谨慎,以避免在任务执行过程中出现不可预料的错误和异常。


1.2、Runnable和Callable

Callable接口和Runnable接口都可以用于创建线程池中的任务,但是它们之间有一些区别。

  1. 返回值类型:Runnable接口的run方法没有返回值,而Callable接口的call方法可以返回一个结果。

  2. 抛出异常:Runnable接口的run方法不能抛出checked exception,只能抛出unchecked exception,而Callable接口的call方法可以抛出checked exception。

  3. 使用方式:Runnable接口通常用于执行一些没有返回值的任务,而Callable接口通常用于执行一些需要返回结果的任务。

下面是一个使用Runnable接口的示例:

package com.lxg.juc.callable;

import java.util.concurrent.Callable;

/**
 * @auther xiaolin
 * @creatr 2023/4/16 15:56
 */

//实现Runnable接口的方式
class MyThread implements Runnable{

    @Override
    public void run() {
        System.out.println("runnable");
    }
}


public class Demo1 {

    public static void main(String[] args) {

        //实现Runnable接口的方式创建线程
        new Thread(new MyThread(),"AA").start();
    }
}

怎么使用Callable?

JUC基础_第6张图片

FutureTask是Java中的一个类,它提供了一种异步执行任务的方式,可以在任务执行完毕后获取任务的执行结果。FutureTask的原理是通过一个内部的状态来记录任务的执行状态,任务可以处于三种状态:未开始、正在执行、已完成。当任务执行完成后,可以通过get()方法获取任务的执行结果。

FutureTask的好处在于它可以提高程序的执行效率和响应速度。通过异步执行任务,可以让程序在等待某个操作完成的同时,继续执行其他操作,从而提高了程序的并发性。此外,FutureTask还提供了一些方法来控制任务的执行,例如cancel()方法可以取消任务的执行,isCancelled()方法可以判断任务是否已被取消。

总之,FutureTask是一个非常实用的工具类,它可以帮助开发者实现异步任务的执行,提高程序的并发性和响应速度。

FutureTask只能汇总一次指的是FutureTask的get()方法只能获取一次任务的执行结果。当调用FutureTask的get()方法获取任务的执行结果时,如果任务已经执行完成,则get()方法会立即返回结果;如果任务还未执行完成,则get()方法会阻塞等待任务执行完成并返回结果。

一旦调用了get()方法获取任务的执行结果,FutureTask就会将结果保存起来,并将任务的状态设置为已完成。此后再调用get()方法获取任务的执行结果时,将不会重新执行任务,而是直接返回之前保存的结果。

因此,如果需要多次获取任务的执行结果,可以通过将任务的执行结果保存到一个变量中来实现,而不是反复调用FutureTask的get()方法。
package com.lxg.juc.callable;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

/**
 * @auther xiaolin
 * @creatr 2023/4/16 15:56
 */

//比较两个接口的区别
//实现Runnable接口的方式
class MyThread implements Runnable{

    @Override
    public void run() {
        System.out.println("runnable");
    }
}
//实现Callable接口的方式
class MyThread2 implements Callable {

    @Override
    public Object call() throws Exception {
        System.out.println(Thread.currentThread().getName()+" come in callable1");
        return "执行callable1完毕";
    }
}

public class Demo1 {

    public static void main(String[] args) throws ExecutionException, InterruptedException {

        //实现Runnable接口的方式创建线程
//        new Thread(new MyThread(),"AA").start();

        //实现Callable接口的方式创建线程
        //报错,Thread类的构造方法中没有Callable接口的参数
//        new Thread(new MyThread2(),"BB").start();
        FutureTask<Integer> futureTask1 = new FutureTask(new MyThread2());

        //lambda表达式的方式
        FutureTask<Integer> futureTask2 = new FutureTask(()->{
            System.out.println(Thread.currentThread().getName()+" come in callable2");
            return "执行callable2完毕";
        });

        new Thread(futureTask2,"f2").start();
        new Thread(futureTask1,"f1").start();


       /* while (!futureTask2.isDone()){
            System.out.println("等待结果");
        }*/


        System.out.println(futureTask2.get());
        System.out.println(futureTask2.get());

        System.out.println(futureTask1.get());

        System.out.println(Thread.currentThread().getName()+" come over");

        //FutureTask原理 未来任务
        /**
         * FutureTask提供了一种异步执行任务的方式,可以在任务执行完毕后获取任务的执行结果。F
         *
         */


    }
}

下面是一个使用Callable接口的示例:

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class CallableExample {
    public static void main(String[] args) throws Exception {
        // 创建一个线程池,包含1个核心线程
        ExecutorService executor = Executors.newFixedThreadPool(1);

        // 提交一个Callable任务
        Future<String> future = executor.submit(new Task("Task1"));

        // 获取任务执行结果
        String result = future.get();
        System.out.println("Result: " + result);

        // 关闭线程池
        executor.shutdown();
    }

    static class Task implements Callable<String> {
        private String name;

        public Task(String name) {
            this.name = name;
        }

        @Override
        public String call() throws Exception {
            System.out.println("Task " + name + " is running on thread " + Thread.currentThread().getName());
            Thread.sleep(5000);
            return "Task " + name + " is completed";
        }
    }
}

在上面的示例中,我们使用Callable接口来创建任务,并通过Future对象来获取任务的执行结果。需要注意的是,我们需要使用ExecutorService的submit方法来提交Callable任务,而不是直接调用它的call方法。

八、JUC强大的辅助类

1、减少计数CountDownLatch

CountDownLatch是Java的一个并发工具类,它可以用于协调多个线程之间的执行顺序。它的作用是让某个线程等待直到倒计时结束,再开始执行。具体来说,CountDownLatch通过一个计数器来实现,计数器初始值可以设定,当某个线程调用CountDownLatch的await()方法时,它会被阻塞,直到计数器的值为0,才会继续执行。而其他线程在完成自己的任务后,可以通过调用CountDownLatch的countDown()方法来减少计数器的值,当计数器的值为0时,所有等待的线程都会被唤醒。这样,就可以实现多个线程的协作,让它们按照一定的顺序执行。

CountDownLatch类可以设置一个计数器,然后通过countDown方法来进行
减1的操作,使用await方法等待计数器不大于0,然后继续执行await方法
之后的语句。

CountDownLatch主要有两个方法,当一个或多个线程调用await方法时,这
些线程会阻塞。

其它线程调用countDown方法会将计数器减1(调用countDown方法的线程
不会阻塞)

当计数器的值变为0时,因awit方法阻塞的线程会被唤醒,继续执行

package com.lxg.juc;

import java.util.concurrent.CountDownLatch;

/**
 * @auther xiaolin
 * @creatr 2023/4/16 17:56
 */

//CountDownLatch演示
public class CountDownLatchDemo {
    
    //6个同学陆续离开教室后值班同学才可以关门
    public static void main(String[] args) {
        
        //创建CountDownLatch对象,参数为6,表示有6个同学离开教室
        CountDownLatch countDownLatch = new CountDownLatch(6);

        for (int i = 1; i <= 6; i++) {
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + "\t 离开教室");
                countDownLatch.countDown();
            }, String.valueOf(i)).start();
        }
        
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(Thread.currentThread().getName() + "\t 班长最后关门走人");


    }
}

2、循环栅栏CyclicBarrier

一个多线程同步工具类,它可以让多个线程在一个屏障处等待,直到所有线程都到达这个屏障处,才能继续执行下面的任务。CyclicBarrier类的构造函数可以指定等待线程的数量,当所有线程都到达屏障处时,CyclicBarrier会自动将所有线程唤醒,然后执行指定的任务。CyclicBarrier类可以用于一些需要多个线程协同完成的任务,例如多个线程同时读取一个大文件,每个线程读取一部分,等所有线程都读取完毕后再进行数据的合并处理。

假设有一个场景:有5个工人需要完成一项任务,任务分为5个步骤,每个工人负责其中一个步骤。只有当所有工人都完成自己的步骤后,才能进行下一个步骤。这时候就可以使用CyclicBarrier类来实现。

代码实现如下:

import java.util.concurrent.CyclicBarrier;

public class WorkerTask implements Runnable {
    private int workerId;
    private CyclicBarrier cyclicBarrier;

    public WorkerTask(int workerId, CyclicBarrier cyclicBarrier) {
        this.workerId = workerId;
        this.cyclicBarrier = cyclicBarrier;
    }

    @Override
    public void run() {
        System.out.println("工人" + workerId + "开始工作...");
        try {
            // 模拟工作时间
            Thread.sleep((long) (Math.random() * 5000));
            System.out.println("工人" + workerId + "完成工作,等待其他工人...");
            cyclicBarrier.await();
            System.out.println("工人" + workerId + "开始下一步工作...");
            // 模拟工作时间
            Thread.sleep((long) (Math.random() * 5000));
            System.out.println("工人" + workerId + "完成工作,等待其他工人...");
            cyclicBarrier.await();
            // 依次类推,直到完成所有步骤
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

public class CyclicBarrierExample {
    public static void main(String[] args) {
        int workerNum = 5;
        CyclicBarrier cyclicBarrier = new CyclicBarrier(workerNum, new Runnable() {
            @Override
            public void run() {
                System.out.println("所有工人完成上一步工作,开始下一步工作...");
            }
        });

        for (int i = 0; i < workerNum; i++) {
            new Thread(new WorkerTask(i, cyclicBarrier)).start();
        }
    }
}

以上代码中,CyclicBarrier实例的构造函数中指定了等待的线程数量为5,每个线程代表一个工人,当所有工人都完成自己的工作后,CyclicBarrier会自动唤醒所有线程,然后执行指定的任务,即输出"所有工人完成上一步工作,开始下一步工作…"。每个工人的工作通过模拟线程睡眠来实现。

package com.lxg.juc;

import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

/**
 * @auther xiaolin
 * @creatr 2023/4/16 22:29
 */

//集齐7颗龙珠召唤神龙
public class CyclicBarrierDemo {

    //创建固定值
    private static final int NUMBER = 7;

    public static void main(String[] args) {
        //创建CyclicBarrier
        CyclicBarrier cyclicBarrier = new CyclicBarrier(NUMBER, () -> {
            System.out.println("集齐7颗龙珠,召唤神龙");
        });

        for (int i = 1; i <= NUMBER; i++) {
            final int tempInt = i;
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + "\t收集到第" + tempInt + "颗龙珠,等待其他龙珠收集完毕");
                try {
                    cyclicBarrier.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }, String.valueOf(i)).start();
        }

    }
}

3、信号灯Semaphore

6辆汽车停3个车位

package com.lxg.juc;

import java.util.Random;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;

/**
 * @auther xiaolin
 * @creatr 2023/4/17 9:04
 */

//六辆汽车停三个车位
public class SemaphoreDemo {

    public static void main(String[] args) {
        //创建Semaphore对象,设置许可证数量
        Semaphore semaphore = new Semaphore(3);

        //模拟6辆汽车
        for (int i = 1; i <= 6; i++) {
            new Thread(() -> {
                //抢占车位
                try {
                    semaphore.acquire();
                    System.out.println(Thread.currentThread().getName() + " 抢占到车位");

                    int time = new Random().nextInt(10);
                    //设置随机停车时间
                    TimeUnit.SECONDS.sleep(time);

                    System.out.println(Thread.currentThread().getName() + " 停了"+time+"秒后离开车位");
                } catch (Exception e) {
                    e.printStackTrace();
                }finally {
                    //释放车位
                    semaphore.release();
                }
            }, String.valueOf(i)+"号车").start();
        }
    }
}

信号量为1时和lock一致了

4、如何让几个线程按顺序执行

4.1、join

使用join方法:在一个线程中调用另一个线程的join方法,可以让当前线程等待另一个线程执行完毕后再继续执行。可以通过多次调用join方法来实现多个线程的顺序执行。

Thread t1 = new Thread(() -> {
    System.out.println("Thread 1");
});
Thread t2 = new Thread(() -> {
    try {
        t1.join();
        System.out.println("Thread 2");
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
});
Thread t3 = new Thread(() -> {
    try {
        t2.join();
        System.out.println("Thread 3");
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
});

t1.start();
t2.start();
t3.start();
4.2、CountDownLatch

使用CountDownLatch类:CountDownLatch是一个同步工具类,可以让一个或多个线程等待其他线程完成操作后再继续执行。可以通过多个CountDownLatch实例来实现多个线程的顺序执行。

CountDownLatch latch1 = new CountDownLatch(1);
CountDownLatch latch2 = new CountDownLatch(1);

Thread t1 = new Thread(() -> {
    System.out.println("Thread 1");
    latch1.countDown();
});
Thread t2 = new Thread(() -> {
    try {
        latch1.await();
        System.out.println("Thread 2");
        latch2.countDown();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
});
Thread t3 = new Thread(() -> {
    try {
        latch2.await();
        System.out.println("Thread 3");
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
});

t1.start();
t2.start();
t3.start();
4.3、condition

前往

condition.await()方法会释放当前线程持有的锁,使得其他线程可以获得锁并执行。这是因为condition.await()方法内部会调用Lock对象的unlock()方法释放锁,然后将当前线程挂起等待。当条件满足时,其他线程会调用condition.signal()condition.signalAll()方法唤醒挂起的线程,唤醒的线程会重新尝试获取锁,如果获取成功则继续执行,否则继续等待。因此,只有当唤醒的线程获取到锁后,其他线程才可能抢到锁。

5、Lock和Condition的区别

Lock和Condition都是Java中用于实现线程同步的工具,它们的主要区别如下:

  1. Lock是一个接口,Condition是一个类。在使用Lock时,需要使用Lock的实现类(如ReentrantLock)来实例化一个Lock对象;在使用Condition时,需要使用Lock对象的newCondition方法来创建一个Condition对象。

  2. Lock比synchronized关键字更加灵活。Lock可以精确控制锁的获取和释放,可以在不同的代码块中获取和释放锁,而synchronized只能在同一个代码块中获取和释放锁。

  3. Lock可以实现公平锁和非公平锁。在使用Lock时,可以使用ReentrantLock的构造方法来指定锁的公平性;在synchronized中,锁是非公平的。

  4. Condition可以让线程以更加灵活的方式等待某个特定的条件。在使用Condition时,可以让线程只在某个特定条件满足时才被唤醒,而不是简单地等待一个锁。

  5. Lock和Condition需要手动释放锁。在使用Lock和Condition时,需要手动调用Lock对象的unlock方法来释放锁;在synchronized中,锁会在代码块执行完毕后自动释放。

总之,Lock和Condition相比synchronized具有更高的灵活性和可控性,但同时也需要更多的代码来实现线程同步。因此,在实现线程同步时,应根据具体的需求和场景来选择合适的工具。

九、ReentrantReadWriteLock读写锁

1、悲观锁和乐观锁

JUC基础_第7张图片

JUC基础_第8张图片

2、表锁、行锁、和页面锁

表锁和行锁都是数据库中的锁机制,用于控制并发访问时的数据一致性。

表锁是指在对某个表进行操作时,锁住整张表,其他用户无法对该表进行任何操作,直到当前用户释放锁。表锁一般适用于对整张表进行大批量操作时使用,例如表的重建、备份、优化等。

行锁是指在对某个表的某一行进行操作时,只锁住该行,其他用户可以对该表的其他行进行操作。行锁一般适用于对表进行单行操作时使用,例如单行数据的查询、修改、删除等。

在实际应用中,表锁和行锁应该根据具体情况来选择,以达到最佳的性能和一致性。

页面锁是数据库中的一种锁机制,它是介于行锁和表锁之间的一种锁粒度。

页面锁是指锁定数据库中的一个数据页面(通常是8KB),也就是将该页面中的所有行都锁定,其他用户无法修改该页面中的任何行,直到当前用户释放锁。页面锁的主要应用场景是对一组相邻的行进行操作,例如批量插入或删除数据。

与行锁相比,页面锁的锁粒度更大,可以锁定多行数据,从而提高并发度;与表锁相比,页面锁的锁粒度更小,可以避免锁住整个表的情况,从而提高并发度。

  1. 表级锁:开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低。
  2. 行级锁:开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。
  3. 页面锁:开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般

Mysql锁(表级锁,页级锁,行级锁)_mysql页锁_栗子~~的博客-CSDN博客

3、读锁(共享锁)和写锁(独占锁)

3.1、读写锁概述

读写互斥,读读共享

读锁和写锁是用于控制对共享资源的访问的锁。

读锁允许多个线程同时读取共享资源,但不允许写入操作。因此,读锁是共享的,可以同时由多个线程持有。

写锁只允许一个线程写入共享资源,并且在持有写锁时,其他线程无法读取或写入共享资源。因此,写锁是独占的,只能由一个线程持有。

读写锁是一种特殊的锁,允许多个线程同时持有读锁,但只允许一个线程持有写锁。这种锁通常用于高并发读写场景,可以提高程序的并发性能。

读读不会死锁

读写会发生死锁

假如一个线程A的函数需要读锁1,其内部运行的某个函数也需要读锁2,在线程A得到读锁1后另一个线程B需要写锁,线程B写锁上锁以后会阻塞等待线程A释放读锁1,线程A继续向下运行,等到第二次拿读锁2的时候,为了避免不断读锁上锁造成对写锁的饥饿,读锁2会阻塞等待线程B写锁释放,因此造成了死锁。

写写也会发生死锁

JUC基础_第9张图片

3.2、读写锁案例和volatile

Java 提供了一种稍弱的同步机制,即 volatile 变量,用来确保将变量的更新操作通知到其他线程。可以将volatile 看做一个轻量级的锁,但是又与锁有些不同:

➢ 对于多线程,不是一种互斥关系

➢ 不能保证变量状态的“原子性操作”

volatile是Java中的关键字,用于标记变量,表示该变量可能会被多个线程同时访问并修改。在多线程环境下,使用volatile可以确保变量的可见性和有序性,即当一个线程修改了该变量的值后,其他线程能够立即看到修改后的值,而不是使用缓存中的旧值。

volatile的主要作用是防止指令重排,即保证程序执行的顺序与代码的顺序一致。在单线程环境下,由于编译器会对代码进行优化,可能会将代码的执行顺序重排,这样可以提高程序的执行效率。但在多线程环境下,指令重排可能导致线程之间出现数据不一致的问题,使用volatile可以避免这种情况。

需要注意的是,volatile不能保证原子性,即不能保证多个线程同时对变量进行修改时的正确性。如果需要保证原子性,可以使用Atomic类或synchronized关键字。

内存可见性

内存可见性(Memory Visibility)是指当某个线程正在使用对象状态而另一个线程在同时修改该状态,需要确保当一个线程修改了对象状态后,其他线程能够看到发生的状态变化。

可见性错误是指当读操作与写操作在不同的线程中执行时,我们无法确保执行读操作的线程能适时地看到其他线程写入的值,有时甚至是根本不可能的事情。

我们可以通过同步来保证对象被安全地发布。除此之外我们也可以使用一种更加轻量级的volatile 变量。

package com.lxg.juc.readwrite;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * @auther xiaolin
 * @creatr 2023/4/17 22:49
 */

//创建资源类
class MyCache{
    //创建Map集合
    private volatile Map<String,Object> map = new HashMap<>();

    //创建读写锁对象
    private ReadWriteLock lock = new ReentrantReadWriteLock();



    //写入数据
    public void put(String key,Object value){
        //加写锁
        lock.writeLock().lock();

        try {
            System.out.println(Thread.currentThread().getName()+" 正在写入:"+key);
            //暂停一会,模拟网络延迟
            Thread.sleep(300);
            map.put(key,value);
            System.out.println(Thread.currentThread().getName()+" 写入完成:"+key);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            //释放写锁
            lock.writeLock().unlock();
        }

    }

    //读取数据
    public Object get(String key){

        //加读锁
        lock.readLock().lock();
        Object result = null;
        try {
            System.out.println(Thread.currentThread().getName()+" 正在读取:"+key);
            //暂停一会,模拟网络延迟
            Thread.sleep(300);
            result = map.get(key);
            System.out.println(Thread.currentThread().getName()+" 读取完成:"+result);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            //释放读锁
            lock.readLock().unlock();
        }

        return result;
    }


}


public class ReadWriteLockDemo {

    public static void main(String[] args) throws InterruptedException {
        MyCache myCache = new MyCache();

        //创建5个线程写入数据
        for (int i = 1; i <= 5; i++) {
             int num = i;
            new Thread(()->{
                myCache.put(num+"",num+"-value");
            },String.valueOf(i)).start();
        }

        TimeUnit.SECONDS.sleep(1);

        //创建5个线程读取数据
        for (int i = 1; i <= 5; i++) {
            int num = i;
            new Thread(()->{
                myCache.get(num+"");
            },String.valueOf(i)).start();
        }

    }

}
3.3、读写锁深入

JUC基础_第10张图片

JUC基础_第11张图片

JUC基础_第12张图片

读锁不能升级为写锁

package com.lxg.juc.readwrite;

import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * @auther xiaolin
 * @creatr 2023/4/17 23:53
 */
//读写锁降级
public class Demo1 {

    public static void main(String[] args) {

        //可重入读写锁对象
        ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
        ReentrantReadWriteLock.ReadLock readLock = rwLock.readLock();//读锁
        ReentrantReadWriteLock.WriteLock writeLock = rwLock.writeLock();//写锁

        //锁降级
        //1.先获取写锁
        writeLock.lock();
        System.out.println("xiaolin");

        //2.获取读锁
        readLock.lock();
        System.out.println("---read");

        //3.释放写锁
        writeLock.unlock();

        //4.释放读锁
        readLock.unlock();
    }
}

十、BlockingQueue阻塞队列

1、BlockingQueue简介和架构

BlockingQueue是Java中的一个线程安全的队列接口,它提供了一种阻塞式的队列操作方式,可以用于多线程之间的数据共享和通信。在多线程编程中,线程之间需要协调和同步,BlockingQueue提供了一种简单而有效的方式来实现这种协调和同步。

BlockingQueue的实现类可以基于不同的数据结构,例如数组或链表。在使用BlockingQueue时,可以使用put()方法将元素添加到队列中,如果队列已满,则put()方法会阻塞等待队列中有空闲空间;可以使用take()方法从队列中取出元素,如果队列为空,则take()方法会阻塞等待队列中有元素可取。

BlockingQueue还提供了其他的方法,例如offer()、poll()、peek()等,它们与put()、take()方法的区别在于,当队列已满或为空时,offer()和poll()方法会返回false或null,而不是阻塞等待。另外,BlockingQueue还提供了一些定时阻塞的方法,例如offer(timeout)、poll(timeout)等,它们可以在一定时间内等待队列的状态发生变化。

BlockingQueue的实现类有很多种,例如ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue等,每种实现类都有其特点和适用场景。例如,ArrayBlockingQueue适用于固定大小的队列,LinkedBlockingQueue适用于可变大小的队列,PriorityBlockingQueue适用于按优先级排序的队列。


Concurrent包中,BlockingQueue很好的解决了多线程中,如何高效安全
“传输”数据的问题。通过这些高效并且线程安全的队列类,为我们快速搭建
高质量的多线程程序带来极大的便利。本文详细介绍了BlockingQueue家庭
中的所有成员,包括他们各自的功能以及常见使用场景。“
阻塞队列,顾名思义,首先它是一个队列,通过一个共享的队列,可以使得数据
由队列的一端输入,从另外一端输出;

JUC基础_第13张图片

当队列是空的,从队列中获取元素的操作将会被阻塞
当队列是满的,从队列中添加元素的操作将会被阻塞
试图从空的队列中获取元素的线程将会被阻塞,直到其他线程往空的队列插入新的元素
试图向已满的队列中添动加新元素的线程将会被阻塞,直到其他线程从队列中移除一个或多个元素或者完全清空,使队列变得空闲起来并后续新增

常用的队列主要有以下两种:

先进先出(FIFO):先插入的队列的元素也是最先出队列,类似于排队的功能。从某种程度上来说这种队列也体现了一种公平性

后进先出(IFO):后插入队列的元素最先出队列,这种队列优先处理最近发
生的事件(栈)

在多线程领域:所谓阻塞,在某些情况下会挂起线程(即阻塞),一日条件满浞,被挂起
的线程又会自动被唤起

为什么需要Blocking Queue.
好处是我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,因为这一切
BlockingQueue都给你一手包办了

在concurrent包发布以前,在多线程环境下,我们每个程序员都必须去自己控制这些细
节,尤其还要兼顾效率和线程安全,而这会给我们的程序带来不小的复杂度。

2、阻塞队列的分类

2.1、ArrayBlockingQueue(常用)
ArrayBlockingQueue是BlockingQueue接口的一个实现类,它是一个基于数组的有界阻塞队列。它的容量在创建时就确定了,且不可更改。由于基于数组实现,因此ArrayBlockingQueue在插入和删除元素时可以获得较高的性能。

ArrayBlockingQueue提供了与BlockingQueue接口相同的方法,例如put()、take()、offer()、poll()等。其中,put()和take()方法是阻塞的,当队列已满或为空时,put()方法会阻塞等待队列中有空闲空间,take()方法会阻塞等待队列中有元素可取。而offer()和poll()方法则是非阻塞的,当队列已满或为空时,offer()方法会返回false,poll()方法会返回null。

由于ArrayBlockingQueue是有界队列,因此它的容量是固定的,不能动态改变。在创建ArrayBlockingQueue时,需要指定队列的容量和是否使用公平锁。公平锁表示线程获取锁的顺序与线程进入等待队列的顺序一致,是一种公平的竞争方式;非公平锁则是一种不公平的竞争方式,允许插队,可以获得更高的吞吐量。

ArrayBlockingQueue适用于生产者-消费者模式的场景,它可以作为生产者和消费者之间的数据缓冲区,实现线程之间的数据共享和通信。由于是有界队列,因此可以有效地控制数据的流量,防止数据的过度生产或消费。

基于数组的阻塞队列实现,在ArrayBlockingQueue内部,维护了一个定长数
组,以便缓存队列中的数据对象,这是一个常用的阻塞队列,除了一个定长数
组外,Array BlockingQueue内部还保存着两个整形变量,分别标识着队列的
头部和尾部在数组中的位置。

ArrayBlockingQueue在生产者放入数据和消费者获取数据,都是共用同一个锁对象,由此也意味着两者无法真正并行运行,这点尤其不同于LinkedBlockingQueue;按照实现原理来分析,ArrayBlockingQueue完全可以采用分离锁,从而实现生产者和消费者操作的完全并行运行。Doug Leà之所以没这样去做,也许是因为ArrayBlocking Queue的数据写入和获取操作已经足够轻巧,以至于引入独立的锁机制,除了给代码带来额外的复杂性外,其在性能上完全占不到任何便宜。ArrayBlockingQueue和LinkedBlockingQueue间还有一个明显的不同之处在于,前者在插入或删除元素时不会产生或销毁任何额外的对象实例,而后者则会生成一个额外的Node对象。这在长时间内需要高效并发地处理大批量数据的系统中,其对于GC的影响还是存在一定的区别。而在创建ArrayBlockingQueue时,我们还
可以控制对象的内部锁是否采用公平锁,默认采用非公平锁

一句话总结:由数组结构组成的有界阻塞队列

ArrayBlockingQueue 的删除元素操作性能较高,是因为它使用了类似于循环队列的数据结构,通过维护一个 head 和 tail 指针来实现。当一个元素被删除时,只需要将 head 指针向后移动一位即可,不需要将后面的元素往前挪一位。

具体地说,ArrayBlockingQueue 内部使用一个数组来存储元素,head 和 tail 分别指向队列的头和尾。当一个元素被插入队列时,tail 指针向后移动一位;当一个元素被删除时,head 指针向后移动一位。因此,删除元素时只需要将 head 指针向后移动一位,不需要移动其他元素。

需要注意的是,ArrayBlockingQueue 的删除操作只是将 head 指针向后移动一位,并没有真正地将数组中的元素删除。因此,在进行删除操作时,需要将被删除元素的引用设置为 null,以便让垃圾回收器回收该元素的内存空间。

2.2、 LinkedBlockingQueue(常用)

基于链表的阻塞队列,同ArrayListBlockingQueue类似,其内部也维持着一
个数据缓冲队列(该队列由一个链表构成),当生产者往队列中放入一个数据
时,队列会从生产者手中获取数据,并缓存在队列内部,而生产者立即返回;
只有当队列缓中区达到最大值缓存容量时(LinkedBlockingQueue可以通过
构造函数指定该值),才会阻塞生产者队列,直到消费者从队列中消费掉一份
数据,生产者线程会被唤醒,反之对于消费者这端的处理也基于同样的原理。
而LinkedBlockingQueue之所以能够高效的处理并发数据,还因为其对于生
产者端和消费者端分别采用了独立的锁来控制数据同步,这也意味着在高并发
的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列
的并发性能。

ArrayBlockingQueue和LinkedBlockingQueue是两个最普通也是最常用
的阻塞队列,一般情况下,在处理多线程间的生产者消费者问题,使用这两个
类足以。

一句话总结:由链表结构组成的有界(但大小默认值为integer.MAX VALUE)阻塞队列

2.3、DelayQueue

DelayQueue中的元素只有当其指定的延迟时间到了,才能够从队列中获取到
该元素。DelayQueue是一个没有大小限制的队列,因此往队列中插入数据的
操作(生产者)永远不会被阻塞,而只有获取数据的操作(消费者)才会被阻
塞。

一句话总结:使用优先级队列实现的延迟无界阻塞队列。

2.4、PriorityBlockingQueue

基于优先级的阻塞队列(优先级的判断通过构造函数传入的Compator对象来决定),但需要注意的是PriorityBlockingQueue并不会阻塞数据生产者,而只会在没有可消费的数据时,阻塞数据的消费者。

因此使用的时候要特别注意,生产者生产数据的速度绝对不能快于消费者消费
数据的速度,否则时间一长,会最终耗尽所有的可用堆内存空间。

在实现PriorityBlockingQueue时,内部控制线程同步的锁采用的是公平锁。

一句话总结:支持优先级排序的无界阻塞队列。

2.5、SynchronousQueue

一种无缓冲的等待队列,类以于无中介的直接交易,有点像原始社会中的生产者和消费者,生产者拿着产品去集市销售给产品的最终消费者,而消费者必须亲自去集市找到所要商品的直接生产者,如果一方没有找到合适的目标,那么对不起,大家都在集市等待。相对于有缓冲的BlockingQueue来说,少了一个中间经销商的环节(缓冲区),如果有经销商,生产者直接把产品批发给经销商,而无需在意经销商最终会将这些产品卖给那些消费者,由于经销商可以库存一部分商品,因此相对于直接交易模式,总体来说采用中间经销商的模式会吞吐量高一些(可以批量买卖);但另一方面,又因为经销商的引入,使得
产品从生产者到消费者中间增加了额外的交易环节,单个产品的及时响应性能可能会降低。

声明一个SynchronousQueue有两种不同的方式,它们之间有着不太一样的行为。

公平模式和非公平模式的区别:

公平模式:SynchronousQueue会采用公平锁,并配合一个FIFO队列来阻塞多余的生产者和消费者,从而体系整体的公平策略;

非公平模式(SynchronousQueue默认):SynchronousQueue采用非公平锁,同时配合一个IFO队列来管理多余的生产者和消费者,而后一种模式,如果生产者和消费者的处理速度有差距,则很容易出现饥渴的情况,即可能有某些生产者或者是消费者的数据永远都得不到处理。

一句话总结:不存储元素的阻塞队列,也即单个元素的队列。

2.6、LinkedTransferQueue

LinkedTransferQueue是一个由链表结构组成的无界阻塞TransferQueue队列。相对于其他阻塞队列,LinkedTransferQueue多了tryTransfer和transfer方法。

LinkedTransferQueue采用一种预占模式。意思就是消费者线程取元素时,如果队列不为空,则直接取走数据,若队列为空,那就生成一个节点(节点元素为null)入队,然后消费者线程被等待在这个节点上,后面生产者线程入队时发现有一个元素为null的节点,生产者线程就不入队了,直接就将元素填充到该节点,并唤醒该节点等待的线程,被唤醒的消费者线程取走元素,从调用的方法返回。

一句话总结:由链表组成的无界阻塞队列。

2.7、LinkedBlockingDeque

LinkedBlockingDeque是一个由链表结构组成的双向阻塞队列,即可以从队列的两端插入和移除元素。

对于一些指定的操作,在插入或者获取队列元素时如果队列状态不允许该操作可能会阻塞住该线程直到队列状态变更为允许操作,这里的阻塞一般有两种情况

插入元素时:如果当前队列已满将会进入阻塞状态,一直等到队列有空的位置时再讲该元素插入,该操作可以通过设置超时参数,超时后返回false表示操作失败,也可以不设置超时参数一直阻塞,中断后抛出InterruptedException异常

读取元素时:如果当前队列为空会阻塞住直到队列不为空然后返回元素,同样可以通过设置超时参数

一句话:由链表组成的双向阻塞队列

3、BlockingQueue核心方法

JUC基础_第14张图片

4、案例

package com.lxg.juc.queue;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;

/**
 * @auther xiaolin
 * @creatr 2023/4/18 13:26
 */
public class BlockingQueueDemo {

    public static void main(String[] args) throws InterruptedException {
        //创建一个阻塞队列
        BlockingQueue<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());
//
        System.out.println(blockingQueue.add("d"));
//        System.out.println(blockingQueue.remove());
//        System.out.println(blockingQueue.remove());
//        System.out.println(blockingQueue.remove());
//        System.out.println(blockingQueue.remove());


        //第二组方法
        //特殊值
//        System.out.println(blockingQueue.offer("a"));
//        System.out.println(blockingQueue.offer("b"));
//        System.out.println(blockingQueue.offer("c"));
//        System.out.println(blockingQueue.offer("d"));
//
//        System.out.println(blockingQueue.poll());
//        System.out.println(blockingQueue.poll());
//        System.out.println(blockingQueue.poll());
//        System.out.println(blockingQueue.poll());


        //第三组方法
        //阻塞
//        blockingQueue.put("a");
//        blockingQueue.put("b");
//        blockingQueue.put("c");
        blockingQueue.put("d");
//
//        System.out.println(blockingQueue.take());
//        System.out.println(blockingQueue.take());
//        System.out.println(blockingQueue.take());
//        System.out.println(blockingQueue.take());


        //第四组方法
        //超时
        System.out.println(blockingQueue.offer("a"));
        System.out.println(blockingQueue.offer("b"));
        System.out.println(blockingQueue.offer("c"));
        System.out.println(blockingQueue.offer("d",2, TimeUnit.SECONDS));

        System.out.println(blockingQueue.poll());
        System.out.println(blockingQueue.poll());
        System.out.println(blockingQueue.poll());
        System.out.println(blockingQueue.poll(2,TimeUnit.SECONDS));
    }
}

5、小总结

1、在多线程领域:所谓的阻塞,在某些情况下会挂起线程(即阻塞),一旦条件满足,被挂起的线程又会自动被唤起

2、为什么需要BlockingQueue?

在concurrent包发布以前,在多线程环境下,我们每个程序员都必须去自己控制这些细节,尤其还要兼顾效率和线程安全,而这会给我们的程序带来不小的复杂度。使用后我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,因为这一切BlockingQueue都给你一手包办了

十一、线程池

1、线程池概述

线程池(英语:thread pool):一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。·

例子:10年前单核CPU电脑,假的多线程,像马戏团小丑玩多个球,CPU需要来回切换。现在是多核电脑,多个线程各自跑在独立的CPU上,不用切换效率高。

线程池的优势:线程池做的工作只要是控制运行的线程数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量超出数量的线程排队等候,等其他线程执行完毕,再从队列中取出任务来执行。

主要特点
降低资源消耗:通过重复利用已创建的线程降低线程创建和销毁造成的销耗。

提高响应速度:当任务到达时,任务可以不需要等待线程创建就能立即执行。

提高线程的可管理性:线程是稀缺资源,如果无限制的创建,不仅会销耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

Java中的线程池是通过Executor框架实现的,该框架中用到了Executor,Executors,
ExecutorService,ThreadPoolExecutor这几个类

2、线程池架构

JUC基础_第15张图片

3、线程池的使用方式

3.1、Executors.newFixedThreadPool(int)

Executors.newFixedThreadPool(int)是Java中的一个静态方法,它返回一个固定大小的线程池。在这个线程池中,线程的数量是固定的,不会随着任务的数量的增加而增加。如果提交的任务数量超过了线程池中的线程数量,那么这些任务会被放在一个任务队列中,等待线程池中的线程执行完任务后再执行。该方法接受一个整数参数,表示线程池中的线程数量。

3.2、Executors.newSingleThreadExecutor()

Executors.newSingleThreadExecutor() 是 Java 中提供的一个 ExecutorService 的工厂方法,用于创建一个只有一个线程的线程池。它会创建一个只有一个线程的线程池,并且这个线程池中的线程只会顺序执行提交的任务。如果该线程在执行任务时出现异常,它会自动创建一个新线程继续执行后续任务,保证线程池中至少有一个线程在执行任务。这个方法返回的是一个 ExecutorService 对象,可以用来提交任务和关闭线程池。

3.3、Executors.newCachedThreadPool()

Executors.newCachedThreadPool() 是 Java 中提供的一个 ExecutorService 的工厂方法,用于创建一个可缓存的线程池。它会创建一个可以根据需要自动调整大小的线程池,当线程池中的线程空闲时间超过 60 秒时,它会自动回收这个线程;当提交的任务数超过了当前线程池的容量时,它会自动增加线程池的容量。这个方法返回的是一个 ExecutorService 对象,可以用来提交任务和关闭线程池。由于线程池大小会根据任务数量自动调整,因此适用于执行大量短期异步任务的场景。

3.4、案例

线程池的submit和execute方法都可以用来向线程池提交任务,但是它们有以下区别:

  1. submit方法可以接受Callable和Runnable类型的任务,而execute方法只能接受Runnable类型的任务。

  2. submit方法会返回一个Future对象,可以通过该对象获取任务的执行结果或取消任务的执行,而execute方法没有返回值。

  3. submit方法可以通过参数指定任务的返回值类型,而execute方法不支持这种功能。

  4. submit方法可以抛出异常,而execute方法不会抛出异常。

因此,如果需要获取任务的执行结果或取消任务的执行,建议使用submit方法;如果不需要获取任务的执行结果,建议使用execute方法。

package com.lxg.juc.pool;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @auther xiaolin
 * @creatr 2023/4/18 14:32
 */
public class ThreadPoolDemo1 {
    public static void main(String[] args) {
        //一池5个处理线程
        ExecutorService threadPool1 = Executors.newFixedThreadPool(5);//5个窗口

        //一池1个处理线程
        ExecutorService threadPool2 = Executors.newSingleThreadExecutor();//1个窗口

        //一池N个处理线程
        ExecutorService threadPool3 = Executors.newCachedThreadPool();//N个窗口



        //10个人来银行办理业务,目前池子里有5个工作人员提供服务
        //2.为线程池中的线程分配任务
        /*try{
            for (int i = 1; i <= 10; i++) {
                //执行

                threadPool1.execute(()->{
                    //模拟办理业务
                    try {
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    System.out.println(Thread.currentThread().getName()+" 正在办理业务");
                });
            }
        }catch(Exception e){
            e.printStackTrace();
        }finally{
            //3.关闭线程池
            threadPool1.shutdown();
        }*/


        /*try{
            for (int i = 1; i <= 10; i++) {
                //执行

                threadPool2.execute(()->{
                    //模拟办理业务
                    try {
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    System.out.println(Thread.currentThread().getName()+" 正在办理业务");
                });
            }
        }catch(Exception e){
            e.printStackTrace();
        }finally{
            //3.关闭线程池
            threadPool2.shutdown();
        }*/

        try{
            for (int i = 1; i <= 100; i++) {
                //执行

                threadPool3.execute(()->{
                    //模拟办理业务
                    try {
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    System.out.println(Thread.currentThread().getName()+" 正在办理业务");
                });
            }
        }catch(Exception e){
            e.printStackTrace();
        }finally{
            //3.关闭线程池
            threadPool3.shutdown();
        }
    }
}

4、ThreadPoolExecutor线程池的七个参数

JUC基础_第16张图片

  1. corePoolSize:线程池的核心线程数,即线程池中始终保持的线程数量。如果提交的任务数小于corePoolSize,线程池中的线程数会一直保持在corePoolSize这个数量级别上。

  2. maximumPoolSize:线程池中最大线程数,即线程池中允许创建的最大线程数量。如果提交的任务数大于corePoolSize,那么线程池会根据需要创建新的线程,直到线程数达到maximumPoolSize为止。

  3. keepAliveTime:线程池中空闲线程的存活时间。如果线程池中的线程数量大于corePoolSize,并且这些线程处于空闲状态,那么多余的空闲线程会在keepAliveTime时间后被回收。

  4. unit:keepAliveTime的时间单位。

  5. workQueue:任务队列,用于存放等待执行的任务。当线程池中的线程数量达到corePoolSize时,新的任务会被加入到任务队列中等待执行。

  6. threadFactory:用于创建新线程的工厂类。

  7. handler:当线程池中的线程数已经达到maximumPoolSize,并且任务队列已经满了,那么就需要使用handler来处理新的任务。handler有四种选择:ThreadPoolExecutor.AbortPolicy(抛出异常)、ThreadPoolExecutor.CallerRunsPolicy(由提交任务的线程来执行)、ThreadPoolExecutor.DiscardOldestPolicy(抛弃队列中最旧的任务)、ThreadPoolExecutor.DiscardPolicy(抛弃当前任务)。

这七个参数共同作用,决定了线程池的行为和特性。可以根据实际需求来调整这些参数,以达到最佳性能。

5、线程池底层工作原理

线程池是一种管理和复用线程的机制,它可以在需要时分配线程来执行任务,并在任务完成后将线程返回到线程池中以供下次使用。线程池的底层原理涉及以下几个方面:

  1. 线程池的管理:线程池通常由一个线程池管理器来管理,它负责创建、销毁和管理线程池中的线程。线程池管理器还可以监控线程池中的活动线程,以及管理任务队列和工作线程之间的协调。

  2. 线程池的工作线程:线程池中的工作线程是执行任务的线程,它们从任务队列中获取任务并执行。线程池中的工作线程通常是预先创建的,以避免频繁地创建和销毁线程。在任务执行完毕后,工作线程将返回到线程池中以供下次使用。

  3. 任务队列的管理:线程池中的任务队列是用来存储等待执行的任务的,它通常是一个先进先出(FIFO)队列。当一个任务被提交到线程池时,它将被添加到任务队列中,等待工作线程的执行。任务队列的管理包括添加和删除任务,以及管理任务的优先级和顺序。

  4. 线程池的调度:线程池的调度是指如何选择工作线程来执行任务。线程池通常有一个调度器,它负责从任务队列中选择任务,并将任务分配给空闲的工作线程执行。调度器还可以根据任务的优先级和其他属性来调整任务的执行顺序。

总之,线程池的底层原理涉及线程的管理、工作线程的执行、任务队列的管理和调度器的调度等方面。通过合理的线程池设计和优化,可以提高系统的并发性能和稳定性。

JUC基础_第17张图片

拒绝策略

ThreadPoolExecutor.AbortPolicy(抛出异常):
默认。直接抛出RejectExecutionException异常阻止系统正常运行

ThreadPoolExecutor.CallerRunsPolicy(由提交任务的线程来执行):
“调用者执行”一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量

ThreadPoolExecutor.DiscardOldestPolicy(抛弃队列中最旧的任务):
抛弃队列中等待最久的任务,然后把当前任务加入队列中,尝试再次提交当前任务

ThreadPoolExecutor.DiscardPolicy(抛弃当前任务):
该策略默默地抛弃无法处理的任务,不予任何处理也不抛出异常。如果运行任务丢失,这是最好的一种策略

6、自定义线程池

6.1、为什么不允许Executors的方式创建线程池

JUC基础_第18张图片

package com.lxg.juc.pool;

import java.util.concurrent.*;

/**
 * @auther xiaolin
 * @creatr 2023/4/18 16:22
 */

//自定义线程池创建
public class ThreadPoolDemo2 {

    public static void main(String[] args) {

        ExecutorService threadPool = new ThreadPoolExecutor(
                2,
                5,
                2L,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(3),
                new MyThreadFactory("自定义线程", Thread.MAX_PRIORITY),
//                Executors.defaultThreadFactory(),
//                new ThreadPoolExecutor.AbortPolicy(),
                new MyRejectedExecutionHandler()
        );
        try{
            for (int i = 1; i <= 10; i++) {
                //执行

                threadPool.execute(()->{
                    //模拟办理业务
                    try {
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    System.out.println(Thread.currentThread().getName()+" 正在办理业务");
                });
            }
        }catch(Exception e){
            e.printStackTrace();
        }finally{
            //3.关闭线程池
            threadPool.shutdown();
        }
    }
}

//自定义拒绝策略
class MyRejectedExecutionHandler implements RejectedExecutionHandler {

    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        System.out.println("Task " + r.toString() + " is rejected.");
    }
}


// 定义一个名为 MyThreadFactory 的类,实现 ThreadFactory 接口
class MyThreadFactory implements ThreadFactory {

    // 定义私有变量 namePrefix,表示线程名前缀
    private final String namePrefix;

    // 定义私有变量 priority,表示线程优先级
    private final int priority;

    private int i = 1;

    // 构造方法,接收两个参数:namePrefix 和 priority
    public MyThreadFactory(String namePrefix, int priority) {
        // 将传入的 namePrefix 赋值给类中的 namePrefix 变量
        this.namePrefix = namePrefix;
        // 将传入的 priority 赋值给类中的 priority 变量
        this.priority = priority;
    }

    // 实现 ThreadFactory 接口中的 newThread 方法
    @Override
    public Thread newThread(Runnable r) {
        // 创建一个新的线程对象,传入两个参数:runnable 和线程名
        Thread t = new Thread(r, namePrefix + "-" + i++);
        // 设置线程的优先级,使用传入的 priority 变量
        t.setPriority(priority);
        // 返回创建的新线程对象
        return t;
    }
}


自定义线程工厂类:

自定义线程工厂类可以让我们更加灵活地创建线程,可以自定义线程的名称、优先级等属性。下面是一个简单的自定义线程工厂类的示例代码:

import java.util.concurrent.ThreadFactory;

public class MyThreadFactory implements ThreadFactory {

    private final String namePrefix;
    private final int priority;

    public MyThreadFactory(String namePrefix, int priority) {
        this.namePrefix = namePrefix;
        this.priority = priority;
    }

    @Override
    public Thread newThread(Runnable r) {
        Thread t = new Thread(r, namePrefix + "-" + System.currentTimeMillis());
        t.setPriority(priority);
        return t;
    }
}

这个线程工厂类提供了一个构造函数,可以指定线程名称的前缀和线程优先级。在 newThread 方法中,我们创建了一个新的线程,并设置了线程的名称和优先级,然后返回这个新的线程。我们可以使用这个自定义的线程工厂类来创建线程池,例如:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class MyThreadPool {

    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(5, new MyThreadFactory("MyThread", Thread.MAX_PRIORITY));
        for (int i = 0; i < 10; i++) {
            executor.execute(() -> {
                System.out.println(Thread.currentThread().getName() + " is running.");
            });
        }
        executor.shutdown();
    }
}

在这个示例代码中,我们使用了 MyThreadFactory 来创建线程池,指定了线程名称的前缀为 “MyThread”,线程优先级为最高优先级。然后我们提交了 10 个任务给线程池执行,每个任务都会打印当前线程的名称。

自定义线程池拒绝策略:

线程池拒绝策略是指当线程池无法处理新的任务时,该如何处理这些任务。通常情况下,线程池会有一个任务队列来缓存等待执行的任务,但是如果任务队列已满,此时新的任务又不断地被提交给线程池,就会出现无法处理的情况。这时就需要使用线程池的拒绝策略来处理这些无法处理的任务。

下面是一个简单的自定义线程池拒绝策略的示例代码:

import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ThreadPoolExecutor;

public class MyRejectedExecutionHandler implements RejectedExecutionHandler {

    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        System.out.println("Task " + r.toString() + " is rejected.");
    }
}

这个拒绝策略只是简单地打印了一条日志,表示当前任务被拒绝了。我们可以使用这个自定义的拒绝策略来创建线程池,例如:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;

public class MyThreadPool {

    public static void main(String[] args) {
        ExecutorService executor = new ThreadPoolExecutor(2, 2,
                0L, TimeUnit.MILLISECONDS,
                new ArrayBlockingQueue<>(1),
                new MyThreadFactory("MyThread", Thread.MAX_PRIORITY),
                new MyRejectedExecutionHandler());
        for (int i = 0; i < 5; i++) {
            executor.execute(() -> {
                System.out.println(Thread.currentThread().getName() + " is running.");
            });
        }
        executor.shutdown();
    }
}

在这个示例代码中,我们使用了 MyRejectedExecutionHandler 来处理被拒绝的任务。我们创建了一个线程池,指定了核心线程数为 2,最大线程数为 2,任务队列容量为 1,然后提交了 5 个任务给线程池执行。由于任务队列容量为 1,所以只有 1 个任务能够被缓存,剩下的 4 个任务会被拒绝,并由 MyRejectedExecutionHandler 处理。

十二、Fork/Join分支合并框架

1、Fork/join框架简介

Fok/小oin它可以将一个大的任务拆分成多个子任务进行并行处理,最后将子任务结果合并成最后的计算结果,并进行输出。Fork/小oin框架要完成两件事情:

Fok:把一个复杂任务进行分拆,大事化小
Join:把分拆任务的结果进行合并

JUC基础_第19张图片

  1. 任务分割:首先Fok/小oin框架需要把大的任务分割成足够小的子任务,如果子任务比较大的话还要对子任务进行继续分割

  2. 执行任务并合并结果:分割的子任务分别放到双端队列里,然后几个启动线程分别从双端队列里获取任务执行。子任务执行完的结果都放在另外一个队列里,启动一个线程从队列里取数据,然后合并这些数据。

    在Java的Fork/小oin框架中,使用两个类完成上述操作

ForkJoinTask:我们要使用Fork/小oin框架,首先需要创建一个ForkJoin任务。该类提供了在任务中执行fok和join的机制。通常情况下我们不需要直接集成ForkJoinTask类,只需要继承它的子类,Fork/小oin框架提供了两个子类:

  • RecursiveAction:用于没有返回结果的任务
  • b.RecursiveTask:用于有返回结果的任务

ForkJoinPoo:ForkJoinTask需要通过ForkJoinPool来执行

RecursiveTask:继承后可以实现递归(自己调自己)调用的任务

Fork/小oin框架的实现原理

ForkJoinPool由ForkJoinTask数组和ForkJoinWorkerThread数组组成,ForkJoinTask数组负责将存放以及将程序提交给ForkJoin Pool,而ForkJoinWorkerThread负责执行这些任务。

2、CPU密集型和I/O密集型

CPU密集型是指需要大量计算资源的任务,例如复杂的数学运算、图像处理、视频编解码、科学计算等。这些任务通常需要大量的CPU计算能力,而磁盘I/O和网络I/O等其他资源的占用相对较少。在这种情况下,CPU的计算能力成为了瓶颈,可能导致程序性能受到限制。因此,在处理CPU密集型任务时,需要使用优化的算法和技术,以充分利用CPU资源,提高程序性能。

相对而言,I/O密集型任务则需要大量的磁盘I/O和网络I/O资源,例如读写大文件、数据库操作、网络通信等。在这种情况下,CPU计算能力通常不是瓶颈,而是磁盘I/O和网络I/O的速度成为了限制性因素。因此,在处理I/O密集型任务时,需要使用优化的I/O操作和技术,以提高I/O效率,从而提高程序性能。

3、Fork方法

JUC基础_第20张图片

JUC基础_第21张图片

java中变量什么时候需要初始化而什么时候不需要

Java中变量需要在使用之前进行初始化,否则会出现编译错误。但是,有些情况下变量的初始化可以在声明时进行,而不需要在使用之前进行初始化。以下是一些情况:

1. 类变量和实例变量可以在声明时进行初始化,因为它们有默认值。
2. 局部变量必须在使用之前进行初始化,否则会出现编译错误。
3. 如果变量是final类型,则必须在声明时进行初始化,否则会出现编译错误。
4. 如果变量是静态final类型,则必须在声明时或静态初始化块中进行初始化,否则会出现编译错误。

4、案例

package com.lxg.juc.forkjoin;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.concurrent.RecursiveTask;

/**
 * @auther xiaolin
 * @creatr 2023/4/18 17:24
 */

class MyTask extends RecursiveTask<Integer>{

    //拆分差值不超过10,计算10以内运算
    private static final Integer ADJUST_VALUE = 10;

    private int begin;//拆分开始值

    private int end;//拆分结束值


    private int result;//计算结果

    //创建有参构造器
    public MyTask(int begin, int end) {
        this.begin = begin;
        this.end = end;
    }

    //拆分和合并过程
    @Override
    protected Integer compute() {
        //判断相加两个数之间的差值是否小于10
        if((end - begin) <= ADJUST_VALUE){
            //计算
            for (int i = begin; i <= end; i++) {
                result += i;
            }
        }else{
            //拆分
            //计算中间值
            int middle = (begin + end) / 2;
            //拆分任务
            MyTask task01 = new MyTask(begin, middle);
            MyTask task02 = new MyTask(middle + 1, end);
            //执行任务
            task01.fork();
            task02.fork();
            //合并
            result = task01.join() + task02.join();
        }
        return result;
    }
}

public class ForkJoinDemo {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //创建MyTask对象
        MyTask task = new MyTask(0, 100);

        //第一种
        //创建分支合并池对象
        ForkJoinPool forkJoinPool = new ForkJoinPool();
        ForkJoinTask<Integer> forkJoinTask = forkJoinPool.submit(task);
        //获取结果
        Integer result = forkJoinTask.get();



        //第二种
//        Integer result = task.invoke();
        
        System.out.println(result);

        //关闭池对象
        forkJoinPool.shutdown();
    }
}

这两种方式的效果是一样的,都是提交任务到ForkJoinPool中执行,并等待任务完成返回结果。

但是,它们的实现方式有所不同。

在第一种方式中,我们首先创建了一个ForkJoinPool对象,然后使用submit方法提交了任务。submit方法会返回一个ForkJoinTask对象,我们可以使用该对象的get方法获取任务的结果。

在第二种方式中,我们直接调用了任务的invoke方法,该方法会将任务提交到ForkJoinPool中执行,并等待任务完成返回结果。

总的来说,这两种方式的效果是一样的,但是在不同的场景下可能会有不同的选择。如果我们需要重复使用同一个ForkJoinPool对象来执行多个任务,那么第一种方式可能更适合;如果我们只需要执行一个任务,那么第二种方式更简单直接。

5、总结

采用 “工作窃取”模式(work-stealing):

当执行新的任务时它可以将其拆分分成更小的任务执行,并将小任务加到线程队列中,然后再从一个随机线程的队列中偷一个并把它放在自己的队列中。

相对于一般的线程池实现,fork/join框架的优势体现在对其中包含的任务的处理方式上.在一般的线程池中,如果一个线程正在执行的任务由于某些原因无法继续运行,那么该线程会处于等待状态。而在fork/join框架实现中,如果某个子问题由于等待另外一个子问题的完成而无法继续运行。那么处理该子问题的线程会主动寻找其他尚未运行的子问题来执行.这种方式减少了线程的等待时间,提高了性能。

十三、CompletableFuture异步回调

1、概述

CompletableFuture是Java 8中新增的一个类,用于支持异步编程和回调操作。它可以让我们更加方便地进行异步操作,并且支持链式调用和组合操作。

下面是一个简单的示例,展示了如何使用CompletableFuture进行异步回调:

package com.lxg.juc.completablefuture;

import java.util.concurrent.CompletableFuture;

/**
 * @auther xiaolin
 * @creatr 2023/4/18 17:51
 */
public class CompletableFutureDemo {
    public static void main(String[] args) {
        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
            // 模拟耗时任务
            try {
                System.out.println("开始执行异步任务...");
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 异步任务,返回一个字符串
            return "Hello, World!";
        });

        // 注册回调函数,在异步任务完成后执行
        future.thenAccept(result -> {
            System.out.println("异步任务完成,结果为:" + result);
        });

        // 等待异步任务完成
        System.out.println("等待异步任务完成...");
        
        future.join();

        System.out.println("结束等待,程序退出...");
    }
}

在上面的示例中,我们创建了一个CompletableFuture对象,并使用supplyAsync方法传入一个异步执行的任务,该任务会返回一个字符串。接着,我们使用thenAccept方法注册了一个回调操作,该操作会在任务执行完成后被调用,并将任务的结果作为参数传入。

当执行完异步任务后,回调操作会被自动执行,并输出字符串Hello, CompletableFuture!

除了thenAccept方法,CompletableFuture还提供了许多其他的回调方法,例如thenApplythenComposethenCombine等,可以根据不同的需求进行选择和组合。

2、案例

2.1、没有返回值和有返回值的异步调用
package com.lxg.juc.completablefuture;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;

/**
 * @auther xiaolin
 * @creatr 2023/4/18 17:57
 */
public class CompleteableFutureDemo2 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //异步调用,无返回值
        CompletableFuture<Void> completableFuture1 = CompletableFuture.runAsync(() -> {
            System.out.println(Thread.currentThread().getName() + " completableFuture1:runAsync=>Void");
        });

        completableFuture1.get();

        //异步调用,有返回值
        CompletableFuture<Integer> completableFuture2 = CompletableFuture.supplyAsync(() -> {
            System.out.println(Thread.currentThread().getName() + " completableFuture2:supplyAsync=>Integer");
            //模拟异常
            int i = 10 / 0;
            return 1024;
        });



        System.out.println(completableFuture2.whenComplete((t, u) -> {
            System.out.println("t=>" + t); //正常的返回结果
            System.out.println("u=>" + u); //异常信息
        }).exceptionally(f -> {
            System.out.println("exception=>" + f.getMessage());
            return 4444;
        }).get());

    }
}

thenAcceptwhenComplete的区别在于:

  1. thenAccept方法的操作只有在CompletableFuture正常完成时才会执行,而whenComplete方法的操作无论CompletableFuture是否抛出异常都会执行。
  2. thenAccept方法不接收异常信息,只接收CompletableFuture的结果,而whenComplete方法接收CompletableFuture的结果和异常信息。

因此,如果只需要在CompletableFuture正常完成时执行一些操作,可以使用thenAccept方法;如果需要无论CompletableFuture是否抛出异常都执行一些操作,可以使用whenComplete方法。

exceptionally(Function fn)`:当CompletableFuture抛出异常时,执行指定的函数并返回一个新的CompletableFuture,新的CompletableFuture的结果类型与原始类型相同。例如:

javaCompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
    throw new RuntimeException("exception");
}).exceptionally(e -> {
    System.out.println(e.getMessage()); // 输出:exception
    return 0;
});
System.out.println(future.get()); // 输出:0

行异步操作,并且支持链式调用和组合操作。

下面是一个简单的示例,展示了如何使用CompletableFuture进行异步回调:

package com.lxg.juc.completablefuture;

import java.util.concurrent.CompletableFuture;

/**
 * @auther xiaolin
 * @creatr 2023/4/18 17:51
 */
public class CompletableFutureDemo {
    public static void main(String[] args) {
        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
            // 模拟耗时任务
            try {
                System.out.println("开始执行异步任务...");
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 异步任务,返回一个字符串
            return "Hello, World!";
        });

        // 注册回调函数,在异步任务完成后执行
        future.thenAccept(result -> {
            System.out.println("异步任务完成,结果为:" + result);
        });

        // 等待异步任务完成
        System.out.println("等待异步任务完成...");
        
        future.join();

        System.out.println("结束等待,程序退出...");
    }
}

在上面的示例中,我们创建了一个CompletableFuture对象,并使用supplyAsync方法传入一个异步执行的任务,该任务会返回一个字符串。接着,我们使用thenAccept方法注册了一个回调操作,该操作会在任务执行完成后被调用,并将任务的结果作为参数传入。

当执行完异步任务后,回调操作会被自动执行,并输出字符串Hello, CompletableFuture!

除了thenAccept方法,CompletableFuture还提供了许多其他的回调方法,例如thenApplythenComposethenCombine等,可以根据不同的需求进行选择和组合。

2、案例

2.1、没有返回值和有返回值的异步调用
package com.lxg.juc.completablefuture;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;

/**
 * @auther xiaolin
 * @creatr 2023/4/18 17:57
 */
public class CompleteableFutureDemo2 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //异步调用,无返回值
        CompletableFuture<Void> completableFuture1 = CompletableFuture.runAsync(() -> {
            System.out.println(Thread.currentThread().getName() + " completableFuture1:runAsync=>Void");
        });

        completableFuture1.get();

        //异步调用,有返回值
        CompletableFuture<Integer> completableFuture2 = CompletableFuture.supplyAsync(() -> {
            System.out.println(Thread.currentThread().getName() + " completableFuture2:supplyAsync=>Integer");
            //模拟异常
            int i = 10 / 0;
            return 1024;
        });



        System.out.println(completableFuture2.whenComplete((t, u) -> {
            System.out.println("t=>" + t); //正常的返回结果
            System.out.println("u=>" + u); //异常信息
        }).exceptionally(f -> {
            System.out.println("exception=>" + f.getMessage());
            return 4444;
        }).get());

    }
}

thenAcceptwhenComplete的区别在于:

  1. thenAccept方法的操作只有在CompletableFuture正常完成时才会执行,而whenComplete方法的操作无论CompletableFuture是否抛出异常都会执行。
  2. thenAccept方法不接收异常信息,只接收CompletableFuture的结果,而whenComplete方法接收CompletableFuture的结果和异常信息。

因此,如果只需要在CompletableFuture正常完成时执行一些操作,可以使用thenAccept方法;如果需要无论CompletableFuture是否抛出异常都执行一些操作,可以使用whenComplete方法。

exceptionally(Function fn)`:当CompletableFuture抛出异常时,执行指定的函数并返回一个新的CompletableFuture,新的CompletableFuture的结果类型与原始类型相同。例如:

javaCompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
    throw new RuntimeException("exception");
}).exceptionally(e -> {
    System.out.println(e.getMessage()); // 输出:exception
    return 0;
});
System.out.println(future.get()); // 输出:0

你可能感兴趣的:(java)