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(常用)


公平锁: 十分公平,必须先来后到~;
非公平锁: 十分不公平,可以插队;(默认为非公平锁)
▼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();

▼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。 - 这种设计适合单线程环境,但在多线程环境下可能会导致问题。
- 迭代器是 快速失败(fail-fast) 的,如果在迭代过程中集合被修改,会抛出
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

同样的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 是正常书写形式
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的构造器中啊

▼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; }
执行流程
- 创建
MyThread实例(实现了Callable)。 - 创建
FutureTask实例,包装MyThread。 - 创建
Thread实例,传入FutureTask。 - 启动线程,执行
FutureTask的run()方法。 FutureTask内部调用MyThread的call()方法,获取返回值。- 通过
FutureTask.get()获取任务结果并打印。
代码改进建议
-
线程池:
-
在实际开发中,建议使用线程池(如
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

▼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. 阻塞队列

1)BlockQueue
是Collection的一个子类
什么情况下我们会使用阻塞队列
生产者-消费者模型 多线程并发处理、线程池、任务限流、线程间数据交换(
SynchronousQueue)。

BlockingQueue 有四组api
| 方式 | 抛出异常 | 不会抛出异常,有返回值 | 阻塞,等待 | 超时等待 |
|---|---|---|---|---|
| 添加 | add | offer | put | offer(timenum.timeUnit) |
| 移出 | remove | poll | take | poll(timenum,timeUnit) |
| 判断队首元素 | element | peek | - | - |
▼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; }

阿里巴巴的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)拒绝策略
- new ThreadPoolExecutor.AbortPolicy(): //该拒绝策略为:银行满了,还有人进来,不处理这个人的,并抛出异常
超出最大承载,就会抛出异常:队列容量大小+maxPoolSize
-
new ThreadPoolExecutor.CallerRunsPolicy(): //该拒绝策略为:哪来的去哪里 main线程进行处理
-
**new ThreadPoolExecutor.DiscardPolicy(): **//该拒绝策略为:队列满了,丢掉异常,不会抛出异常。
-
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数的一倍到两倍之间。
下面是黑马的
12. 四大函数式接口
新时代的程序员:lambda表达式、链式编程、函数式接口、Stream流式计算
函数式接口:只有一个方法的接口

1)Function 函数型接口

▼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 断定型接口

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

▼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 核心思想->把大任务拆分为小任务!
1)ForkJoin 特点: 工作窃取!
实现原理是:双端队列!从上面和下面都可以去拿到任务进行执行!

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)使用一个并行流去计算整个计算,提高效率。
LongStream.range(0L, 20_0000_0000L):- 创建一个从
0到20_0000_0000L(不包括20_0000_0000L)的LongStream。 - 这个范围包含 20 亿个整数。
- 创建一个从
.parallel():- 将流转换为并行流。
- 并行流会将任务拆分为多个子任务,利用多核 CPU 并行执行。
.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(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定;


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操作之前,必须把此变量同步回主内存

遇到问题:程序不知道主存中的值已经被修改过了!;
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 ,怎么样保证原子性?

使用原子类

▼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实现的可见性)

4)总结
- volatile可以保证可见性;
- 不能保证原子性
- 由于内存屏障,可以保证避免指令重排的现象产生
面试官:那么你知道在哪里用这个内存屏障用得最多呢?单例模式
这里双重检测加锁是保证了操作原子性,只有一个线程能创建一个实例,其他线程无法创建第二个
volatile关键字是为了防止因为指令重排导致的多线程问题,有可能线程A创建一个实例,
虚拟机只执行了分配空间,对象地址引用这两步,这是线程B过来发现对象已经被创建了,但是获取到的对象是还没有被初始化的
18. 玩转单例模式
单例模式(Singleton Pattern)是一种创建型设计模式,确保一个类只有一个实例,并提供全局访问点。它常用于管理共享资源,如数据库连接、线程池等。
关键点
- 私有构造函数:防止外部直接实例化。
- 静态实例变量:保存类的唯一实例。
- 静态获取方法:提供全局访问点。
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); } }
使用枚举,线程安全,防止反射攻击

枚举类型的最终反编译源码:
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 的操作逻辑是:
- 比较内存地址
V中的值是否等于期望值E。 - 如果相等,则将内存地址
V的值更新为新值N。 - 如果不相等,则说明其他线程已经修改了值,操作失败。
CAS 是一个原子操作,由底层硬件(如 CPU)提供支持。
在 Java 中,CAS 操作是通过 sun.misc.Unsafe 类提供的本地方法实现的,例如:
▼java复制代码public final native boolean compareAndSwapInt(Object o, long offset, int expected, int x);
CAS 的底层实现原理
比较当前工作内存中的值 和 主内存中的值,如果这个值是期望的,那么则执行操作!如果不是就一直循环,使用的是自旋锁。
CAS 的底层依赖于 CPU 的原子指令。以下是一个简化的流程:
- 读取内存值:从内存中读取当前值。
- 比较值:将读取的值与期望值进行比较。
- 更新值:如果值相等,则更新为新值;否则,操作失败。
- 重试:如果操作失败,则重新尝试(自旋)。
CAS 的缺点
- ABA 问题:
- 问题描述:如果一个变量的值从
A变为B,又变回A,CAS 操作会认为值没有变化,但实际上已经被修改过。 - 解决方案:使用版本号或时间戳(如
AtomicStampedReference)。
- 问题描述:如果一个变量的值从
- 自旋开销:
- 在高竞争情况下,CAS 操作可能会失败多次,导致 CPU 空转。
- 只能保证一个变量的原子性:
- CAS 只能操作一个变量,无法保证多个变量的原子性。
Unsafe 类


2)什么是ABA问题?

线程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一定会创建新的对象分配新的内存空间。

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)公平锁,非公平锁
- 公平锁:非常公平,不能插队,必须先来后到
- 非公平锁:非常不公平,允许插队,可以改变顺序
2)可重入锁
可重入锁是指一个线程在持有锁的情况下,可以再次获取该锁而不会被阻塞。这种特性避免了线程因重复获取锁而导致的死锁问题。

ReentrantLock 的核心特性
(1)可重入性
- 同一个线程可以多次获取同一把锁。
- 每次获取锁后,必须释放相同次数的锁。
ReentrantLock 与 synchronized 的对比
| 特性 | ReentrantLock | synchronized |
|---|---|---|
| 可重入性 | 支持 | 支持 |
| 公平性 | 支持公平锁和非公平锁 | 非公平锁 |
| 可中断性 | 支持 | 不支持 |
| 超时机制 | 支持 | 不支持 |
| 条件变量 | 支持多个条件变量 | 不支持 |
| 锁的实现 | 基于 AQS(AbstractQueuedSynchronizer) | JVM 内置实现 |
| 性能 | 高竞争下性能更好 | 低竞争下性能更好 |
- 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"); } }
- 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)自旋锁
- 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; }
- 自我设计自旋锁
▼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
2、使用jstack 进程进程号 找到死锁信息

