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在写入的时候通过赋值一个新的数组修改,避免覆盖,造成数据错乱的问题;

CopyOnWriteArrayListVector厉害在哪里?

  • 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. 启动线程,执行 FutureTaskrun() 方法。
  5. FutureTask 内部调用 MyThreadcall() 方法,获取返回值。
  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)
    • 创建一个从 020_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 的初始化也是线程安全的。
  • 唯一性:静态变量 INSTANCEfinal 的,确保它只会被赋值一次,且不可修改。

静态内部类通过 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 包中的许多类(如 AtomicIntegerAtomicLong 等)都是基于 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