JUC笔记

1. 什么是JUC

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

2.线程和进程

进程是操作系统中的应用程序、是资源分配的基本单位,线程是用来执行具体的任务和功能,是CPU调度和分派的最小单位

一个进程往往可以包含多个线程,至少包含一个

1)进程

一个程序,QQ.EXE Music.EXE;数据+代码+pcb

一个进程可以包含多个线程,至少包含一个线程!

Java默认有几个线程?2个线程! main线程、GC线程

2)线程

开了一个进程Typora,写字,等待几分钟会进行自动保存(线程负责的)

对于Java而言:Thread、Runable、Callable进行开启线程的。

提问?JAVA真的可以开启线程吗? 开不了的!

Java是没有权限去开启线程、操作硬件的,这是一个native的一个本地方法,它调用的底层的C++代码。

▼
text
复制代码
public synchronized void start() { /** * This method is not invoked for the main method thread or "system" * group threads created/set up by the VM. Any new functionality added * to this method in the future may have to also be added to the VM. * * A zero status value corresponds to state "NEW". */ if (threadStatus != 0) throw new IllegalThreadStateException(); /* Notify the group that this thread is about to be started * so that it can be added to the group's list of threads * and the group's unstarted count can be decremented. */ group.add(this); boolean started = false; try { start0(); started = true; } finally { try { if (!started) { group.threadStartFailed(this); } } catch (Throwable ignore) { /* do nothing. If start0 threw a Throwable then it will be passed up the call stack */ } } } //这是一个C++底层,Java是没有权限操作底层硬件的 private native void start0();

3)并发

多线程操作同一个资源。

CPU 只有一核,模拟出来多条线程,天下武功,唯快不破。那么我们就可以使用CPU快速交替,来模拟多线程。 并发编程的本质:充分利用CPU的资源!

4) 并行

并行: 多个人一起行走

CPU多核,多个线程可以同时执行。 我们可以使用线程池!

▼
text
复制代码
//获取cpu的核数 public class Test1 { public static void main(String[] args) { //获取cpu的核数 System.out.println(Runtime.getRuntime().availableProcessors()); }

5)线程的状态(6个)

▼
text
复制代码
public enum State { //运行 NEW, //运行 RUNNABLE, //阻塞 BLOCKED, //等待 死死的等 WAITING, //超时等待 TIMED_WAITING, //终止 TERMINATED; }

6)wait/sleep

1、来自不同的类

wait => Object

sleep => Thread

一般情况企业中使用休眠是:

▼
java
复制代码
TimeUnit.DAYS.sleep(1); //休眠1天 TimeUnit.SECONDS.sleep(1); //休眠1s

2、关于锁的释放

wait 会释放锁;

sleep睡觉了,不会释放锁;

3、使用的范围是不同的

wait 必须在同步代码块中使用;

sleep 可以在任何地方睡;

4、是否需要捕获异常

wait需要捕获 InterruptedException

sleep需要捕获 InterruptedException

▼
text
复制代码
synchronized (lock) { try { lock.wait(); // 等待 } catch (InterruptedException e) { // 处理中断异常 Thread.currentThread().interrupt(); // 恢复中断状态 } }

3.Lock

1)传统的 synchronized

▼
text
复制代码
package com.marchsoft.juctest; import lombok.Synchronized; /** * Description:synchronized * * @author jiaoqianjin * Date: 2020/8/10 21:36 **/ public class Demo01 { public static void main(String[] args) { final 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(); } } // 资源类 OOP 属性、方法 class Ticket { private int number = 30; //卖票的方式 public synchronized void sale() { if (number > 0) { System.out.println(Thread.currentThread().getName() + "卖出了第" + (number--) + "张票剩余" + number + "张票"); } } }

2)Lock

Lock的三种实现 ReentrantLock(常用)

image-20200810221525974

image-20200810221731649

公平锁: 十分公平,必须先来后到~;

非公平锁: 十分不公平,可以插队;(默认为非公平锁)

▼
text
复制代码
package com.marchsoft.juctest; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; /** * Description: * * @author jiaoqianjin * Date: 2020/8/10 22:05 **/ public class LockDemo { public static void main(String[] args) { final Ticket2 ticket = new Ticket2(); 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(); } } //lock三部曲 //1、 Lock lock=new ReentrantLock(); //2、 lock.lock() 加锁 //3、 finally=> 解锁:lock.unlock(); class Ticket2 { private int number = 30; // 创建锁 Lock lock = new ReentrantLock(); //卖票的方式 public void sale() { lock.lock(); // 开启锁 try { if (number > 0) { System.out.println(Thread.currentThread().getName() + "卖出了第" + (number--) + "张票剩余" + number + "张票"); } }finally { lock.unlock(); // 关闭锁 } } }

3)Synchronized 与Lock 的区别

1、Synchronized 内置的Java关键字,Lock是一个Java接口

2、Synchronized 无法判断获取锁的状态,Lock可以判断

3、Synchronized 会自动释放锁,lock必须要手动加锁和手动释放锁!可能会遇到死锁

4、Synchronized 线程1(获得锁->阻塞)、线程2(等待);lock就不一定会一直等待下去,lock会有一个trylock去尝试获取锁,不会造成长久的等待。

5、Synchronized 是可重入锁,不可以中断的,非公平的;Lock,可重入的,可以判断锁,可以自己设置公平锁和非公平锁;

6、Synchronized 适合锁少量的代码同步问题,Lock适合锁大量的同步代码;

4. 生产者和消费者的关系

1)Synchronzied 版本

▼
text
复制代码
package com.marchsoft.juctest; /** * Description: * * @author jiaoqianjin * Date: 2020/8/10 22:33 **/ 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 { // 判断等待 if (num != 0) { this.wait(); } num++; System.out.println(Thread.currentThread().getName() + "=>" + num); // 通知其他线程 +1 执行完毕 this.notifyAll(); } // -1 public synchronized void decrement() throws InterruptedException { // 判断等待 if (num == 0) { this.wait(); } num--; System.out.println(Thread.currentThread().getName() + "=>" + num); // 通知其他线程 -1 执行完毕 this.notifyAll(); } }

2)存在问题(虚假唤醒)

问题,如果有四个线程,会出现虚假唤醒

线程1进入方法,++后释放,线程2进入方法,!=被wait,线程3进入方法,--后释放,释放的瞬间线程1拿到了锁,进入后++,线程2从wait中被唤醒,向下执行后++,出现了2这样的数字

解决方式 ,if 改为while即可,防止虚假唤醒

结论:就是用if判断的话,唤醒后线程会从wait之后的代码开始运行,但是不会重新判断if条件,直接继续运行if代码块之后的代码,而如果使用while的话,也会从wait之后的代码运行,但是唤醒后会重新判断循环条件,如果不成立再执行while代码块之后的代码块,成立的话继续wait。

3)Lock版

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

image-20200811094721678

▼
text
复制代码
package com.marchsoft.juctest; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; /** * Description: * * @author jiaoqianjin * Date: 2020/8/11 9:48 **/ public class LockCAP { 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(); // +1 public void increment() throws InterruptedException { lock.lock(); try { // 判断等待 while (num != 0) { condition.await(); } num++; System.out.println(Thread.currentThread().getName() + "=>" + num); // 通知其他线程 +1 执行完毕 condition.signalAll(); }finally { lock.unlock(); } } // -1 public void decrement() throws InterruptedException { lock.lock(); try { // 判断等待 while (num == 0) { condition.await(); } num--; System.out.println(Thread.currentThread().getName() + "=>" + num); // 通知其他线程 +1 执行完毕 condition.signalAll(); }finally { lock.unlock(); } } }

4)Condition的优势

精准的通知和唤醒的线程!

通过多个 lock.newCondition() 监视器,实现线程执行顺序

如果我们要指定通知的下一个进行顺序怎么办呢? 我们可以使用Condition来指定通知进程~

▼
text
复制代码
package com.marchsoft.juctest; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; /** * Description: * A 执行完 调用B * B 执行完 调用C * C 执行完 调用A * * @author jiaoqianjin * Date: 2020/8/11 9:58 **/ 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() + "==> AAAA" ); num = 2; condition2.signal(); }catch (Exception e) { e.printStackTrace(); }finally { lock.unlock(); } } public void printB() { lock.lock(); try { // 业务代码 判断 -> 执行 -> 通知 while (num != 2) { condition2.await(); } System.out.println(Thread.currentThread().getName() + "==> BBBB" ); num = 3; condition3.signal(); }catch (Exception e) { e.printStackTrace(); }finally { lock.unlock(); } } public void printC() { lock.lock(); try { // 业务代码 判断 -> 执行 -> 通知 while (num != 3) { condition3.await(); } System.out.println(Thread.currentThread().getName() + "==> CCCC" ); num = 1; condition1.signal(); }catch (Exception e) { e.printStackTrace(); }finally { lock.unlock(); } } } /* A==> AAAA B==> BBBB C==> CCCC A==> AAAA B==> BBBB C==> CCCC ... */

5. 8锁现象

如何判断锁的是谁!锁到底锁的是谁?

锁会锁住:对象、Class

深刻理解我们的锁

问题1

两个同步方法,先执行发短信还是打电话

▼
text
复制代码
public class dome01 { public static void main(String[] args) { Phone phone = new Phone(); new Thread(() -> { phone.sendMs(); }).start(); TimeUnit.SECONDS.sleep(1); new Thread(() -> { phone.call(); }).start(); } } class Phone { public synchronized void sendMs() { System.out.println("发短信"); } public synchronized void call() { System.out.println("打电话"); } }

输出结果为

发短信

打电话

为什么? 如果你认为是顺序在前? 这个答案是错误的!

问题2:

我们再来看:我们让发短信 延迟4s

▼
text
复制代码
public class dome01 { public static void main(String[] args) throws InterruptedException { Phone phone = new Phone(); new Thread(() -> { try { phone.sendMs(); } catch (InterruptedException e) { e.printStackTrace(); } }).start(); TimeUnit.SECONDS.sleep(1); new Thread(() -> { phone.call(); }).start(); } } class Phone { public synchronized void sendMs() throws InterruptedException { TimeUnit.SECONDS.sleep(4); System.out.println("发短信"); } public synchronized void call() { System.out.println("打电话"); } }

现在结果是什么呢?

结果:还是先发短信,然后再打电话!

why?

原因:并不是顺序执行,而是synchronized 锁住的对象是方法的调用!对于两个方法用的是同一个锁,谁先拿到谁先执行,另外一个等待

问题三

加一个普通方法

▼
text
复制代码
public class dome01 { public static void main(String[] args) throws InterruptedException { Phone phone = new Phone(); new Thread(() -> { try { phone.sendMs(); } catch (InterruptedException e) { e.printStackTrace(); } }).start(); TimeUnit.SECONDS.sleep(1); new Thread(() -> { phone.hello(); }).start(); } } class Phone { public synchronized void sendMs() throws InterruptedException { TimeUnit.SECONDS.sleep(4); System.out.println("发短信"); } public synchronized void call() { System.out.println("打电话"); } public void hello() { System.out.println("hello"); } }

输出结果为

hello

发短信

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

问题四

如果我们使用的是两个对象,一个调用发短信,一个调用打电话,那么整个顺序是怎么样的呢?

▼
text
复制代码
public class dome01 { public static void main(String[] args) throws InterruptedException { Phone phone1 = new Phone(); Phone phone2 = new Phone(); new Thread(() -> { try { phone1.sendMs(); } catch (InterruptedException e) { e.printStackTrace(); } }).start(); TimeUnit.SECONDS.sleep(1); new Thread(() -> { phone2.call(); }).start(); } } class Phone { public synchronized void sendMs() throws InterruptedException { TimeUnit.SECONDS.sleep(4); System.out.println("发短信"); } public synchronized void call() { System.out.println("打电话"); } public void hello() { System.out.println("hello"); } }

输出结果

打电话

发短信

原因:两个对象两把锁,不会出现等待的情况,发短信睡了4s,所以先执行打电话

问题五、六

如果我们把synchronized的方法加上static变成静态方法!那么顺序又是怎么样的呢?

(1)我们先来使用一个对象调用两个方法!

答案是:先发短信,后打电话

(2)如果我们使用两个对象调用两个方法!

答案是:还是先发短信,后打电话

原因是什么呢? 为什么加了static就始终前面一个对象先执行呢!为什么后面会等待呢?

原因是:对于static静态方法来说,对于整个类Class来说只有一份,对于不同的对象使用的是同一份方法,相当于这个方法是属于这个类的,如果静态static方法使用synchronized锁定,那么这个synchronized锁会锁住整个对象!不管多少个对象,对于静态的锁都只有一把锁,谁先拿到这个锁就先执行,其他的进程都需要等待!

问题七

如果我们使用一个静态同步方法、一个同步方法、一个对象调用顺序是什么?

▼
text
复制代码
public class dome01 { public static void main(String[] args) throws InterruptedException { Phone phone1 = new Phone(); // Phone phone2 = new Phone(); new Thread(() -> { try { phone1.sendMs(); } catch (InterruptedException e) { e.printStackTrace(); } }).start(); TimeUnit.SECONDS.sleep(1); new Thread(() -> { phone1.call(); }).start(); } } class Phone { public static synchronized void sendMs() throws InterruptedException { TimeUnit.SECONDS.sleep(4); System.out.println("发短信"); } public synchronized void call() { System.out.println("打电话"); } public void hello() { System.out.println("hello"); } }

输出结果

打电话

发短信

原因:因为一个锁的是Class类的模板,一个锁的是对象的调用者。所以不存在等待,直接运行。

问题八

如果我们使用一个静态同步方法、一个同步方法、两个对象调用顺序是什么?

▼
text
复制代码
public class dome01 { public static void main(String[] args) throws InterruptedException { Phone phone1 = new Phone(); Phone phone2 = new Phone(); new Thread(() -> { try { phone1.sendMs(); } catch (InterruptedException e) { e.printStackTrace(); } }).start(); TimeUnit.SECONDS.sleep(1); new Thread(() -> { phone2.call(); }).start(); } } class Phone { public static synchronized void sendMs() throws InterruptedException { TimeUnit.SECONDS.sleep(4); System.out.println("发短信"); } public synchronized void call() { System.out.println("打电话"); } public void hello() { System.out.println("hello"); } }

输出结果

打电话

发短信

原因:两把锁锁的不是同一个东西

小解

new 出来的 this 是具体的一个对象

**static **修饰的synchronized方法,锁对象指向的是class类

6. 集合不安全

1)List 不安全

▼
text
复制代码
//java.util.ConcurrentModificationException 并发修改异常! public class ListTest { public static void main(String[] args) { List<Object> arrayList = new ArrayList<>(); for(int i=1;i<=10;i++){ new Thread(()->{ arrayList.add(UUID.randomUUID().toString().substring(0,5)); System.out.println(arrayList); },String.valueOf(i)).start(); } } }

会导致 java.util.ConcurrentModificationException 并发修改异常!

ArrayList 在并发情况下是不安全的

解决方案:

▼
text
复制代码
public class ListTest { public static void main(String[] args) { /** * 解决方案 * 1. List<String> list = new Vector<>(); * 2. List<String> list = Collections.synchronizedList(new ArrayList<>()); * 3. List<String> list = new CopyOnWriteArrayList<>(); */ List<String> list = new CopyOnWriteArrayList<>(); for (int i = 1; i <=10; i++) { new Thread(() -> { list.add(UUID.randomUUID().toString().substring(0,5)); System.out.println(list); },String.valueOf(i)).start(); } } }

CopyOnWriteArrayList:写入时复制! COW 计算机程序设计领域的一种优化策略

核心思想是,如果有多个调用者(Callers)同时要求相同的资源(如内存或者是磁盘上的数据存储),他们会共同获取相同的指针指向相同的资源,直到某个调用者视图修改资源内容时,系统才会真正复制一份专用副本(private copy)给该调用者,而其他调用者所见到的最初的资源仍然保持不变。这过程对其他的调用者都是透明的(transparently)。此做法主要的优点是如果调用者没有修改资源,就不会有副本(private copy)被创建,因此多个调用者只是读取操作时可以共享同一份资源。

读的时候不需要加锁,如果读的时候有多个线程正在向CopyOnWriteArrayList添加数据,读还是会读到旧的数据,因为写的时候不会锁住旧的CopyOnWriteArrayList。

多个线程调用的时候,list,读取的时候,固定的,ArrayList在 写入(存在覆盖操作);CopyOnWriteArrayList在写入的时候通过赋值一个新的数组修改,避免覆盖,造成数据错乱的问题;

CopyOnWriteArrayList比Vector厉害在哪里?

  • Vector:
    • 使用 同步方法(synchronized 方法)来保证线程安全。
    • 锁的粒度是整个对象,即同一时间只能有一个线程访问 Vector 的方法(无论是读还是写)。
    • 这种粗粒度锁在高并发场景下会导致性能瓶颈。
  • CopyOnWriteArrayList:
    • 使用 写时复制(Copy-On-Write) 机制来保证线程安全。
    • 读操作完全无锁,支持高并发读。
    • 写操作会加锁,但锁的粒度更细,只针对写操作本身。
    • 这种设计在读多写少的场景下性能更高。

迭代器的行为

  • Vector:
    • 迭代器是 快速失败(fail-fast) 的,如果在迭代过程中集合被修改,会抛出 ConcurrentModificationException。
    • 这种设计适合单线程环境,但在多线程环境下可能会导致问题。
  • CopyOnWriteArrayList:
    • 迭代器是 弱一致性 的,迭代过程中不会抛出 ConcurrentModificationException。
    • 迭代器基于创建时的数组副本,因此不会受到写操作的影响。
    • 这种设计适合多线程环境。

2)set 不安全

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

解决方案还是两种:

  • 使用Collections工具类的synchronized包装的Set类
  • 使用CopyOnWriteArraySet 写入复制的JUC解决方案
▼
text
复制代码
public class SetTest { public static void main(String[] args) { /** * 1. Set<String> set = Collections.synchronizedSet(new HashSet<>()); * 2. Set<String> set = new CopyOnWriteArraySet<>(); */ // Set<String> set = new HashSet<>(); Set<String> set = new CopyOnWriteArraySet<>(); for (int i = 1; i <= 30; i++) { new Thread(() -> { set.add(UUID.randomUUID().toString().substring(0,5)); System.out.println(set); },String.valueOf(i)).start(); } } }

HashSet底层是什么?

hashSet底层就是一个HashMap;

▼
text
复制代码
public HashSet() { map = new HashMap<>(); } // add set 本质就是map key是无法重复的! public boolean add(E e){ return map.put(e, PRESENT) ==null; }

3)Map不安全

▼
text
复制代码
//map 是这样用的吗? 不是,工作中不使用这个 //默认等价什么? new HashMap<>(16,0.75); Map<String, String> map = new HashMap<>(); //加载因子、初始化容量

默认加载因子是0.75,默认的初始容量是16

image-20200811150700927

同样的HashMap基础类也存在并发修改异常!

▼
text
复制代码
public class MapTest { public static void main(String[] args) { //map 是这样用的吗? 不是,工作中不使用这个 //默认等价什么? new HashMap<>(16,0.75); /** * 解决方案 * 1. Map<String, String> map = Collections.synchronizedMap(new HashMap<>()); * Map<String, String> map = new ConcurrentHashMap<>(); */ Map<String, String> map = new ConcurrentHashMap<>(); //加载因子、初始化容量 for (int i = 1; i < 100; i++) { new Thread(()->{ map.put(Thread.currentThread().getName(), UUID.randomUUID().toString().substring(0,5)); System.out.println(map); },String.valueOf(i)).start(); } } }

并发类型的包下

ConcurrentHashMap是关于map的

ConcurrentLinkedDeque是关于队列的

CopyOnWriteArrayList 和CopyOnWriteArraySet 是正常书写形式

image-20250303211443575

ConcurrentHashMap底层原理:

JDK 7 中的实现

  • 分段锁(Segment):
    • ConcurrentHashMap 将数据分成多个段(Segment),每个段是一个独立的哈希表。
    • 每个段有自己的锁(ReentrantLock),不同段的操作可以并行执行。
    • 默认有 16 个段,因此最多支持 16 个线程并发写操作。
  • 数据结构:
    • 每个段内部是一个 HashEntry 数组,类似于 HashMap 的链表结构。
    • 通过分段锁,减少了锁的竞争,提高了并发性能。

JDK 8 中的实现

  • 数组 + 链表 + 红黑树:
    • ConcurrentHashMap 取消了分段锁的设计,改为使用 Node 数组 + 链表 + 红黑树的结构。
    • 当链表长度超过一定阈值(默认 8)时,链表会转换为红黑树,以提高查询性能。
  • CAS + synchronized:
    • 使用 CAS(Compare-And-Swap)操作来实现无锁化的线程安全。
    • 对于写操作,只在特定桶上加锁(synchronized),锁的粒度更细。

线程安全机制

JDK 7 中的线程安全

  • 分段锁:
    • 每个段(Segment)有自己的锁,不同段的操作可以并行执行。
    • 同一段内的操作需要竞争锁,但锁的粒度较小。

JDK 8 中的线程安全

  • CAS + synchronized:
    • 读操作:完全无锁,支持高并发读。
    • 写操作:
      • 使用 CAS 操作来保证原子性。
      • 对于哈希冲突的桶(链表或红黑树),使用 synchronized 加锁。
    • 锁的粒度是单个桶(Node),锁竞争更少。

7. Callable

1、可以有返回值; 2、可以抛出异常; 3、方法不同,run()/call()

futuretask实现了Runnable接口的子接口runnablefuture,

futuretask又有一个带callable类型参数的构造器,所以futuretask可以作为Runnable接口的实现类传到thread的构造器中啊

image-20250303220149257

▼
text
复制代码
public class callable { public static void main(String[] args) throws ExecutionException, InterruptedException { // 1. 创建 MyThread 实例,MyThread 实现了 Callable 接口 MyThread myThread = new MyThread(); // 2. 创建 FutureTask 实例,将 MyThread 实例作为参数传入 // FutureTask 是 Runnable 的实现类,同时包装了 Callable FutureTask<Integer> future = new FutureTask<>(myThread); // 3. 创建 Thread 实例,将 FutureTask 作为参数传入 // FutureTask 实现了 Runnable,因此可以作为 Thread 的参数 new Thread(future).start(); // 4. 调用 FutureTask 的 get() 方法获取 Callable 任务的返回值 // get() 方法会阻塞,直到任务执行完成并返回结果 Integer integer = future.get(); // 5. 打印任务返回的结果 System.out.println(integer); } } // 6. 定义 MyThread 类,实现 Callable 接口 class MyThread implements Callable<Integer> { // 7. 实现 call() 方法,定义任务逻辑 @Override public Integer call() throws Exception { // 返回一个固定的值 1024 return 1024; }

执行流程

  1. 创建 MyThread 实例(实现了 Callable)。
  2. 创建 FutureTask 实例,包装 MyThread。
  3. 创建 Thread 实例,传入 FutureTask。
  4. 启动线程,执行 FutureTask 的 run() 方法。
  5. FutureTask 内部调用 MyThread 的 call() 方法,获取返回值。
  6. 通过 FutureTask.get() 获取任务结果并打印。

代码改进建议

  1. 线程池:

    • 在实际开发中,建议使用线程池(如 ExecutorService)来管理线程,而不是直接创建 Thread 对象。

    • 例如:

      java

      复制

      ▼
      text
      复制代码
      ExecutorService executor = Executors.newFixedThreadPool(1); Future<Integer> future = executor.submit(new MyThread()); Integer result = future.get(); executor.shutdown();

8. 常用的辅助类

1)CountDownLatch

▼
text
复制代码
public class CountDownLatchDemo { public static void main(String[] args) throws InterruptedException { // 总数是6 CountDownLatch countDownLatch = new CountDownLatch(6); for (int i = 1; i <= 6; i++) { new Thread(() -> { System.out.println(Thread.currentThread().getName() + "==> Go Out"); countDownLatch.countDown(); // 每个线程都数量 -1 },String.valueOf(i)).start(); } countDownLatch.await(); // 等待计数器归零 然后向下执行 System.out.println("close door"); } }

主要方法:

  • countDown 减一操作;
  • await 等待计数器归零

await 等待计数器归零,就唤醒,再继续向下执行

2)CyclickBarrier

image-20200811202603352

▼
text
复制代码
public class CyclicBarrierDemo { public static void main(String[] args) { // 主线程 CyclicBarrier cyclicBarrier = new CyclicBarrier(7,() -> { System.out.println("召唤神龙"); }); for (int i = 1; i <= 7; i++) { // 子线程 int finalI = i; new Thread(() -> { System.out.println(Thread.currentThread().getName() + "收集了第" + finalI + "颗龙珠"); try { cyclicBarrier.await(); // 加法计数 等待 } catch (InterruptedException e) { e.printStackTrace(); } catch (BrokenBarrierException e) { e.printStackTrace(); } }).start(); } } }

[!IMPORTANT]

  • 内部类可以访问外部类的成员变量,无需 final 修饰。
  • 内部类访问外部方法的局部变量时,局部变量必须是 final 或等效于 final 的。
  • Lambda 表达式本质上是一个匿名内部类,因此它也遵循相同的规则:捕获的局部变量必须是 final 或等效于 final 的。

内部类(包括匿名内部类)可以访问外部类的变量,但有一些限制。具体来说,内部类访问外部类的局部变量时,这些局部变量必须是 final 或 等效于 final 的(即不可变)。

3)Semaphore(信号量)

▼
text
复制代码
public class SemaphoreDemo { public static void main(String[] args) { // 线程数量,停车位,限流 Semaphore semaphore = new Semaphore(3); for (int i = 0; i <= 6; i++) { new Thread(() -> { // acquire() 得到 try { semaphore.acquire(); System.out.println(Thread.currentThread().getName() + "抢到车位"); TimeUnit.SECONDS.sleep(2); System.out.println(Thread.currentThread().getName() + "离开车位"); }catch (Exception e) { e.printStackTrace(); }finally { semaphore.release(); // release() 释放 } }).start(); } } }
▼
text
复制代码
Thread-1抢到车位 Thread-0抢到车位 Thread-2抢到车位 Thread-0离开车位 Thread-2离开车位 Thread-1离开车位 Thread-5抢到车位 Thread-3抢到车位 Thread-4抢到车位 Thread-5离开车位 Thread-3离开车位 Thread-6抢到车位 Thread-4离开车位 Thread-6离开车位 Process finished with exit code 0

原理:

semaphore.acquire()获得资源,如果资源已经使用完了,就等待资源释放后再进行使用!

semaphore.release()释放,会将当前的信号量释放+1,然后唤醒等待的线程!

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

9. 读写锁

读 - 读 不互斥

读 - 写 不互斥

写 - 写 互斥

如果我们不加锁的情况,多线程的读写会造成数据不可靠的问题。

我们也可以采用synchronized这种重量锁和轻量锁 lock去保证数据的可靠。

但是这次我们采用更细粒度的锁:ReadWriteLock 读写锁来保证

▼
text
复制代码
public class ReadWriteLockDemo { public static void main(String[] args) { MyCache2 myCache = new MyCache2(); int num = 6; for (int i = 1; i <= num; i++) { int finalI = i; new Thread(() -> { myCache.write(String.valueOf(finalI), String.valueOf(finalI)); },String.valueOf(i)).start(); } for (int i = 1; i <= num; i++) { int finalI = i; new Thread(() -> { myCache.read(String.valueOf(finalI)); },String.valueOf(i)).start(); } } } class MyCache2 { private volatile Map<String, String> map = new HashMap<>(); private ReadWriteLock lock = new ReentrantReadWriteLock(); public void write(String key, String value) { lock.writeLock().lock(); // 写锁 try { System.out.println(Thread.currentThread().getName() + "线程开始写入"); map.put(key, value); System.out.println(Thread.currentThread().getName() + "线程写入ok"); }finally { lock.writeLock().unlock(); // 释放写锁 } } public void read(String key) { lock.readLock().lock(); // 读锁 try { System.out.println(Thread.currentThread().getName() + "线程开始读取"); map.get(key); System.out.println(Thread.currentThread().getName() + "线程写读取ok"); }finally { lock.readLock().unlock(); // 释放读锁 } } }

10. 阻塞队列

image-20200812093115670

1)BlockQueue

是Collection的一个子类

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

生产者-消费者模型 多线程并发处理、线程池、任务限流、线程间数据交换(SynchronousQueue)。

image-20200812093254651

BlockingQueue 有四组api

方式抛出异常不会抛出异常,有返回值阻塞,等待超时等待
添加addofferputoffer(timenum.timeUnit)
移出removepolltakepoll(timenum,timeUnit)
判断队首元素elementpeek--
▼
text
复制代码
/** * 抛出异常 */ public static void test1(){ //需要初始化队列的大小 ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(3); System.out.println(blockingQueue.add("a")); System.out.println(blockingQueue.add("b")); System.out.println(blockingQueue.add("c")); //抛出异常:java.lang.IllegalStateException: Queue full // System.out.println(blockingQueue.add("d")); System.out.println(blockingQueue.remove()); System.out.println(blockingQueue.remove()); System.out.println(blockingQueue.remove()); //如果多移除一个 //这也会造成 java.util.NoSuchElementException 抛出异常 System.out.println(blockingQueue.remove()); } ======================================================================================= /** * 不抛出异常,有返回值 */ public static void test2(){ ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(3); System.out.println(blockingQueue.offer("a")); System.out.println(blockingQueue.offer("b")); System.out.println(blockingQueue.offer("c")); //添加 一个不能添加的元素 使用offer只会返回false 不会抛出异常 System.out.println(blockingQueue.offer("d")); System.out.println(blockingQueue.poll()); System.out.println(blockingQueue.poll()); System.out.println(blockingQueue.poll()); //弹出 如果没有元素 只会返回null 不会抛出异常 System.out.println(blockingQueue.poll()); } ======================================================================================= /** * 等待 一直阻塞 */ public static void test3() throws InterruptedException { ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(3); //一直阻塞 不会返回 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()); } ======================================================================================= /** * 等待 超时阻塞 * 这种情况也会等待队列有位置 或者有产品 但是会超时结束 */ public static void test4() throws InterruptedException { ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(3); blockingQueue.offer("a"); blockingQueue.offer("b"); blockingQueue.offer("c"); System.out.println("开始等待"); blockingQueue.offer("d",2, TimeUnit.SECONDS); //超时时间2s 等待如果超过2s就结束等待 System.out.println("结束等待"); System.out.println("===========取值=================="); System.out.println(blockingQueue.poll()); System.out.println(blockingQueue.poll()); System.out.println(blockingQueue.poll()); System.out.println("开始等待"); blockingQueue.poll(2,TimeUnit.SECONDS); //超过两秒 我们就不要等待了 System.out.println("结束等待"); }

2)同步队列

同步队列 没有容量,也可以视为容量为1的队列;

进去一个元素,必须等待取出来之后,才能再往里面放入一个元素;

put方法 和 take方法;

SynchronousQueue和 其他的BlockingQueue 不一样 它不存储元素;

put了一个元素,就必须从里面先take出来,否则不能再put进去值!

并且SynchronousQueue 的take是使用了lock锁保证线程安全的。

▼
text
复制代码
public class SynchronousQueue { public static void main(String[] args) { BlockingQueue<String> synchronousQueue = new java.util.concurrent.SynchronousQueue<>(); // 网queue中添加元素 new Thread(() -> { try { System.out.println(Thread.currentThread().getName() + "put 01"); synchronousQueue.put("1"); System.out.println(Thread.currentThread().getName() + "put 02"); synchronousQueue.put("2"); System.out.println(Thread.currentThread().getName() + "put 03"); synchronousQueue.put("3"); } catch (InterruptedException e) { e.printStackTrace(); } }).start(); // 取出元素 new Thread(()-> { try { System.out.println(Thread.currentThread().getName() + "take" + synchronousQueue.take()); System.out.println(Thread.currentThread().getName() + "take" + synchronousQueue.take()); System.out.println(Thread.currentThread().getName() + "take" + synchronousQueue.take()); }catch (InterruptedException e) { e.printStackTrace(); } }).start(); } }

11. 线程池

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

池化技术

程序的运行,本质:占用系统的资源!我们需要去优化资源的使用 ===> 池化技术

线程池、JDBC的连接池、内存池、对象池 等等。。。。

资源的创建、销毁十分消耗资源

池化技术:事先准备好一些资源,如果有人要用,就来我这里拿,用完之后还给我,以此来提高效率。

1)线程池的好处:

1、降低资源的消耗;

2、提高响应的速度;

3、方便管理;

==线程复用、可以控制最大并发数、管理线程;==

2)线程池:三大方法

  • ExecutorService threadPool = Executors.newSingleThreadExecutor();//单个线程
  • ExecutorService threadPool2 = Executors.newFixedThreadPool(5); //创建一个固定的线程池的大小
  • ExecutorService threadPool3 = Executors.newCachedThreadPool(); //可伸缩的
▼
text
复制代码
//工具类 Executors 三大方法; public class Demo01 { public static void main(String[] args) { ExecutorService threadPool = Executors.newSingleThreadExecutor();//单个线程 ExecutorService threadPool2 = Executors.newFixedThreadPool(5); //创建一个固定的线程池的大小 ExecutorService threadPool3 = Executors.newCachedThreadPool(); //可伸缩的 //线程池用完必须要关闭线程池 try { for (int i = 1; i <=100 ; i++) { //通过线程池创建线程 threadPool.execute(()->{ System.out.println(Thread.currentThread().getName()+ " ok"); }); } } catch (Exception e) { e.printStackTrace(); } finally { threadPool.shutdown(); } } }

3)七大参数

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

image-20250304103041010

阿里巴巴的Java操作手册中明确说明:对于Integer.MAX_VALUE初始值较大,所以一般情况我们要使用底层的ThreadPoolExecutor来创建线程池。

▼
text
复制代码
public static void main(String[] args) { ThreadPoolExecutor threadPool = new ThreadPoolExecutor( 2, 5, 3, TimeUnit.SECONDS, new ArrayBlockingQueue<>(3), Executors.defaultThreadFactory(), new ThreadPoolExecutor.CallerRunsPolicy() ); try { for (int i = 0; i <11; i++) { threadPool.execute(() -> { System.out.println("当前线程为" + Thread.currentThread().getName()); }); } } catch (Exception e) { throw new RuntimeException(e); } finally { threadPool.shutdown(); }

4)拒绝策略

  1. new ThreadPoolExecutor.AbortPolicy(): //该拒绝策略为:银行满了,还有人进来,不处理这个人的,并抛出异常

超出最大承载,就会抛出异常:队列容量大小+maxPoolSize

  1. new ThreadPoolExecutor.CallerRunsPolicy(): //该拒绝策略为:哪来的去哪里 main线程进行处理

  2. **new ThreadPoolExecutor.DiscardPolicy(): **//该拒绝策略为:队列满了,丢掉异常,不会抛出异常。

  3. new ThreadPoolExecutor.DiscardOldestPolicy(): //该拒绝策略为:队列满了,尝试去和最早的进程竞争,不会抛出异常

5)如何设置线程池的最大线程数

1、CPU密集型:电脑的核数是几核就选择几;选择maximunPoolSize的大小

▼
text
复制代码
// 获取cpu 的核数 int max = Runtime.getRuntime().availableProcessors(); ExecutorService service =new ThreadPoolExecutor( 2, max, 3, TimeUnit.SECONDS, new LinkedBlockingDeque<>(3), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy() );

2、I/O密集型:

在程序中有15个大型任务,io十分占用资源;I/O密集型就是判断我们程序中十分耗I/O的线程数量,大约是最大I/O数的一倍到两倍之间。

下面是黑马的

image-20250108095436910

12. 四大函数式接口

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

函数式接口:只有一个方法的接口

image-20200812143426348

image-20200812143713392

1)Function 函数型接口

image-20200812144105334

▼
text
复制代码
public class FunctionDemo { public static void main(String[] args) { Function<String, String> function = (str) -> {return str;}; System.out.println(function.apply("aaaaaaaaaa")); } }

2)Predicate 断定型接口

image-20200812144545558

▼
text
复制代码
public class PredicateDemo { public static void main(String[] args) { Predicate<String> predicate = (str) -> {return str.isEmpty();}; // false System.out.println(predicate.test("aaa")); // true System.out.println(predicate.test("")); } }

3)Suppier 供给型接口

image-20200812144653640
▼
text
复制代码
/** * 供给型接口,只返回,不输入 */ public class Demo4 { public static void main(String[] args) { Supplier<String> supplier = ()->{return "1024";}; System.out.println(supplier.get()); } }

4)Consummer 消费型接口

image-20200812144803229

▼
text
复制代码
/** * 消费型接口 没有返回值!只有输入! */ public class Demo3 { public static void main(String[] args) { Consumer<String> consumer = (str)->{ System.out.println(str); }; consumer.accept("abc"); } }

13. Stream 流式计算

▼
text
复制代码
/** * Description: * 题目要求: 用一行代码实现 * 1. Id 必须是偶数 * 2.年龄必须大于23 * 3. 用户名转为大写 * 4. 用户名倒序 * 5. 只能输出一个用户 * * @author jiaoqianjin * Date: 2020/8/12 14:55 **/ public class StreamDemo { public static void main(String[] args) { User u1 = new User(1, "a", 23); User u2 = new User(2, "b", 23); User u3 = new User(3, "c", 23); User u4 = new User(6, "d", 24); User u5 = new User(4, "e", 25); List<User> list = Arrays.asList(u1, u2, u3, u4, u5); // lambda、链式编程、函数式接口、流式计算 list.stream() .filter(user -> {return user.getId()%2 == 0;}) .filter(user -> {return user.getAge() > 23;}) .map(user -> {return user.getName().toUpperCase();}) .sorted((user1, user2) -> {return user2.compareTo(user1);}) .limit(1) .forEach(System.out::println); } }

14. ForkJoin

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

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

image-20200812163638389

1)ForkJoin 特点: 工作窃取!

实现原理是:双端队列!从上面和下面都可以去拿到任务进行执行!

image-20200812163701588

2)如何使用ForkJoin?

  • 1、通过ForkJoinPool来执行

  • 2、计算任务 execute(ForkJoinTask<?> task)

  • 3、计算类要去继承ForkJoinTask;

  • ForkJoinPool:用于执行 ForkJoinTask 的线程池。 ForkJoinPool 默认使用所有可用的 CPU 核心。

  • RecursiveTask<V>:用于有返回值的任务。

  • RecursiveAction:用于无返回值的任务。

    ForkJoin 的计算类

▼
text
复制代码
package com.marchsoft.forkjoin; import java.util.concurrent.RecursiveTask; /** * Description: * * @author jiaoqianjin * Date: 2020/8/13 8:33 **/ public class ForkJoinDemo extends RecursiveTask<Long> { private long star; private long end; /** 临界值 */ private long temp = 1000000L; public ForkJoinDemo(long star, long end) { this.star = star; this.end = end; } /** * 计算方法 * @return */ @Override protected Long compute() { if ((end - star) < temp) { Long sum = 0L; for (Long i = star; i < end; i++) { sum += i; } return sum; }else { // 使用ForkJoin 分而治之 计算 //1 . 计算平均值 long middle = (star + end) / 2; ForkJoinDemo forkJoinDemo1 = new ForkJoinDemo(star, middle); // 拆分任务,把线程压入线程队列 forkJoinDemo1.fork(); //使用 fork() 提交子任务。 ForkJoinDemo forkJoinDemo2 = new ForkJoinDemo(middle, end); forkJoinDemo2.fork(); long taskSum = forkJoinDemo1.join() + forkJoinDemo2.join(); //使用 join() 获取子任务的结果。 return taskSum; } } }

测试类

▼
text
复制代码
package com.marchsoft.forkjoin; import java.util.concurrent.ExecutionException; import java.util.concurrent.ForkJoinPool; import java.util.concurrent.ForkJoinTask; import java.util.stream.LongStream; /** * Description: * * @author jiaoqianjin * Date: 2020/8/13 8:43 **/ public class ForkJoinTest { private static final long SUM = 20_0000_0000; public static void main(String[] args) throws ExecutionException, InterruptedException { test1(); test2(); test3(); } /** * 使用普通方法 */ public static void test1() { long star = System.currentTimeMillis(); long sum = 0L; for (long i = 1; i < SUM ; i++) { sum += i; } long end = System.currentTimeMillis(); System.out.println(sum); System.out.println("时间:" + (end - star)); System.out.println("----------------------"); } /** * 使用ForkJoin 方法 */ public static void test2() throws ExecutionException, InterruptedException { long star = System.currentTimeMillis(); ForkJoinPool forkJoinPool = new ForkJoinPool(); ForkJoinTask<Long> task = new ForkJoinDemo(0L, SUM); ForkJoinTask<Long> submit = forkJoinPool.submit(task); Long along = submit.get(); System.out.println(along); long end = System.currentTimeMillis(); System.out.println("时间:" + (end - star)); System.out.println("-----------"); } /** * 使用 Stream 流计算 */ public static void test3() { long star = System.currentTimeMillis(); long sum = LongStream.range(0L, 20_0000_0000L).parallel().reduce(0, Long::sum); System.out.println(sum); long end = System.currentTimeMillis(); System.out.println("时间:" + (end - star)); System.out.println("-----------"); } }

.parallel().reduce(0, Long::sum)使用一个并行流去计算整个计算,提高效率。

  1. LongStream.range(0L, 20_0000_0000L):
    • 创建一个从 0 到 20_0000_0000L(不包括 20_0000_0000L)的 LongStream。
    • 这个范围包含 20 亿个整数。
  2. .parallel():
    • 将流转换为并行流。
    • 并行流会将任务拆分为多个子任务,利用多核 CPU 并行执行。
  3. .reduce(0, Long::sum):
    • 对流中的元素进行归约操作。
    • 0 是初始值。
    • Long::sum 是一个方法引用,表示对两个 long 值求和。
    • 最终将所有元素的值累加,返回总和。

15. 异步回调

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

Future 是Java 5引入的一个接口,用于表示异步计算的结果。它提供了一些基本的方法来检查计算是否完成、获取计算结果以及取消任务。

▼
text
复制代码
ExecutorService executor = Executors.newSingleThreadExecutor(); Future<Integer> future = executor.submit(() -> { Thread.sleep(1000); return 42; }); System.out.println("Task is submitted"); Integer result = future.get(); // 阻塞直到任务完成 System.out.println("Result: " + result); executor.shutdown();

缺点:

  • Future 缺乏对任务完成后的回调支持,获取结果时必须阻塞等待。
  • 不支持任务之间的链式调用或组合。

CompletableFuture 是Java 8引入的一个类,实现了 Future 接口,并提供了更强大的功能。它支持任务完成后的回调、任务之间的链式调用、任务组合以及异常处理。

主要特点:

  • 异步回调:可以通过 thenApply, thenAccept, thenRun 等方法在任务完成后执行回调。
  • 任务组合:可以通过 thenCombine, thenCompose, allOf, anyOf 等方法组合多个任务。
  • 异常处理:可以通过 exceptionally, handle 等方法处理任务执行过程中的异常。
  • 手动完成:可以通过 complete, completeExceptionally 方法手动完成任务。

总结

  • Future:适用于简单的异步任务,功能较为基础,缺乏对任务完成后的回调支持。
  • CompletableFuture:功能强大,适用于复杂的异步编程场景,支持任务组合、链式调用和异常处理。
(1)没有返回值的runAsync异步回调
▼
text
复制代码
public static void main(String[] args) throws ExecutionException, InterruptedException { // 发起 一个 请求 System.out.println(System.currentTimeMillis()); System.out.println("---------------------"); CompletableFuture<Void> future = CompletableFuture.runAsync(()->{ //发起一个异步任务 try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"....."); }); System.out.println(System.currentTimeMillis()); System.out.println("------------------------------"); //输出执行结果 System.out.println(future.get()); //获取执行结果 }
(2)有返回值的异步回调supplyAsync
▼
text
复制代码
//有返回值的异步回调 CompletableFuture<Integer> completableFuture=CompletableFuture.supplyAsync(()->{ System.out.println(Thread.currentThread().getName()); try { TimeUnit.SECONDS.sleep(2); int i=1/0; } catch (InterruptedException e) { e.printStackTrace(); } return 1024; }); System.out.println(completableFuture.whenComplete((t, u) -> { //success 回调 System.out.println("t=>" + t); //正常的返回结果 System.out.println("u=>" + u); //抛出异常的 错误信息 }).exceptionally((e) -> { //error回调 System.out.println(e.getMessage()); return 404; }).get());

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

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

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

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

16. JMM

1)对Volatile 的理解

Volatile 是 Java 虚拟机提供 轻量级的同步机制,用于保证变量的可见性和有序性。

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

如何实现可见性

volatile 的可见性是通过 内存屏障(Memory Barrier) 实现的。内存屏障是一种 CPU 指令,用于控制指令的执行顺序和内存的可见性。

(1)写操作的内存屏障

当一个线程写一个 volatile 变量时:

  • JVM 会在写操作之后插入一个 写屏障(Store Barrier)。
  • 写屏障确保:
    • 将该线程工作内存中的变量值刷新到主内存。
    • 禁止将写操作之前的指令重排序到写操作之后。

(2)读操作的内存屏障

当一个线程读一个 volatile 变量时:

  • JVM 会在读操作之前插入一个 读屏障(Load Barrier)。
  • 读屏障确保:
    • 从主内存中读取最新的变量值,而不是从线程的工作内存中读取。
    • 禁止将读操作之后的指令重排序到读操作之前。

多处理器总线嗅探:

为了提高处理速度,处理器不直接和内存进行通信,而是先将系统内存的数据读到内部缓存后再进行操作,但操作不知道何时会写到内存。 总线嗅探(Bus Snooping) 是多处理器系统中用于维护缓存一致性(Cache Coherence)的一种机制。在多处理器系统中,每个处理器都有自己的缓存(Cache),这可能导致多个缓存中存储同一主内存地址的不同副本。总线嗅探通过监听总线上的内存操作,确保所有处理器的缓存数据保持一致。

总线嗅探的实现方式

总线嗅探通常与缓存一致性协议(Cache Coherence Protocol)结合使用。常见的缓存一致性协议包括:

(1)写失效(Write Invalidate)

  • 当一个处理器修改了缓存中的数据时,它会广播一个“失效”消息到总线上。
  • 其他处理器监听到这个消息后,会将对应缓存行的数据标记为无效。
  • 后续读取该数据时,处理器必须从主内存或其他缓存中获取最新值。

(2)写更新(Write Update)

  • 当一个处理器修改了缓存中的数据时,它会广播新数据到总线上。
  • 其他处理器监听到这个消息后,会更新自己缓存中的对应数据。

总线嗅探与 Java 内存模型(JMM)

Java 内存模型(JMM)定义了多线程环境下的内存可见性和有序性规则。总线嗅探机制与 JMM 的可见性规则密切相关:

  • volatile 关键字:volatile 变量的写操作会触发总线嗅探机制,确保修改后的值立即刷新到主内存,并且其他处理器的缓存失效。
  • synchronized 关键字:锁的释放和获取也会触发总线嗅探机制,确保线程之间的可见性。

2)什么是JMM?

Java内存模型(Java Memory Model,JMM)是Java虚拟机(JVM)规范中定义的一种抽象模型,用于描述多线程环境下,线程如何与内存交互以及如何保证线程之间的可见性、有序性和原子性。

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

1、线程解锁前,必须把共享变量立刻刷回主存;

2、线程加锁前,必须读取主存中的最新值到工作内存中;

3、加锁和解锁是同一把锁;

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

8种操作:

  • Read(读取):作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用;

  • load(载入):作用于工作内存的变量,它把read操作从主存中变量放入工作内存中;

  • Use(使用):作用于工作内存中的变量,它把工作内存中的变量传输给执行引擎,每当虚拟机遇到一个需要使用到变量的值,就会使用到这个指令;

  • assign(赋值):作用于工作内存中的变量,它把一个从执行引擎中接受到的值放入工作内存的变量副本中;

  • store(存储):作用于主内存中的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用;

  • write(写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中;

  • lock(锁定):作用于主内存的变量,把一个变量标识为线程独占状态;

  • unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定;

image-20200812215247240

image-20200812215606080

JMM对这8种操作给了相应的规定:

  • 不允许read和load、store和write操作之一单独出现。即使用了read必须load,使用了store必须write

  • 不允许线程丢弃他最近的assign操作,即工作变量的数据改变了之后,必须告知主存

  • 不允许一个线程将没有assign的数据从工作内存同步回主内存

  • 一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是对变量实施use、store操作之前,必须经过assign和load操作

  • 一个变量同一时间只有一个线程能对其进行lock。多次lock后,必须执行相同次数的unlock才能解锁

  • 如果对一个变量进行lock操作,会清空所有工作内存中此变量的值,在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值

  • 如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量

  • 对一个变量进行unlock操作之前,必须把此变量同步回主内存

img

遇到问题:程序不知道主存中的值已经被修改过了!;

17. volatile

1)保证可见性

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

2)不保证原子性

原子性:不可分割;

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

▼
java
复制代码
/** * 不保证原子性 * number <=2w * */ public class VDemo02 { private static volatile int number = 0; public static void add(){ number++; //++ 不是一个原子性操作,是两个~3个操作 // } public static void main(String[] args) { //理论上number === 20000 for (int i = 1; i <= 20; i++) { new Thread(()->{ for (int j = 1; j <= 1000 ; j++) { add(); } }).start(); } while (Thread.activeCount()>2){ //main gc Thread.yield(); } System.out.println(Thread.currentThread().getName()+",num="+number); } }

如果不加lock和synchronized ,怎么样保证原子性?

image-20200812215844788

使用原子类

image-20200812215909271

▼
java
复制代码
public class VDemo02 { private static volatile AtomicInteger number = new AtomicInteger(); public static void add(){ // number++; number.incrementAndGet(); //底层是CAS保证的原子性 } public static void main(String[] args) { //理论上number === 20000 for (int i = 1; i <= 20; i++) { new Thread(()->{ for (int j = 1; j <= 1000 ; j++) { add(); } }).start(); } while (Thread.activeCount()>2){ //main gc Thread.yield(); } System.out.println(Thread.currentThread().getName()+",num="+number); } }

这些类的底层都直接和操作系统挂钩!是在内存中修改值。

Unsafe类是一个很特殊的存在;

3)禁止指令重排

什么是指令重排?

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

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

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

▼
java
复制代码
int x=1; //1 int y=2; //2 x=x+5; //3 y=x*x; //4 //我们期望的执行顺序是 1_2_3_4 可能执行的顺序会变成2134 1324 //可不可能是 4123? 不可能的 1234567

volatile可以避免指令重排:

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

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

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

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

image-20200812220019582

4)总结

  • volatile可以保证可见性;
  • 不能保证原子性
  • 由于内存屏障,可以保证避免指令重排的现象产生

面试官:那么你知道在哪里用这个内存屏障用得最多呢?单例模式

这里双重检测加锁是保证了操作原子性,只有一个线程能创建一个实例,其他线程无法创建第二个

volatile关键字是为了防止因为指令重排导致的多线程问题,有可能线程A创建一个实例,

虚拟机只执行了分配空间,对象地址引用这两步,这是线程B过来发现对象已经被创建了,但是获取到的对象是还没有被初始化的

18. 玩转单例模式

单例模式(Singleton Pattern)是一种创建型设计模式,确保一个类只有一个实例,并提供全局访问点。它常用于管理共享资源,如数据库连接、线程池等。

关键点

  1. 私有构造函数:防止外部直接实例化。
  2. 静态实例变量:保存类的唯一实例。
  3. 静态获取方法:提供全局访问点。

1)饿汉式

▼
java
复制代码
/** * 饿汉式单例 */ public class Hungry { /** * 可能会浪费空间 */ private byte[] data1=new byte[1024*1024]; private byte[] data2=new byte[1024*1024]; private byte[] data3=new byte[1024*1024]; private byte[] data4=new byte[1024*1024]; private Hungry(){ } private final static Hungry hungry = new Hungry(); public static Hungry getInstance(){ return hungry; } }

2)DCL懒汉式

▼
java
复制代码
//懒汉式单例模式 public class LazyMan { private static boolean key = false; private LazyMan(){ synchronized (LazyMan.class){ if (key==false){ //防止多个对象都是通过构造器创建的,LazyMan lazyMan还是等于null ,所以设置个key变量,来保证创建一个对象后 key=true,来进行判断 key=true; } else{ throw new RuntimeException("不要试图使用反射破坏异常"); } } System.out.println(Thread.currentThread().getName()+" ok"); } private volatile static LazyMan lazyMan; //双重检测锁模式 简称DCL懒汉式 public static LazyMan getInstance(){ //需要加锁 如果lazyMan!=null 则后续线程无需获取锁浪费时间了 if(lazyMan==null){ synchronized (LazyMan.class){ if(lazyMan==null){ lazyMan=new LazyMan(); /** * 1、分配内存空间 * 2、执行构造方法,初始化对象 * 3、把这个对象指向这个空间 * * 就有可能出现指令重排问题 * 比如执行的顺序是1 3 2 等 执行1和 3后,别的线程认为lazyMan!=null,但其实lazyMan还没有初始化,为 null,只是分配了空间。 * 我们就可以添加volatile保证指令重排问题 */ } } } return lazyMan; } public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchFieldException { //Java中有反射 // LazyMan instance = LazyMan.getInstance(); Field key = LazyMan.class.getDeclaredField("key"); key.setAccessible(true); Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null); declaredConstructor.setAccessible(true); //无视了私有的构造器 LazyMan lazyMan1 = declaredConstructor.newInstance(); //通过构造器创建对象 key.set(lazyMan1,false); LazyMan instance = declaredConstructor.newInstance(); System.out.println(instance); System.out.println(lazyMan1); System.out.println(instance == lazyMan1); } }

3)静态内部类

▼
java
复制代码
public class Singleton { // 私有构造函数,防止外部实例化 private Singleton() {} // 静态内部类 private static class SingletonHolder { // 在静态内部类中创建外部类的实例 private static final Singleton INSTANCE = new Singleton(); } // 提供全局访问点 public static Singleton getInstance() { return SingletonHolder.INSTANCE; } }

静态内部类的特性

静态内部类是外部类的静态成员,只有在被引用时才会加载。这意味着:

  • 延迟加载:静态内部类不会在外部类加载时立即加载,而是在调用 getInstance() 时才会加载。
  • 线程安全:类的加载过程是线程安全的,由 JVM 保证。

JVM 在加载类时,会确保类的初始化是线程安全的。静态内部类的静态变量 INSTANCE 在类加载时被初始化,且只会初始化一次。

如何保证单例

  • 延迟加载:SingletonHolder 类只有在调用 getInstance() 时才会被加载,此时才会创建 INSTANCE。
  • 线程安全:JVM 保证类的加载过程是线程安全的,因此 INSTANCE 的初始化也是线程安全的。
  • 唯一性:静态变量 INSTANCE 是 final 的,确保它只会被赋值一次,且不可修改。

静态内部类通过 JVM 的类加载机制和静态变量的特性,确保了单例的唯一性和线程安全性。这是实现单例模式的推荐方式之一,既简单又高效。

4)枚举

▼
java
复制代码
//enum 是什么? enum本身就是一个Class 类 public enum EnumSingle { INSTANCE; public EnumSingle getInstance(){ return INSTANCE; } } class Test{ public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { EnumSingle instance1 = EnumSingle.INSTANCE; Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(String.class,int.class); declaredConstructor.setAccessible(true); //java.lang.NoSuchMethodException: com.ogj.single.EnumSingle.<init>() EnumSingle instance2 = declaredConstructor.newInstance(); System.out.println(instance1); System.out.println(instance2); } }

使用枚举,线程安全,防止反射攻击

image-20200812220204965

枚举类型的最终反编译源码:

public final class EnumSingle extends Enum {

▼
java
复制代码
public static EnumSingle[] values() { return (EnumSingle[])$VALUES.clone(); } public static EnumSingle valueOf(String name) { return (EnumSingle)Enum.valueOf(com/ogj/single/EnumSingle, name); } private EnumSingle(String s, int i) { super(s, i); } public EnumSingle getInstance() { return INSTANCE; } public static final EnumSingle INSTANCE; private static final EnumSingle $VALUES[]; static { INSTANCE = new EnumSingle("INSTANCE", 0); $VALUES = (new EnumSingle[] { INSTANCE }); } }

19. 深入理解CAS

1)什么是CAS?

CAS(Compare-And-Swap,比较并交换)是 Java 中实现并发操作的一种无锁机制。它是一种原子操作,广泛用于多线程编程中,用于实现线程安全的无锁算法。Java 中的 java.util.concurrent 包中的许多类(如 AtomicInteger、AtomicLong 等)都是基于 CAS 实现的。

CAS 的核心思想

CAS 操作包含三个操作数:

  • 内存地址(V):需要更新的变量。
  • 期望值(E):当前线程认为变量应该具有的值。
  • 新值(N):要更新的值。

CAS 的操作逻辑是:

  1. 比较内存地址 V 中的值是否等于期望值 E。
  2. 如果相等,则将内存地址 V 的值更新为新值 N。
  3. 如果不相等,则说明其他线程已经修改了值,操作失败。

CAS 是一个原子操作,由底层硬件(如 CPU)提供支持。

在 Java 中,CAS 操作是通过 sun.misc.Unsafe 类提供的本地方法实现的,例如:

▼
java
复制代码
public final native boolean compareAndSwapInt(Object o, long offset, int expected, int x);

CAS 的底层实现原理

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

CAS 的底层依赖于 CPU 的原子指令。以下是一个简化的流程:

  1. 读取内存值:从内存中读取当前值。
  2. 比较值:将读取的值与期望值进行比较。
  3. 更新值:如果值相等,则更新为新值;否则,操作失败。
  4. 重试:如果操作失败,则重新尝试(自旋)。

CAS 的缺点

  • ABA 问题:
    • 问题描述:如果一个变量的值从 A 变为 B,又变回 A,CAS 操作会认为值没有变化,但实际上已经被修改过。
    • 解决方案:使用版本号或时间戳(如 AtomicStampedReference)。
  • 自旋开销:
    • 在高竞争情况下,CAS 操作可能会失败多次,导致 CPU 空转。
  • 只能保证一个变量的原子性:
    • CAS 只能操作一个变量,无法保证多个变量的原子性。

Unsafe 类

image-20200812220347822

image-20200812220411463

2)什么是ABA问题?

image-20200812220441615

线程1:期望值是1,要变成2;

线程2:两个操作:

  • 1、期望值是1,变成3
  • 2、期望是3,变成1

所以对于线程1来说,A的值还是1,所以就出现了问题,骗过了线程1;

▼
java
复制代码
public class casDemo { //CAS : compareAndSet 比较并交换 public static void main(String[] args) { AtomicInteger atomicInteger = new AtomicInteger(2020); System.out.println(atomicInteger.compareAndSet(2020, 2021)); System.out.println(atomicInteger.get()); //boolean compareAndSet(int expect, int update) //期望值、更新值 //如果实际值 和 我的期望值相同,那么就更新 //如果实际值 和 我的期望值不同,那么就不更新 System.out.println(atomicInteger.compareAndSet(2021, 2020)); System.out.println(atomicInteger.get()); //因为期望值是2020 实际值却变成了2021 所以会修改失败 //CAS 是CPU的并发原语 // atomicInteger.getAndIncrement(); //++操作 System.out.println(atomicInteger.compareAndSet(2020, 2021)); System.out.println(atomicInteger.get()); } }

20. 原子引用

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

Integer 使用了对象缓存机制,默认范围是-128~127,推荐使用静态工厂方法valueOf获取对象实例,而不是new,因为valueOf使用缓存,而new一定会创建新的对象分配新的内存空间。

image-20200812220608094

AtomicStampedReference带版本号的原子操作

▼
java
复制代码
package com.marchsoft.lockdemo; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicStampedReference; /** * Description: * * @author jiaoqianjin * Date: 2020/8/12 22:07 **/ public class CASDemo { /**AtomicStampedReference 注意,如果泛型是一个包装类,注意对象的引用问题 * 正常在业务操作,这里面比较的都是一个个对象 */ static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(1, 1); // CAS compareAndSet : 比较并交换! public static void main(String[] args) { new Thread(() -> { int stamp = atomicStampedReference.getStamp(); // 获得版本号 System.out.println("a1=>" + stamp); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } // 修改操作时,版本号更新 + 1 atomicStampedReference.compareAndSet(1, 2, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1); System.out.println("a2=>" + atomicStampedReference.getStamp()); // 重新把值改回去, 版本号更新 + 1 System.out.println(atomicStampedReference.compareAndSet(2, 1, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1)); System.out.println("a3=>" + atomicStampedReference.getStamp()); }, "a").start(); // 乐观锁的原理相同! new Thread(() -> { int stamp = atomicStampedReference.getStamp(); // 获得版本号 System.out.println("b1=>" + stamp); try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(atomicStampedReference.compareAndSet(1, 3, stamp, stamp + 1)); System.out.println("b2=>" + atomicStampedReference.getStamp()); }, "b").start(); } } 输出: a1=>1 b1=>1 a2=>2 true a3=>3 false b2=>3

21. 各种锁的理解

1)公平锁,非公平锁

  1. 公平锁:非常公平,不能插队,必须先来后到
  2. 非公平锁:非常不公平,允许插队,可以改变顺序

2)可重入锁

可重入锁是指一个线程在持有锁的情况下,可以再次获取该锁而不会被阻塞。这种特性避免了线程因重复获取锁而导致的死锁问题。

image-20200812213957137

ReentrantLock 的核心特性

(1)可重入性

  • 同一个线程可以多次获取同一把锁。
  • 每次获取锁后,必须释放相同次数的锁。

ReentrantLock 与 synchronized 的对比

特性ReentrantLocksynchronized
可重入性支持支持
公平性支持公平锁和非公平锁非公平锁
可中断性支持不支持
超时机制支持不支持
条件变量支持多个条件变量不支持
锁的实现基于 AQS(AbstractQueuedSynchronizer)JVM 内置实现
性能高竞争下性能更好低竞争下性能更好
  1. Synchonized 锁
▼
java
复制代码
public class Demo01 { public static void main(String[] args) { Phone phone = new Phone(); new Thread(()->{ phone.sms(); },"A").start(); new Thread(()->{ phone.sms(); },"B").start(); } } class Phone{ public synchronized void sms(){ System.out.println(Thread.currentThread().getName()+"=> sms"); call();//这里也有一把锁 } public synchronized void call(){ System.out.println(Thread.currentThread().getName()+"=> call"); } }
  1. Lock 锁
▼
java
复制代码
//lock public class Demo02 { public static void main(String[] args) { Phone2 phone = new Phone2(); new Thread(()->{ phone.sms(); },"A").start(); new Thread(()->{ phone.sms(); },"B").start(); } } class Phone2{ Lock lock=new ReentrantLock(); public void sms(){ lock.lock(); //细节:这个是两把锁,两个钥匙 //lock锁必须配对,否则就会死锁在里面 try { System.out.println(Thread.currentThread().getName()+"=> sms"); call();//这里也有一把锁 } catch (Exception e) { e.printStackTrace(); }finally { lock.unlock(); } } public void call(){ lock.lock(); try { System.out.println(Thread.currentThread().getName() + "=> call"); }catch (Exception e){ e.printStackTrace(); } finally { lock.unlock(); } } }
  • lock锁必须配对,相当于lock和 unlock 必须数量相同;
  • 在外面加的锁,也可以在里面解锁;在里面加的锁,在外面也可以解锁;

3)自旋锁

  1. spinlock
▼
java
复制代码
public final int getAndAddInt(Object var1, long var2, int var4) { int var5; do { var5 = this.getIntVolatile(var1, var2); } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); return var5; }
  1. 自我设计自旋锁
▼
java
复制代码
public class SpinlockDemo { // 默认 // int 0 //thread null AtomicReference<Thread> atomicReference=new AtomicReference<>(); //加锁 public void myLock(){ Thread thread = Thread.currentThread(); System.out.println(thread.getName()+"===> mylock"); //自旋锁 while (!atomicReference.compareAndSet(null,thread)){ System.out.println(Thread.currentThread().getName()+" ==> 自旋中~"); } } //解锁 public void myUnlock(){ Thread thread=Thread.currentThread(); System.out.println(thread.getName()+"===> myUnlock"); atomicReference.compareAndSet(thread,null); } }
▼
java
复制代码
public class TestSpinLock { public static void main(String[] args) throws InterruptedException { ReentrantLock reentrantLock = new ReentrantLock(); reentrantLock.lock(); reentrantLock.unlock(); //使用CAS实现自旋锁 SpinlockDemo spinlockDemo=new SpinlockDemo(); new Thread(()->{ spinlockDemo.myLock(); try { TimeUnit.SECONDS.sleep(3); } catch (Exception e) { e.printStackTrace(); } finally { spinlockDemo.myUnlock(); } },"t1").start(); TimeUnit.SECONDS.sleep(1); new Thread(()->{ spinlockDemo.myLock(); try { TimeUnit.SECONDS.sleep(3); } catch (Exception e) { e.printStackTrace(); } finally { spinlockDemo.myUnlock(); } },"t2").start(); } }

运行结果:

t2进程必须等待t1进程Unlock后,才能Unlock,在这之前进行自旋等待。。。。

4)死锁

如何解开死锁

1、使用jps定位进程号,jdk的bin目录下: 有一个jps

命令:jps -l

image-20250305123309360

2、使用jstack 进程进程号 找到死锁信息

image-20200812214920583

0个评论
点击登录,快来和大家讨论吧~
表情
图片
暂无评论
下载 APP